All notable changes to tailscaled-rs are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Pre-1.0, experimental. This is a from-scratch Rust system daemon —
tailnetd(the daemon) plustnet(a thin CLI) — built on thetailscale-rsengine, adding the layer the embeddable engine omits: an IPN-style state machine, persisted preferences, and a LocalAPI over a Unix domain socket. The engine refuses to run unlessTS_RS_EXPERIMENT=this_is_unstable_softwareis set, and so does this daemon. Interfaces (LocalAPI, prefs schema, CLI flags) are unstable and may change without notice while we are below 1.0. Not affiliated with, endorsed by, or sponsored by Tailscale Inc.; "Tailscale" and "WireGuard" are used nominatively only.
Releases are driven by Conventional Commits:
feat:→ minor bump.fix:→ patch bump.chore:/docs:/style:/refactor:/test:→ no release on their own.- Because the project is pre-1.0, breaking changes may land in a minor bump
(and are called out under Changed) rather than forcing a major bump. The major
version stays at
0until the LocalAPI, prefs schema, and CLI are declared stable.
0.52.0 (2026-06-15)
- tailnetd: emit sd_notify(READY=1) + flip systemd units to Type=notify (#233) (2dd488d)
- tnet: add switch --list --json (Go tailscale switch --list --json) (#235) (069d9eb)
- tnet: exit-node suggest + bump engine v0.40.0 → v0.41.0 (ask #24) (#239) (024d85b)
- tnet: is_ssh_over_tailscale reads /proc session-leader environ under sudo (#238) (cd1f0da)
0.51.0 (2026-06-15)
- install: wire systemd ExecStopPost=--cleanup and operator EnvironmentFile (#225) (8a1bf4c)
- tailnetd: add --port / PORT — consume Config.wireguard_listen_port (#231) (62ea1c9)
- tnet: add debug local-creds + debug stat (Go tailscale debug parity) (#227) (2e09b7f)
- tnet: add debug restun (Go tailscale debug restun) — consume Device::re_stun (#230) (b423e3a)
- tnet: add ssh — host-key-pinned ssh over the tailnet (v0.40.0) (#232) (97e411a)
- tnet: web UI login affordance — surface the auth URL (#223) (f07fbc3)
0.50.0 (2026-06-15)
- daemon: add --debug HTTP server exposing GET /debug/metrics (tsd-iqq.9) (#215) (11ec005)
- localapi: WatchNotifications phase 1 — masked Notify stream (tsd-iqq.11) (#220) (16679dc)
- localapi: WatchNotifications phase 2 — prefs-change broadcast (#221) (aba1a44)
- tnet: add up --json (Go tailscale up --json) (#219) (539a99a)
- tnet: netcheck --format json|json-line + --every (tsd-iqq.13) (#218) (6f31097)
0.49.0 (2026-06-15)
- localapi: add check-ip-forwarding + check-prefs verbs (tsd-iqq.8) (#208) (68c48b3)
- tnet: add ip --assert and whois --json (CLI-flag parity, tsd-iqq.13) (#209) (5ce4f65)
- tnet: add reload-config LocalAPI verb + running-config adoption (tsd-iqq.7) (#214) (16b1fe5)
- cli: neutralize U+2028/U+2029 + bidi overrides in terminal sanitizers (tsd-ct5) (#212) (a9783c8)
- funnel: warn on non-loopback backend; correct stale CI runner comment (#211) (1bec62a)
0.48.0 (2026-06-15)
- daemon: add --cleanup, --no-logs-no-support, and rich --version (#201) (9140508)
- tnet: add
debug rebind(force engine UDP socket rebind) (#203) (f102544) - tnet: add
debug viafor 4via6 route encode/decode (#202) (badfd03)
- conffile: match Go ConfigVAlpha wire keys; drop non-Go AdvertiseTags (#205) (1379087)
- httpproxy: bound and harden the Tailscale-Connect-Error header (#204) (afb707f)
- serve: atomic config save + funnel-off clears stale-host key (#207) (8ae6ea5)
- status: Go-faithful exit-node ID, --watch json/filters, peer IPv6 (#206) (0d3eb90)
- up: wipe key before --reset mutates in-memory prefs (#199) (497fd19)
0.47.1 (2026-06-14)
- cert: write the private key atomically (temp+fsync+rename), guard the filename (#195) (fa0a045)
- conffile: validate --config fields before persisting; warn on Locked (#198) (08a72da)
- localapi: separate cap + timeouts for long-lived Watch/Nc streams (DoS) (#197) (cad049c)
0.47.0 (2026-06-14)
- daemon: outbound HTTP proxy over the tailnet (--outbound-http-proxy-listen) (#191) (848c1ca)
- daemon: SOCKS5 proxy that dials over the tailnet (--socks5-server) (#189) (3fa605b)
- debug: add
tnet debug envandtnet debug metrics(Go parity) (#192) (e341b99)
0.46.0 (2026-06-14)
- funnel: resolve the splice backend from the Web map, not just tcp_forward (#181) (b5881d7)
- revert-guard: correct ephemeral exemption docs + enforce UpOptions↔guard lockstep (#185) (c82e43a)
- taildrop: set the quarantine attribute BEFORE copying received bytes (#183) (53f84a3)
0.45.0 (2026-06-13)
0.44.1 (2026-06-13)
- update: harden the self-replace path (decompression-bomb cap, O_EXCL temp, explicit oversize) (#172) (6442a15)
0.44.0 (2026-06-13)
- cli: add
tnet update(Gotailscale update) — version-check + verified self-install (#169) (b7adb4d) - cli: add
tnet web(Gotailscale web) — read-only status web UI (#171) (e6132ca)
0.43.0 (2026-06-13)
- cli: add
tnet login+ reframe DESIGN scope to full Go parity (no "non-goals") (#165) (7914e8b) - cli: bump engine v0.32.0 → v0.33.0 + add
tnet lock init(complete the lock surface) (#168) (bf0e58e)
0.42.0 (2026-06-13)
0.41.0 (2026-06-13)
- cli: add
tnet cert(Go tailscale cert), consuming engine #16 (#127) (2dbb124) - cli: add
tnet debug prefs(Gotailscale debug prefs) (#143) (367d0c5) - cli: add
tnet dns query(Gotailscale dns query) (#158) (4735a8a) - cli: add
tnet licenses(Gotailscale licenses) (#142) (87f2dfb) - cli: add
tnet metrics printsubcommand (Gotailscale metrics print) (#144) (3be6310) - cli: add workload-identity-federation up flags (--client-id/--client-secret/--id-token/--audience) (#154) (3df6101)
- cli: tnet wait/up --timeout confirms the kernel TUN interface carries the IP (#155) (388df12)
- cli: wire accept-dns (Go --accept-dns / CorpDNS), consuming engine #14 (#126) (06640b8)
- file: Go cp colon-target syntax + variadic files + --targets (closes tsd-x4i) (#137) (1807be3)
- file: Go-faithful
file get <dir>inbox drain + --conflict (fixes silent-overwrite data loss) (#136) (77c037e) - get: add
get --set-flags(Gotailscale get --set-flags) (#149) (6cbaa58) - get: surface hostname in
tnet get(Go parity) + fix flaky conffile test temp path (#148) (1180b43) - install: TUN-relaxed systemd unit for tun-feature builds (tsd-9qm) (#138) (eb8e522)
- ping: --until-direct + Go-faithful -c default + direct/DERP path reporting (#135) (9c83b56)
- serve: model Go's top-level ServeConfig.Web map (tsd-6p4 stage A) (#129) (30d7522)
- serve: serve + render the Go ServeConfig.Web map (tsd-6p4 stage B, read side) (#130) (0f1c7ca)
- serve: serve TLS-terminated raw-TCP forwards (Go --tls-terminated-tcp) (#128) (6787ad8)
- serve: tnet serve authors the Go Web map (tsd-6p4 stage B2, write side) (#131) (33ad18a)
- tailnetd: --config declarative config file (Go ipn.ConfigVAlpha, closes tsd-bin) (#140) (8e72b0c)
- tailnetd: Go-style CLI flags (--statedir/--socket/--verbose/--version) (#139) (f7c5d3c)
- cli: align netcheck/serve/get diagnostic output with Go v1.100.0 (#122) (f0788e3)
- cli: byte-match Go's lock-status wording + correct file-command doc miscites (#123) (1a65ea5)
- cli: neutralize column/row injection from control-supplied names in terminal output (#152) (51b5d30)
- cli: reset SIGPIPE to default so broken output pipes exit cleanly (not a panic) (#151) (2fb9130)
- cli: sanitize control-supplied diagnostic output + correct netcheck JSON claims (#124) (19a4273)
- exit-node: reject
auto:selector instead of silently breaking exit routing (#119) (27cd239) - file: wire
cp --nameto the actual transfer (was silently a no-op) (#141) (2b178ba) - ipn: gate Stopped on a persisted node key (Go hasNodeKeyLocked) + honest dns-status --json doc (#121) (8556063)
- ipn: persist has_logged_in on set-rebuild + clear it on logout (review follow-ups) (#161) (c29128d)
- ipn: revert-guard fresh-node exemption keys on has-logged-in, not prefs-file existence (#156) (14dbde7)
- release: ship a full-featured daemon (tun,ssh,acme) — released binaries were feature-less (#133) (b09fa10)
- status: emit RFC3339 timestamps (Go-ipnstate-compatible), not chrono Display (#147) (6476b1b)
- up: default to a persistent node + add --ephemeral/--no-ephemeral (Go parity, tsd-4qt) (#134) (d46bbee)
0.40.1 (2026-06-12)
0.40.0 (2026-06-12)
0.39.0 (2026-06-12)
0.38.0 (2026-06-12)
0.37.0 (2026-06-12)
0.36.0 (2026-06-12)
- risk: enforce lose-ssh on an SSH-server toggle over Tailscale SSH (completes the risk) (#101) (e9db2b8)
0.35.0 (2026-06-12)
0.34.1 (2026-06-12)
0.34.0 (2026-06-12)
0.33.0 (2026-06-11)
- version: version.Meta JSON shape (incl. cap) + bugreport [note] + version --upstream stub (#93) (56b2236)
0.32.0 (2026-06-11)
0.31.0 (2026-06-11)
0.30.0 (2026-06-11)
0.29.0 (2026-06-11)
0.28.0 (2026-06-11)
0.27.0 (2026-06-11)
0.26.0 (2026-06-11)
0.25.0 (2026-06-11)
0.24.0 (2026-06-11)
0.23.0 (2026-06-11)
0.22.0 (2026-06-11)
0.21.0 (2026-06-11)
- status: engine bump v0.21.2 + enrich tnet status (relay/direct, last-seen, allowed-routes, active-exit) (#69) (eb6c187)
0.20.0 (2026-06-11)
0.19.0 (2026-06-11)
0.18.0 (2026-06-11)
0.17.0 (2026-06-11)
0.16.0 (2026-06-11)
- serve: ServeConfig model + persistence + tnet serve tcp/status/reset (#58) (63d8aa5)
- serve: TCP-forward accept loops (serve --tcp now serves traffic) (#60) (0e65d1d)
0.15.0 (2026-06-10)
0.14.0 (2026-06-10)
0.13.0 (2026-06-10)
- bugreport: tnet bugreport — local diagnostic marker (Go parity, honest) (#52) (7348a16)
- up,set: advertise-tags pref (Go --advertise-tags parity) (#50) (7b04fe7)
0.12.0 (2026-06-10)
0.11.0 (2026-06-10)
- ip: tnet ip -4/-6/-1 and [peer] (Go parity) (#45) (1868ac7)
- profiles: multi-profile state + tnet switch (Go parity) (#46) (fc5aa7e)
- status: --active / --no-peers / --no-self filters (Go parity) (#44) (9fc2826)
- status: tnet status --json (Go ipnstate.Status-shaped subset) (#42) (46447e3)
0.10.0 (2026-06-10)
- get: tnet get — show current prefs (Go parity) (#39) (1b64d6b)
- version: tnet version (+ --daemon, --json) — Go parity (#38) (8f3b711)
- wait,whoami: tnet wait + whoami (Go parity) (#40) (407993c)
0.9.0 (2026-06-10)
0.8.0 (2026-06-10)
- localapi: run read/file engine calls off-lock + harden Taildrop paths (#28) (10432e8)
- nits: consistent clear-flag naming, tun resolver helper, comment cleanup (#31) (63fa6c7)
0.7.0 (2026-06-10)
0.6.0 (2026-06-10)
- diag: tnet ip / whois / ping diagnostics (tsd-iqq.2 part 1) (#23) (9903696)
- file: Taildrop send/receive (tsd-qw8) (#24) (016e072)
- status: surface configured posture in status (tsd-iqq.4 part 1) (#21) (34ca3a5)
0.5.0 (2026-06-09)
- exit: leak-safety guard + documented invariant (tsd-iqq.3) (#19) (53bbd22)
- set: tnet set — live pref mutation without up/down (tsd-iqq.1) (#15) (c918cc1)
- ssh: Tailscale SSH server (tsd-46c) (#17) (3b17972)
0.4.0 (2026-06-09)
0.3.1 (2026-06-09)
0.3.0 (2026-06-09)
0.2.2 (2026-06-09)
0.2.1 (2026-06-09)
0.2.0 (2026-06-09)
- login: interactive/browser login — surface the control auth URL (tsd-8j2) (862a708)
- status: 'tnet status --watch' streaming over LocalAPI (tsd-c3p) (c93e492)
- tun: Phase-3 TUN-mode plumbing (daemon-ready; blocked on one engine export) (9275693)
- tun: wire kernel-TUN transport on engine v0.6.7 (tsd-tth) (a86fb3b)
- bump engine to v0.6.9 + correct macOS-TUN engine ask #6 (3f9675c)
- ci: gate the utun-name test to macOS so Linux+tun compiles (eb6fee3)
- review: bound status() netmap query + drop vestigial param (a85c324)
- review: off-lock SIGHUP reload, --no-tun, boot-attempt guard, hardening (814ccef)
- tun: default macOS TUN name to utun (engine default tailscale0 is rejected) (7b4c41d)
Nothing yet.
0.1.0 - 2026-06-08
The initial MVP of the standalone daemon, hardened and reviewed.
- MVP daemon (
tailnetd) + CLI (tnet). A from-scratch, BSD-3-Clause Rust system daemon built on thetailscale-rsengine, supplying the daemon layer the embeddable engine omits: an IPN-style state machine, persisted preferences, and a LocalAPI over a Unix domain socket. In MVP (Phase 1, userspace networking) it joins a tailnet with a pre-auth key, reachesRunning, and answersstatus/up/down. Verified end-to-end against a live tailnet. - Pre-auth-key handling that keeps keys out of argv/history.
tnetgains--authkey-fileand$TS_AUTH_KEY(precedence: file > flag > env). - SO_PEERCRED authorization on the LocalAPI socket. Reads are open; writes
(
up/down) require root or the daemon's owner, and authorization fails closed if the peer-credential lookup errors. Computed per-connection before the stream split. - Control-plane URL override. A
control_urlfrom prefs/CLI is parsed and applied to the engine config onup(); a malformed URL fails loudly instead of silently falling back to the default control plane. - Extended IPN state parity.
NeedsMachineAuth/InUseOtherUseradded to the state model with honest LIMITATION docs (the engine surfaces no machine-auth signal — documented, not fabricated);derive_stateextracted as a pure, unit-tested helper. - Secure state directory.
ensure_state_dir_secureenforces0700on the state directory before any key file is written. - Tests. LocalAPI integration tests over a real UDS plus a state-machine matrix;
39 tests pass,
clippy -D warningsclean.
- Crate renamed to
tailscaled-rs(imported astailscaled_rs); the installed binaries deliberately staytailnetd+tnetso they never collide onPATHwith a real Tailscale install. Added repository metadata. No behavior change.Publishing to crates.io remains blocked until the
tailscale-rsengine is published there (cargo publishrejects the git dependency). control_urlprecedence made explicit. The engine config is now built fromConfig::default_from_env()soTS_CONTROL_URLis honored, withprefs.control_urloverriding last (precedence: prefs > env > default). HTTP/HTTPS scheme validation added; acontrol_url.rstest pins the parse + scheme contract.- Authorization model simplified. Collapsed
Permissions{read,write}(a dead field) into a 2-variantAccessenum; introduced anAuthPolicybuilt once at startup (the operator-GID seam) instead of a per-call euid lookup; madecurrent_euidprivate. A pureauthorize(&Request, Access) -> Result<(), Denied>was extracted so the security-critical deny path is unit-tested directly. - Prefs are forward/backward compatible via a container-level
#[serde(default)]onprefs.json. - LICENSE made canonically detectable.
LICENSEis now the verbatim BSD-3-Clause template (GitHub auto-detect); the upstream-derivation explanation and trademark notices moved toNOTICE, with both copyright holders in the copyright line and Tailscale Inc. attribution retained per clause 1.
ever_configuredsurvives restart. It now derives from prefs-file existence, so theNoStatevsStoppeddistinction holds across a restart (previously an up→down→restart wrongly reportedNoState; now reportsStopped).- Engine status errors are logged, not swallowed.
status()errors are logged instead of silently downgradingRunning→Starting. - Empty pre-auth keys are ignored consistently. An empty
TS_AUTH_KEYis filtered in the daemon auto-start path, matching the CLI.
- Peercred-gated writes (fail-closed).
up/downrequire root or the daemon owner viaSO_PEERCRED; a credential-lookup error denies the request. - Secrets handled as
secrecy::SecretStringend-to-end (CLI, daemon, auto-start), exposed only at serialization and the engine call, never logged; a Debug-redaction test pins this. 0700enforced on the socket's parent directory insideserve()rather than trusting the caller.- LocalAPI server hardening: request line length capped at 64 KiB (anti-OOM);
in-flight connections drained via a
JoinSetwith a 2 s bound on shutdown; concurrent connections capped with aSemaphore.