tailscale.dart logo

Dart 💙 Tailscale

pub package License: MIT Dart 3.10.4+ Platforms Docs API reference

Build Dart and Flutter apps that talk to each other directly — no public servers, no VPN setup, no NAT punching code — over an encrypted Tailscale or Headscale tailnet.

package:tailscale embeds upstream Go tsnet and exposes typed Dart APIs for lifecycle, node identity, HTTP, TCP, UDP, TLS, Serve, Funnel, prefs, exit nodes, and diagnostics. Your app authenticates as its own node on the tailnet — users never install or configure a Tailscale client.

Status: 0.3.1, pre-1.0. The core API is stable enough to build on, but minor versions may include breaking changes until 1.0. Production users are welcome — please open an issue or start a discussion if something blocks you.

Documentation

The developer site is the canonical place to browse the package — full guide, examples, architecture diagrams, and a runtime model walkthrough.

Where What
Developer site Guide, examples, architecture — start here for rich browsing
API reference Generated dartdoc for every public symbol
pub.dev Install, versions
CHANGELOG Release notes and breaking changes
example/ Runnable Dart snippets
doc/ API status, roadmap, RFCs, and architecture notes
test/README.md Test tiers, Headscale E2E, and live Tailscale suites

What you can build

  • A Flutter chat or collaboration app where peers reach each other directly when possible — without you running relay or signaling infrastructure.
  • A headless Dart service that joins your tailnet and exposes private HTTPS without opening any public port.
  • An on-device dashboard that calls private internal APIs (Grafana, Home Assistant, internal admin) without a corporate VPN.
  • A shared Funnel endpoint — publish a local development server to the public internet, terminated with a real cert by Tailscale.
  • Anything you'd reach for a WireGuard or libp2p library for, but you'd rather use Tailscale's identity, ACLs, and DERP fallback than build them yourself.

When this is the right choice

  • You want app-level networking, scoped to one process — not system-wide tunnels users have to consent to.
  • You want familiar Dart shapes (http.Client, byte streams, datagrams) instead of dart:io.Socket wrappers around a localhost proxy.
  • You're happy to delegate auth, WireGuard, ACLs, MagicDNS, DERP, HTTPS certs, Serve, and Funnel policy to upstream Tailscale.

When to use something else

  • You need a system-wide VPN. Use the official Tailscale apps; this package is per-process userspace networking.
  • Windows is a hard requirement today. v1 is POSIX-only — see Platform support.
  • You can't run a Go toolchain at build time. This package compiles upstream tsnet on first build.

Install

dependencies:
  tailscale: ^0.3.1

The first dart run, dart test, or flutter build triggers a native build hook that compiles the Go runtime for the target platform. Subsequent builds are cached and only recompile when Go source changes.

Prerequisites:

  • Dart SDK 3.10.4 or newer.
  • Go 1.25 or newer on PATH.
  • Native toolchain for the target platform: Xcode for iOS/macOS, Android NDK through Flutter for Android, and a C toolchain for Linux.

Quick start

import 'package:tailscale/tailscale.dart';

Future<void> main() async {
  Tailscale.init(stateDir: '/app/state');

  final tailscale = Tailscale.instance;
  final status = await tailscale.up(
    hostname: 'dart-node',
    authKey: 'tskey-auth-...',
  );

  print('node: ${status.stableNodeId}');
  print('ipv4: ${status.ipv4}');
}

Subsequent launches can call up() without an auth key. The node identity is persisted in stateDir.

For short-lived CI jobs, preview environments, and disposable test nodes, pass ephemeral: true to register a node that Tailscale removes after it goes inactive:

await Tailscale.instance.up(
  hostname: 'preview-pr-842',
  authKey: 'tskey-auth-...',
  ephemeral: true,
);

Use a fresh or cleared stateDir for each disposable identity. If stateDir already contains node credentials, up(ephemeral: true) reconnects as that existing node instead of registering a new ephemeral one.

Feature support

Area API Status Notes
Lifecycle init, up, down, logout, status Supported up(ephemeral: true) supports disposable CI/test nodes; up() resolves on the first stable state: running, needs login, or needs machine auth.
Reactive state onStateChange, onError, onNodeChanges Supported Go pushes updates to Dart; callers do not poll.
Node identity nodes, nodeByIp, whois Supported Use stable node IDs for durable references.
Outbound HTTP http.client Supported A normal package:http client routed through tsnet.
Inbound HTTP http.bind Supported Package-native request/response types backed by fd streams.
Raw TCP tcp.dial, tcp.bind Supported Explicit read/write halves and half-close.
Raw UDP udp.bind Supported Message-preserving datagrams with remote endpoint metadata.
TLS listener tls.bind, tls.domains Supported Requires MagicDNS and HTTPS enabled on the tailnet.
Serve serve.forward, serve.clear Supported Tailnet-only publication for an existing loopback HTTP server.
Funnel funnel.forward, funnel.clear Supported Public HTTPS publication through Tailscale Funnel policy.
Tailscale Services N/A Planned Upstream tsnet.Server.ListenService is newer than the current tailscale.com v1.92.2 pin.
Routing controls prefs, exitNode Supported Subnet routes, Shields Up, tags, hostname, auto-update, and exit nodes.
Diagnostics diag Supported Ping, metrics, DERP map, and update checks.
Taildrop taildrop Planned Exported as a stub; not implemented in this release.
Profiles profiles Planned Exported as a stub; not implemented in this release.
Windows N/A Unsupported v1 is POSIX-only while the Windows data-plane backend is designed.

See doc/api-status.md for the full namespace-by-namespace API map.

Examples

A few canonical snippets below. The developer site hosts the full set covering raw TCP/UDP, TLS termination, Funnel, exit nodes, and routing controls; runnable variants live in example/.

All snippets assume the node has been initialized and started:

Tailscale.init(stateDir: '/app/state');
await Tailscale.instance.up(authKey: 'tskey-auth-...');

Call a private HTTP service

http.client is a standard package:http client. Requests resolve MagicDNS and route through the embedded node.

final response = await Tailscale.instance.http.client.get(
  Uri.parse('http://api.tailnet.example.ts.net/health'),
);

if (response.statusCode != 200) {
  throw StateError('health check failed: ${response.statusCode}');
}

Handle inbound HTTP directly

Use http.bind when the handler lives in this Dart process. No localhost proxy is opened.

final server = await Tailscale.instance.http.bind(port: 8080);

server.requests.listen((request) async {
  await request.respond(
    headers: {'content-type': 'text/plain; charset=utf-8'},
    body: 'hello from ${request.local.address}',
  );
});

Use Shelf middleware directly

The tested adapter in example/shelf_adapter.dart adds a bindShelf extension for apps that want Shelf middleware and routing. Add shelf to your app and copy or import the adapter; package:tailscale does not take Shelf as a core dependency.

import 'package:shelf/shelf.dart';
import 'package:tailscale/tailscale.dart';

import 'shelf_adapter.dart';

Future<void> main() async {
  final handler = const Pipeline()
      .addMiddleware(logRequests())
      .addHandler((Request request) {
        return Response.ok(
          'hello from Shelf over Tailscale',
          headers: {'content-type': 'text/plain; charset=utf-8'},
        );
      });

  final server = await Tailscale.instance.http.bindShelf(
    port: 8080,
    handler: handler,
  );

  print('Shelf listening on ${server.tailnet}');
}

Reuse an existing loopback server

Use serve.forward when your app already owns a local HTTP server and you want to publish that existing loopback port.

final publication = await Tailscale.instance.serve.forward(
  tailnetPort: 443,
  localPort: 3000,
);

print('tailnet URL: ${publication.url}');

serve.forward traffic follows Tailscale Serve semantics, including Tailscale identity headers for tailnet clients. funnel.forward follows the same local server shape for public Funnel publication, but Funnel traffic is public and does not include Tailscale identity headers.

Platform support

Platform Status Notes
iOS Supported Userspace tsnet, no VPN entitlement. Validated with the Flutter smoke app.
Android Supported Userspace tsnet, no root. Validated with the Flutter smoke app.
macOS Supported Native asset and kqueue reactor path validated locally.
Linux Supported Native asset and epoll reactor path validated in Headscale E2E.
Windows Unsupported Excluded from the package platform list until a Windows-native backend is designed.

The package is intentionally POSIX-first because owned transports use native descriptors plus a shared kqueue/epoll reactor. Windows needs a different transport backend rather than a thin port of the POSIX implementation.

Runtime model

Dart app
  |
  | typed API calls and streams
  v
FFI worker isolate
  |
  | control ops + fd-backed data-plane handoff
  v
Go tsnet runtime
  |
  | WireGuard, ACLs, MagicDNS, DERP
  v
Tailnet peers

Control-plane calls go through a worker isolate so Dart's main isolate does not block on native work. Runtime events come back through Dart ports as streams.

Owned transports (http.bind, tcp.bind, udp.bind, tls.bind) use private fd-backed capabilities. That keeps listener ownership inside the package and avoids pretending that a localhost proxy is secure. Forwarding APIs (serve.forward, funnel.forward) intentionally use loopback because their purpose is to publish an existing local HTTP server the application already owns.

Roadmap

The core package path is implemented: lifecycle, node identity, HTTP, TCP, UDP, TLS, Serve/Funnel, prefs, exit nodes, diagnostics, Headscale E2E, and hosted-Tailscale live validation. Remaining launch and post-launch work is tracked in the design docs under doc/ — see Documentation for the index.

Contributing

Issues, bug reports, and PRs are welcome.

  • Found a bug or have a feature request? Open an issue.
  • Have a question or want to share what you're building? Start a discussion.
  • Want to send a PR? Run dart analyze, dart test, and tool/test_pr_gate.sh before pushing. The full test setup — including the Headscale E2E suite and opt-in live Tailscale runs — is documented in test/README.md.

If you're using package:tailscale in production, I'd love to hear about it — open a discussion and let me know.

License

MIT

Libraries

tailscale