tailscale-mpe-rust

A pure-Rust Tailscale client for the ESP32-S3 — control plane, WireGuard, disco, STUN & DERP, all from scratch.

github.com/punnerud/tailscale-mpe-rust

no_std core ~1.5 MB full demo ~6.5 Mbit/s dual-core ~20 ms in-tunnel latency LilyGO T-Dongle S3
Not the unrelated tailscale-rust crate — this is a from-scratch firmware plus a portable no_std protocol core. A ~$10 USB dongle becomes a real Tailscale node you can ping, browse to, and route through, running hand-rolled WireGuard crypto on a dual-core Xtensa LX7.

How big is Tailscale itself? Under 500 kB.

Subtract the WiFi + esp-idf runtime any networked ESP32 project already has (a 998 kB baseline), and the Tailscale functionality alone adds:

Adds on top of a WiFi baselineExtra flash
Control plane (ts2021) + WireGuard crypto+391 kB
  + working data plane (disco + STUN + direct UDP)+470 kB
  + DERP relay fallback (full remote reachability)+488 kB

A complete Tailscale node — control plane, WireGuard, NAT traversal and relay fallback — in under half a megabyte. The full demo (in-tunnel webserver, mDNS reflector, outbound, dual-core, …) adds +511 kB (~1.5 MB total). No tailscaled, no Go runtime.

And RAM? ~2 kB static.

Comparing total static SRAM is misleading — most of it (~116 kB here) is the shared esp-idf + WiFi framework that every networked ESP32 firmware already carries. On top of that baseline, the Tailscale layer adds only ~2 kB of static SRAM (its .data+.bss): the protocol runs from flash and the heap, not static RAM. (Runtime tunnel buffers + worker-thread stacks use heap on top, like any networked app.) So "how much RAM does adding Tailscale cost?" → about 2 kB.

How it compares

Other ESP32 Tailscale efforts, by static SRAM (the number this niche markets on). n/p = not published.

ProjectLangNative Tailscale nodeStatic SRAMPortable no_std core
tailscale-mpe-rustRust✅ full — ts2021 + WireGuard + disco + STUN + DERP~118 kB total · +~2 kB over a WiFi baseline✅ bare-metal riscv32
microlinkC✅ full~85–116 kB total
tailscale-iotC⚠ PoC / partialn/p
esp32-tailbridgeC/C++via a WireGuard bridgen/p
stock TailscaleGo✅ full (+ MagicDNS, …)needs an OS — won't fit an MCU

Total static SRAM is on par with the C client (~118 vs ~116 kB for a WiFi build — most of it is the shared esp-idf/WiFi framework both carry). The difference: Rust (memory-safe), a portable no_std core reusable beyond the ESP32, and a ~2 kB incremental RAM cost.

What works

Portable no_std core

Protocol logic lives in a platform-independent tailscale-core crate (#![no_std] + alloc); the ESP32 firmware is a thin adapter for the OS bits (WiFi/UDP/TLS, NVS, the ST7735 display). Migrated so far: icmp, stun, disco, wg, outbound, tcp, peers, and the pure parts of node. Proven genuinely std-less: the core + all crypto deps compile for a bare-metal target (riscv32imc-unknown-none-elf). The same wg.rs is reused unchanged by the host benchmark tool.

Benchmarks (T-Dongle S3 @ 240 MHz, both LX7 cores)

Measured with a host WireGuard load generator that handshakes directly with the dongle over the LAN (no path drift onto DERP), flooding inner UDP and reading the device's reflected RX rate.

MetricSingle-coreDual-core (default)
WireGuard decrypt throughput~3.9–4.0 Mbit/s~6.0–6.6 Mbit/s (~1.6×)
In-tunnel latency (ICMP RTT, median)~21 ms~20 ms
Latency min / packet loss~13 ms / 0%~13 ms / 0%

Bottleneck: pure-Rust ChaCha20-Poly1305 on the LX7 (no hardware ChaCha). Dual-core decrypts on both cores in parallel with unchanged latency — a strict win. Great for control / IoT / SSH / discovery; not a bulk-transfer gateway.

Flash size

App image size (bytes written to flash), via espflash save-image.

BuildApp imageΔ
Baseline — WiFi + ST7735 display only998 kB
+ ts — control plane + WireGuard + crypto/TLS1389 kB+391
+ direct — disco + STUN + UDP data plane1468 kB+79
+ derp — encrypted relay client1486 kB+18
Default — full Tailscale demo (all features)1509 kB

~1.5 MB total — a small fraction of the dongle's flash. WiFi SSID / password (and an optional auth key) are build-time options you fill into src/config.rs (git-ignored).

Extra flash per feature

FeatureExtraAdds
ts+391 kBcontrol plane + WireGuard + crypto + TLS (foundational)
direct+79 kBdisco + STUN + UDP data plane (LAN + NAT-punch)
derp+18 kBencrypted relay fallback
mdns-forward+6 kBmDNS/Bonjour reflector across LANs
outbound+5 kBdevice-initiated ICMP/UDP/TCP out the tunnel
http-server+4 kBin-tunnel TCP + the HTML web demo
icmp+2 kBanswer ping
birthday+2 kBbirthday-paradox port spray (symmetric NAT)
dualcore+2 kB2-core parallel decrypt (+60% throughput)
packet-filter+1 kBenforce netmap ACLs
derp-upgrade+1 kBupgrade relayed peers to a direct path
authkey~0 kBheadless pre-auth-key provisioning
bench (opt-in)+2 kBUDP throughput sink + RX reflection
subnet-router (opt-in)+1 kBNAPT data-path foundation
tcp-proxy (opt-in)+3 kBin-tunnel TCP → a fixed LAN host:port

Build & flash

# 1. Provide your WiFi creds (git-ignored, never committed)
cp src/config.rs.example src/config.rs
$EDITOR src/config.rs        # set WIFI_SSID + WIFI_PASS

# 2. Build + flash (esp-rs Xtensa toolchain + espflash)
. ~/export-esp.sh
cargo build --release
espflash flash --monitor --port /dev/cu.usbmodemXXXX \
  --bootloader  target/xtensa-esp32s3-espidf/release/bootloader.bin \
  --partition-table target/xtensa-esp32s3-espidf/release/partition-table.bin \
  target/xtensa-esp32s3-espidf/release/tailscale-rust

First boot without an auth key prints a login URL on the serial console — open it to add the node to your tailnet. Then ping 100.x.y.z or browse http://100.x.y.z/.

Motivation — a low-latency nervous system for machines

Humanoid robots, vacuum cleaners, drones, self-driving cars and boats — the coming wave of autonomous machines has to coordinate, and coordination is bounded by latency. A cloud round-trip costs tens to hundreds of milliseconds; two machines in the same room, or across town over 5G, can reach each other directly in a few.

Tailscale already gives every device a flat, encrypted, NAT-traversing address space where peers connect directly, peer-to-peer (hole-punched WireGuard), relaying only when they truly must — exactly the substrate machines need: local-first, lowest-latency, no central server in the hot path.

The catch: the stock Tailscale stack (Go + tailscaled) is too heavy for the cheapest, most numerous devices — the microcontrollers that will live inside those robots and appliances. This project shows the whole client fits in under half a megabyte of portable, no-std Rust on a ~$10 chip. So the smallest, cheapest device can be a first-class mesh node — not a second-class thing tethered to a gateway or a cloud account.

If every machine can securely find and reach every other machine — directly, privately, at the lowest possible latency, on hardware anyone can afford — that is an enabler for an abundant, decentralized future for the benefit of all. Lifting all boats.

Sibling project: mpee is the fleet-routing brain (optimize who goes where); tailscale-mpe-rust is the low-latency nervous system (let them all talk, directly).