/\_/\\
( o.o ) tailnet ready~
> ^ <
Native Rust building blocks for running a small, purpose-built Tailscale node
on an ESP32. The protocol layer uses std but does not depend on ESP-IDF, so it
can be tested on a normal host and integrated with different embedded runtimes.
This is an independent implementation. It is not affiliated with or endorsed by Tailscale Inc.
Important
The crate is usable for constrained applications, but it is not a drop-in
replacement for tailscaled. Read the support matrix
before selecting it for a deployment.
The ESP32-S3 reference build is tuned for constrained always-on appliances.
- Tailscale machine, node, discovery, challenge, and network-lock key types
- versioned persistent device identities with zeroized private key storage
- TS2021 Noise IK control-plane upgrade and EarlyNoise
- bounded HTTP/2 framing and HPACK
- interactive or auth-key registration and network-map decoding
- resumable streaming maps and incremental peer/DERP updates
- default-deny packet-filter evaluation
- WireGuard IKpsk2 handshakes, transport encryption, and anti-replay window
- authenticated DISCO ping/pong, CallMeMaybe, and endpoint selection
- authenticated DERP v2 relay fallback for restrictive NATs
- Tailscale-compatible STUN discovery
- transactional node-key rotation, including tailnet-lock re-signing
- generic IPv4/IPv6 dispatch, UDP parsing, and ICMP/ICMPv6 echo replies
- portable storage, clock, TCP, UDP, and optional packet-device traits
All parsers have explicit size limits. Private keys do not implement Debug,
and key material is zeroized when dropped.
The modules intentionally map to protocol boundaries:
identityandkey: persistent node identityclient,noise,h2, andcontrol: coordination server transportwireguard: encrypted peer sessionsdisco,stun,paths, andderp: direct and relayed pathsnetmap: peers, AllowedIPs, and ACL checksruntime: portable persistence, routing, retry, rotation, and packet-device APIs
Applications supply sockets, persistent storage, scheduling, and packet dispatch. This keeps policy and hardware choices outside the protocol crate.
The Wake-on-LAN firmware uses direct UDP when available and maintains an authenticated DERP connection as its hard-NAT fallback. It advertises its selected DERP region through control, processes CallMeMaybe messages, and reconnects resumable map streams when its endpoints change.
The crate does not contain a Tailscale account password or reusable auth key. An application creates a device identity, stores it in NVS, and sends its public keys to the Tailscale control plane. For interactive enrollment:
- The ESP32 generates machine, node, DISCO, and network-lock keys on first boot and persists the private keys locally.
- The registration response contains a one-time approval URL.
- The firmware prints that URL to its serial log.
- A user who is already signed into Tailscale opens the URL and approves the device for the intended tailnet.
- Tailscale assigns the node a
100.x.y.zaddress and MagicDNS name. - Later boots authenticate with the persisted device keys; the approval step does not repeat unless NVS is erased or the node is removed.
The Wake-on-LAN onboarding guide
shows the complete terminal and browser workflow. Applications may instead
call RegisterRequest::with_auth_key for unattended provisioning, but the auth
key must be supplied securely and must never be compiled into firmware.
use tailscale_esp32::control::HostInfo;
use tailscale_esp32::identity::DeviceIdentity;
let identity = DeviceIdentity::generate()?;
let persistent_bytes = identity.encode();
// Persist `persistent_bytes` in NVS and restore it on the next boot.
let restored = DeviceIdentity::decode(&persistent_bytes)?;
let host = HostInfo::esp32("sensor-node", "0123456789abcdef");
assert_eq!(
identity.machine_key().public(),
restored.machine_key().public()
);
# Ok::<(), Box<dyn std::error::Error>>(())See examples/wake-on-lan for a complete ESP-IDF
application that registers a node, maintains direct connectivity, enforces the
tailnet packet filter, and dispatches authenticated application packets.
The examples directory also contains small, host-runnable
application patterns that can be moved into an ESP-IDF data-plane task:
- an ACL-protected GPIO or relay command endpoint;
- a request/response temperature sensor with direct-to-DERP path fallback;
- an in-place ICMP status light.
Each example starts at the boundary where WireGuard has already authenticated and decrypted a packet. The Wake-on-LAN firmware remains the complete reference for Wi-Fi, enrollment, control maps, WireGuard, DISCO, and DERP integration.
Host-side validation:
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test --all-targets
cargo test --test derp_live -- --ignored # requires internet + openssl
cargo test --manifest-path examples/wake-on-lan/Cargo.toml \
--target x86_64-unknown-linux-gnuESP32-S3 build validation:
cd examples/wake-on-lan
cp .env.example .env
# Fill in local values, then:
./scripts/flash.shFlashing the example is not the final enrollment step. Follow the approval URL in the serial monitor as described in the example's onboarding guide.
The crate currently targets Rust 1.88 or newer. The reference firmware uses Espressif's Rust toolchain and ESP-IDF 5.4.3.
This code handles long-lived network identities and unauthenticated UDP input.
Treat a device identity like a VPN private key, use encrypted storage when the
physical threat model requires it, and never commit .env files. Please report
security issues privately as described in SECURITY.md.
MIT. See LICENSE.