tailscale 0.5.0
tailscale: ^0.5.0 copied to clipboard
Embed Tailscale userspace networking in any Dart or Flutter app. Join a tailnet, discover peers, and communicate over encrypted WireGuard tunnels — no Tailscale app required.
0.5.0 #
A feature and performance release. Inbound connections and HTTP requests now
carry the calling node's Tailscale identity, resolved at accept time, and the
LocalAPI loopback that backs whois() is dramatically faster.
Features:
- Inbound TCP/TLS connections accepted from a listener now expose the calling
node's identity as
TailscaleConnection.identity(TailscaleNodeIdentity?) — host name, stable node ID, tags, and tailnet IPs. It isnullfor outboundtcp.dial(you chose the target) and when the caller can't be resolved; resolution is best-effort and never blocks or fails an accept. - Inbound
Http.bindrequests expose the same viaTailscaleHttpRequest.identity— the in-process counterpart to theTailscale-User-Loginheaders thatserve.forwardadds.nullfor public Funnel callers, which originate outside the tailnet.
Performance:
whois()(and every other LocalAPI call) no longer forkslsofper request. The loopback auth path was hunting a macOS GUI credential file that an embedded tsnet process can never have; skipping it cuts awhois()round-trip from ~40 ms to ~0.3 ms.- Attaching identity to an inbound connection is a ~80 ns read from an in-memory index mirrored from the netmap, not a per-accept LocalAPI round-trip. The index falls back to a live lookup while cold and is dropped on teardown, so identity is never stale.
0.4.0 #
A security and reliability release. It hardens the embedded-tsnet data plane, tightens credential handling, and updates the bundled Tailscale stack. No public API shape changes.
Security:
- Tailscale Funnel forwarding now strips reserved
Tailscale-*identity headers (Tailscale-User-Login, etc.) from inbound public requests before proxying to the loopback backend, and pinsX-Forwarded-Proto/-Host. Previously a public Funnel client could spoof these headers — which are only trustworthy on the authenticated Serve path — straight through to the backend. logout()now best-effort revokes the node key with the control plane before wiping local state (same on re-auth viaup()with a new auth key). A surviving copy of the state database (e.g. a backup) is therefore no longer a live credential, and the device is deregistered rather than lingering until key expiry.- The state database (node and machine private keys) is created owner-only
(
0600, including the WAL sidecars), and the state directory is enforced0700even when it already exists. - The public Funnel listener now bounds concurrent connections to limit resource exhaustion from the open internet.
Reliability and resource hygiene:
- Established a single-owner rule for fd capabilities across the shared reactor and the main isolate, eliminating a deterministic cross-isolate double-close on registration failure (which under fd reuse could sever an unrelated live descriptor), plus a related response-fd over-close and an inbound-accept leak.
- Closing an HTTP binding now drains its accept backlog, releasing queued descriptors and unblocking their handler goroutines.
- Inbound UDP datagrams larger than 60 KiB are now dropped (and logged) instead of being silently truncated and delivered as a short datagram.
- The worker now fails API calls fast if its background isolate terminates, instead of hanging indefinitely.
- Additional fixes: TCP accept-loop descriptor leak and silent-failure handling,
native-memory cleanup on decode errors, eager (fail-at-parse) string-list
decoding, and closing the prior HTTP client on repeated
up().
Dependencies and build:
- Bumped
tailscale.comfrom v1.92.2 to v1.100.0. - Now requires the Go 1.26.4 toolchain (up from 1.25.5), enforced by the Go
module directive. With the default
GOTOOLCHAIN=auto, Go fetches it automatically — including on a Go 1.25+ base — so no manual toolchain install is needed unlessGOTOOLCHAINdisables auto-upgrade. - Added Dependabot (Go modules, pub, and GitHub Actions) and a least-privilege CI token.
Documentation:
init()and the README now recommend storing state in a backup-excluded, app-private directory (the node's WireGuard key must not leak into iCloud or Google backups), andWaitingFile.namedocuments that the sender-chosen filename is attacker-controlled and must be sanitized before use in a path.
Validation:
- Verified against the existing unit, FFI, fd, runtime, Go, and Headscale E2E suites. The bundled-stack bump passes the full two-node Headscale E2E (node lifecycle, TCP/UDP/HTTP, persisted-credential reconnect, and logout revocation) identically to the prior version.
0.3.1 #
- Adds
Tailscale.up(ephemeral: true)for disposable CI jobs, preview environments, and tests. - Adds
example/shelf_adapter.dart, a tested adapter showing how to run Shelf handlers directly onhttp.bindwithout making Shelf a core dependency. - Updates the README, developer site, API status, and architecture notes to point Shelf users at the tested adapter example.
0.3.0 #
This release is a major API and transport rebuild for public POSIX usage. It keeps the embedded-tsnet lifecycle model, but replaces the old loopback transport helpers with package-native APIs backed by private fd capabilities and a shared POSIX reactor.
Platform contract:
pubspec.yamldeclares Android, iOS, Linux, and macOS support. Windows is intentionally unsupported until a Windows-native data-plane backend or fallback carrier is designed.- Linux CI runs Headscale E2E against the epoll reactor path; macOS, iOS, and Android have been validated through the demo/smoke harness.
Breaking — public API shape:
Tailscale.httpis now the HTTP namespace. UseTailscale.http.clientfor a standardpackage:httpclient routed through the tailnet.- The old
Tailscale.listen(localPort, {tailnetPort})reverse-proxy helper was removed. UseTailscale.http.bind(port: ...)for in-process HTTP handling, orTailscale.serve.forward(...)when forwarding an existing loopback HTTP server. - Inventory APIs now use Tailscale's node terminology:
Tailscale.nodes(),Tailscale.nodeByIp(ip),Tailscale.onNodeChanges,TailscaleNode, andTailscaleNodeIdentity. Tailscale.up()now returnsFuture<TailscaleStatus>and resolves on the first stable state (running,needsLogin, orneedsMachineAuth).PingResult.directis nowPingResult.path(PingPath.direct,derp, orunknown). The.directgetter remains as a convenience for the positive case.ClientVersionnow mirrors upstream fields:latestVersion,urgentSecurityUpdate, and optionalnotifyText.
Core lifecycle and observation:
TailscaleClientis the testable app-facing interface implemented byTailscale.instance.onStateChange,onError, andonNodeChangesare pushed from Go; node updates are debounced and newonNodeChangessubscribers receive the current snapshot.- Structured
TailscaleErrorCodeand per-namespace operation exceptions now preserve known LocalAPI error categories (notFound,forbidden,conflict,preconditionFailed,featureDisabled,unknown).
fd-backed transport APIs:
http.clientstreams outbound request/response bodies over private fd-backed channels while Go ownstsnet.Server.HTTPClient()semantics.http.bind({port})returnsTailscaleHttpServerwith package-native request/response objects and fd-backed request/response bodies.tcp.dial(...)andtcp.bind(...)provide package-native raw TCP streams and listeners via Go-ownedtsnet.Server.Dial/Listenconnections handed to Dart as private fd capabilities.tls.bind(...)accepts TLS-terminated tailnet connections as plaintextTailscaleConnections; certificate acquisition and renewal remain in Go.udp.bind(...)provides message-preserving datagrams with remote endpoint metadata and rejects payloads over 60 KiB.- The POSIX data plane uses a shared kqueue/epoll reactor instead of spawning reader/writer isolates per fd.
Tailscale feature namespaces:
whois(ip)andnodeByIp(ip)are implemented for identity-aware authorization flows.tls.domains()exposes auto-provisioned Tailscale certificate SANs.diag.ping,diag.metrics,diag.derpMap, anddiag.checkUpdateare implemented.prefs.get, single-field prefs setters, andprefs.updateMaskedare implemented.exitNode.current,suggest,use,useById,useAuto,clear, andonCurrentChangeare implemented.serve.forward/clearpublishes an existing loopback HTTP service inside the tailnet using LocalAPI ServeConfig.funnel.forward/clearpublishes an existing loopback HTTP service through Tailscale Funnel usingtsnet.ListenFunnelplus a package-owned reverse proxy. Forwarding targets are loopback-only.taildropandprofilesremain declared roadmap namespaces and throwUnimplementedErrorin this release.
Validation:
- Unit, FFI, fd, runtime, Go, and Headscale E2E suites cover the core feature spine.
- Live Tailscale tests cover hosted-control-plane behavior Headscale cannot
model: routing controls, TLS serving, Serve forwarding, Funnel forwarding,
and Serve cleanup on
down()/restart.
Release hardening:
- HTTP fd response-head envelopes are capped at 256 KiB on both the Dart and Go sides.
- fd transport write/close dispatch failures, listener/server close failures, unread HTTP request bodies, and UDP binding teardown paths now deterministically close local resources.
- Serve/Funnel forwarding canonicalizes
localhostto127.0.0.1before creating loopback proxy targets. - Smoke-matrix tooling redacts bearer credentials from logs and stores generated runner tokens with owner-only file permissions.
0.2.0 #
- tsnet.Server.Close() doesn't fire a terminal state through the IPN bus, so onStateChange subscribers drifted from the engine — stuck at the pre-stop value (usually Running) and their UI routing went stale.
- Stop() now publishes Stopped, gated on srv != nil so a no-op stop stays silent. Logout() follows up with NoState after wiping creds (full sequence on logout from a running node: Stopped → NoState).
- Rewrites the onStateChange lifecycle e2e group around a new _recordUntil helper that captures full emitted sequences, and adds coverage for the no-op-down guard, broadcast delivery to multiple subscribers, and the ordered Stopped → NoState emit on logout.
0.1.0 #
- Initial release.
- Embed a Tailscale node directly in any Dart or Flutter application.
Tailscale.init()— configure once at startup with state directory and log level.up()— start the embedded node and connect to a Tailscale or Headscale network.http— a standardhttp.Clientthat routes requests through the WireGuard tunnel.listen()— accept incoming traffic from the tailnet, forwarded to a local port.status()— typedTailscaleStatuswithNodeStateenum, local IPs, and health.nodes()— typedTailscaleNodesnapshots, separate from status for lightweight polling.onStateChange/onError— real-time streams pushed from Go via NativePort (no polling).down()— disconnect, preserving state for reconnection.logout()— disconnect and clear persisted state.NodeStateenum:noState,needsLogin,needsMachineAuth,starting,running,stopped.- Automatic native Go compilation via Dart build hook — no manual build steps.
- Zero main-isolate jank: all FFI calls run on a background isolate.
- Supports iOS, Android, macOS, Linux, and Windows.
- Works with Tailscale and self-hosted Headscale control servers.
- Full test suite: unit, FFI integration, and E2E against Headscale in Docker.