Skip to content

Releases: GeiserX/tailscale-rs

v0.8.1

10 Jun 03:07

Choose a tag to compare

Fixed — DERP connectivity floor for no-region peers (#24)

A peer whose netmap entry carried no DERP home region (derp_region == None) was given no underlay route at all — the route updater skipped it, so the outbound router dropped every WireGuard packet to it (handshake included). The failure was symmetric (the dataplane's only egress is the underlay route table → we could neither initiate to nor respond to such a peer) and presented as a 30s dial timeout. This is the live blocker for routing through a NAT'd peer on a self-hosted control plane (e.g. Headscale) that doesn't echo preferred_derp.

DERP is the connectivity floor in Tailscale; this restores it. When the netmap supplies no region, the relay region is now inferred — mirroring Go magicsock's c.derpRoute:

  1. Observed route — a region we have actually received a DERP frame from the peer on (it is demonstrably listening there).
  2. Home-region last resort — our own current home region, a bounded interop-safe fallback that rendezvouses a co-regional peer even when control never echoes the peer's region. If the peer is not on that region the DERP server simply drops the relayed frame (no host dial, no leak).

The inference is gated on the region having a live transport task, and is consulted both for the WireGuard underlay route and for the CallMeMaybe direct-path prompt — so a no-region peer also gets its direct-path upgrade attempted instead of being silently skipped.

Anti-leak posture preserved: the inferred region only ever resolves to a DERP transport, never the direct host-dial path. Observed routes are pruned to the live netmap.

Patch bump (internal route-layer change; no public API change). All 43 geiserx_* crates published to crates.io.

Consumers: pure pin bump to 0.8.1, no code change.

v0.8.0 — Device::new_with_secret (SecretString auth key)

10 Jun 01:21

Choose a tag to compare

Added

  • Device::new_with_secret(&Config, Option<secrecy::SecretString>) — a back-compat secret-typed constructor for embedders that hold the registration auth key as a secrecy::SecretString (e.g. a daemon keeping the key zeroized end-to-end). The caller no longer materializes the secret into a plain String at the engine boundary. Device::new(Option<String>) is unchanged. (Honest scope: the engine still resolves the key to a String internally for registration — this closes the caller's plaintext window; engine-side key zeroization is tracked separately.) Adds a secrecy dependency (pure-Rust — no aws-lc/openssl/ring, the ring-only egress invariant is preserved) and re-exports tailscale::SecretString. Minor bump (additive API).
  • docs/LIVE_SETTABLE_PREFS.md — documents which Device prefs are live-settable on a running device (set_exit_node, set_serve_config, logout) vs which require a Device::new rebuild.

This project is not associated with Tailscale Inc.

v0.7.3 — enforce SSH session-recording policy (close bypass)

10 Jun 00:37

Choose a tag to compare

Fixed (security)

SSH session-recording policy is now enforced — a silent bypass is closed. The SSH server (ssh feature) parsed an SSHAction's recorders / on_recording_failure / hold_and_delegate off the wire but dropped them in the domain conversion, so a control policy demanding "record this session or refuse it" was silently downgraded to a plain accept. Those fields are now carried into the domain action and the server applies a fail-closed gate: when a matched rule requires recording (non-empty recorders) but no recorder transport is available, the session is refused (with the policy's reject_session_with_message if set). This matches Go tailssh's posture when reject-on-failure is configured. A hold_and_delegate (check-mode) rule is likewise not silently accepted. The common no-recording path is unchanged.

Deferred follow-ups: the recorder transport (dial recorders + asciinema/CastV2 PTY stream — after which the interim fail-closed relaxes to Go's fail-open-unless-reject-on-failure default), and the hold_and_delegate delegate round-trip.


This project is not associated with Tailscale Inc.

v0.7.2 — session-lifetime correctness

10 Jun 00:02

Choose a tag to compare

Fixed (internal; no public API change)

Four session-lifetime fixes surfaced during the #20/#21 handshake-race review:

  • confirm() bounds the tentative responder session — rejects a delayed/replayed (still-AEAD-valid) transport packet from activating a stale not-yet-confirmed responder session. Mirrors WireGuard bounding a not-yet-confirmed keypair by the reject-after time.
  • get_recv previous-session expiry fix — the recv_prev branch checked the current session's expiry instead of the previous one's.
  • Receive-session id leak fixed — a transmit-session expiry that reset the session state dropped the receive sessions without freeing their ids (unreclaimable leak on long-lived hosts); now routed through the id-freeing path.
  • WireGuard timer constants — named REKEY_AFTER_TIME (120s) + REJECT_AFTER_TIME (180s, spec) on the transmit side; the receive bound is kept lenient (REJECT_AFTER_TIME_RECV = 240s) because this fork rekeys only on outbound traffic — a strict 180s receive bound would drop inbound traffic on a send-idle session until receive-triggered rekey exists.

Go interop KAT + the #19 keepalive / #20 simultaneous-init tests all pass.


This project is not associated with Tailscale Inc.

v0.7.1 — fix simultaneous-initiation handshake race (#20)

09 Jun 22:41

Choose a tag to compare

Fixed

Simultaneous-initiation handshake race no longer wedges relay-only peers (closes #20).

Two peers reachable only via a DERP relay (no direct path) that initiated at the same cadence deterministically failed to ever complete a handshakeHandshake::respond unconditionally overwrote an in-flight Initiated with Responded, so the peer's later HandshakeResponse failed finish()'s state guard → handshake failed to complete + session not found looped every ~5.5s forever. A direct path's timing jitter masks this; a relay-only idle path removes the jitter and the race becomes the steady state.

Fixed by mirroring canonical WireGuard (wireguard-go / boringtun / Linux kernel — none use a deterministic public-key tie-break, which would be interop-unsafe): the per-peer handshake now retains both an in-flight initiator slot and a responder slot at once, so an inbound initiation no longer destroys the pending one. Both handshakes complete; the responder session stays provisional (send-disabled) until a confirming transport packet; the existing receive-session rotation converges them. We always respond to a peer's msg1, so interop with real wireguard-go/Tailscale/kernel peers is unchanged — the Go transport-key KAT still passes byte-for-byte.

Internal change only — no public API change (patch release). Also hardened the flaky ts_forwarder UDP-timeout test against false reds on the loaded self-hosted CI runner.


This project is not associated with Tailscale Inc.

v0.7.0 — idle-wedge fix (persistent-keepalive) + crypto-verification tooling

09 Jun 20:54

Choose a tag to compare

Fixed

Idle DERP-relayed sessions no longer wedge (persistent-keepalive + periodic tick). A purely event-driven WireGuard endpoint advances its timers only on packet send/recv, so an idle tunnel went silent ~10s after the last packet and the dataplane then blocked forever on I/O with no timer wakeup — the NAT/relay mapping went cold and the next dial could never re-handshake (504 until restart). Fix: a per-peer persistent keepalive (default 25s, opt-in via PeerConfig::persistent_keepalive_interval) that re-arms unconditionally and emits an empty authenticated packet to hold the path warm (no upward jitter, stays under the ~30s NAT floor, does not advance the rekey clock), plus a clock-driven periodic tick so it fires on a truly idle tunnel. WG semantics verified canonical against the spec + boringtun + wireguard-go.

Added (verification / hardening — no runtime behavior change)

Cryptographic-verification tooling: a direct BLAKE2s-256 KAT (incl. the 16-byte cookie-MAC short-key path), a dudect constant-time leakage-detection bench over the AEAD tag-verify path, a cargo-fuzz target for the Tailnet-Lock CBOR decoder + a stable-CI smoke test + a Go fxamacker/cbor differential oracle, and docs/CRYPTO_VERIFICATION_STATUS.md (a four-axis framing of what "cryptographically verified" means here). Reconciled docs/CRYPTOGRAPHY.md §6 with the lockfile.

API note

Adds a public persistent_keepalive_interval: Option<Duration> field to PeerConfig/Config — a source-visible addition, hence the minor bump (0.6.10 → 0.7.0). None preserves the prior purely-reactive behavior.


This project is not associated with Tailscale Inc.

v0.6.10 — peer-patch idle re-handshake + macOS route fix

09 Jun 16:54

Choose a tag to compare

Fixed

Incremental peer patches (MapResponse.peers_changed_patch) are now applied, not dropped.
The map-stream decoder logged and discarded these patches, so the per-peer updates control sends mid-session — chiefly a peer's UDP endpoints and home DERP region when an idle peer re-establishes connectivity — never reached the netmap. magicsock kept stale endpoints and couldn't re-handshake the moved peer, wedging idle sessions (observed as a ~3-minute idle failure in the exit-node egress path). Patches now surface as a new PeerUpdate::Patch and are merged in the peer tracker:

  • looked up by node id (an unknown id is ignored — a patch never creates a node),
  • only the fields the patch carries are merged onto the existing node,
  • the tailnet-lock (TKA) gate is re-run before upsert, so a key-rotation patch can't bypass trust enforcement — a patch whose new signature fails verification evicts the peer (fail-closed).

A full/delta resync in the same response still takes precedence.

macOS TUN bring-up no longer fails with "No such file or directory (os error 2)".
The host-networking layer invoked route(8) at /usr/sbin/route, which is the Linux/iproute2 location and does not exist on macOS (macOS ships route(8) in /sbin). The missing binary made every route install fail with ENOENT, which the TUN actor treats as fatal and fail-closes — so the interface never came up. Corrected to /sbin/route. (scutil(8) was already correct.)


This project is not associated with Tailscale Inc.

v0.6.9 — systematic netmap null-tolerance

09 Jun 10:20

Choose a tag to compare

Fixed

Netmap decode now tolerates null for every sequence/map field (Go omitempty ↔ Rust).

Go marshals empty slices/maps as JSON null, so a control plane (notably an IPv6-off Headscale) sends null for array fields the client modeled as required sequences — failing the netmap decode with invalid type: null, expected a sequence/expected a map and looping the map-poll stream forever.

v0.6.8 fixed only Node.addresses. v0.6.9 is the systematic pass: rather than annotate each field (the per-field approach is what let the gap recur), null tolerance is applied at the struct level via #[serde_with::apply] on every type on the deserialized netmap path — Node, MapResponse, DNSConfig/Resolver, DerpMap/Region/HomeParams, SSHPolicy and its nested rules, ControlDialPlan, and the ts_packetfilter_serde filter/cap-grant types — so any Vec/map field added later is covered automatically.

null, []/{}, and a populated container are accepted interchangeably. Option<…> fields (whose null/absence means unchanged from the prior poll — e.g. peers, packet_filter singular) are deliberately left untouched, preserving delta-poll semantics.

Regression tests decode a full MapResponse + peer Node + DNSConfig, a DERP map, an SSH policy, a packet filter, and a dial plan with null everywhere a sequence/map is expected.


This project is not associated with Tailscale Inc.

v0.6.8

08 Jun 23:24
68b5fbb

Choose a tag to compare

Automated C-library binaries for v0.6.8. See CHANGELOG.md.

v0.6.7

08 Jun 22:28
a005920

Choose a tag to compare

Automated C-library binaries for v0.6.7. See CHANGELOG.md.