tailscale 0.2.0 copy "tailscale: ^0.2.0" to clipboard
tailscale: ^0.2.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.

flutter_tailscale

Flutter + Tailscale #

License: MIT Dart 3.10+ Platforms

Bring a Flutter or plain Dart app onto your tailnet as its own node — talk to peers, expose local services, and add private connectivity without a system-wide VPN.

Works with Tailscale and self-hosted Headscale.

Tailscale.init(stateDir: '/path/to/state');

final tailscale = Tailscale.instance;

await tailscale.up(authKey: 'tskey-auth-...');

// Discover peers
final peers = await tailscale.peers(); // List<PeerStatus>
final peer = peers.firstWhere((peer) => peer.online);

// Make requests — standard http.Client, routed through the tunnel
await tailscale.http.get(Uri.parse('http://${peer.ipv4}/api/data'));

// Expose a local HTTP server to receive traffic from the tailnet
await tailscale.listen(8080);

Install #

dependencies:
  tailscale: ^0.1.0

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

Features #

  • App-scoped networking — your app joins the tailnet itself instead of depending on a separate VPN
  • Familiar HTTP client — standard http.Client routed through WireGuard
  • Inbound HTTP publishing — expose a local server to tailnet peers with listen()
  • Typed runtime state — observe node status, peers, and errors through Dart models
  • Persistent identity — reconnect across launches without re-authenticating
  • Automatic cross-platform builds — Go layer compiles for the target via Dart build hooks
  • Headscale compatible — point the control plane at Tailscale or your own deployment
  • Real-time push notifications — Go pushes state changes to Dart via NativePort, no polling

Usage #

import 'package:tailscale/tailscale.dart';

// 1. Configure once at app startup
Tailscale.init(
  stateDir: '/path/to/state',
  logLevel: TailscaleLogLevel.info,
);

final tailscale = Tailscale.instance;
tailscale.onStateChange.listen((state) => print('Node: $state'));
tailscale.onError.listen((e) => print('Error: ${e.message}'));

// 2. Bring the node up (first launch needs an auth key)
await tailscale.up(authKey: 'tskey-auth-...');

//    Subsequent launches reconnect from stored state
await tailscale.up();

// 3. Make requests to peers
final peers = await tailscale.peers();
final peer = peers.firstWhere((p) => p.online);

final response = await tailscale.http.get(
  Uri.parse('http://${peer.ipv4}/api/data'),
);

// 4. Accept incoming HTTP requests from peers
await tailscale.listen(8080); // tailnet:80 -> localhost:8080

// 5. Disconnect (keeps identity)
await tailscale.down();

// 6. Disconnect and fully remove state
await tailscale.logout();

Platform Support #

Platform Status Notes
iOS Full support No VPN entitlement needed
Android Full support Userspace mode, no root required
macOS Full support
Linux Full support
Windows Full support

API #

Member Type Description
init({stateDir, logLevel}) static void Configure once at startup. Stores state in stateDir/tailscale/.
instance static Tailscale Singleton accessor
up({hostname, authKey, controlUrl}) Future<void> Start the node. Subscribe to onStateChange to observe when it reaches Running.
listen(localPort, {tailnetPort}) Future<int> Expose a local HTTP server to peers
status() Future<TailscaleStatus> Current local-node snapshot (state, IPs, health). Before up(), returns stopped or noState based on whether persisted credentials exist.
peers() Future<List<PeerStatus>> Current peer snapshot
onStateChange Stream<NodeState> Pushed lifecycle state changes
onError Stream<TailscaleRuntimeError> Pushed asynchronous runtime errors
down() Future<void> Disconnect (preserves state for reconnection)
logout() Future<void> Disconnect and clear persisted state
http http.Client HTTP client routed through the WireGuard tunnel
TailscaleStatus

A snapshot of the local node's current state. Returned by status(). Peer inventory is separate — call peers() when you need it.

Member Type Description
state NodeState Connection lifecycle state
authUrl Uri? Login URL when authentication is required
tailscaleIPs List<String> Assigned Tailscale IPs
ipv4 String? IPv4 address
health List<String> Health warnings (empty = healthy)
magicDNSSuffix String? Tailnet's MagicDNS suffix
isRunning / needsLogin / isHealthy bool Convenience getters
NodeState

The node's position in the connection lifecycle. Matches Go's ipn.State.

Value Description
noState No persisted credentials, never authenticated
needsLogin Needs authentication (credentials expired or first use)
needsMachineAuth Authenticated, waiting for admin approval
starting Connecting to the tailnet
running Connected and ready for traffic
stopped Engine not running, but persisted credentials exist
PeerStatus

A peer on the tailnet. Matches Go's ipnstate.PeerStatus.

Member Type Description
publicKey String Peer's public key
hostName / dnsName String Hostname and MagicDNS name
os String Operating system
tailscaleIPs List<String> Assigned IPs
ipv4 String? IPv4 address
online / active bool Online status and traffic heuristic
rxBytes / txBytes int Traffic counters
lastSeen DateTime? Last seen timestamp
relay / curAddr String? DERP relay or direct address
TailscaleRuntimeError & TailscaleLogLevel

TailscaleRuntimeError — typed background error pushed through onError.

Member Type Description
message String Human-readable error
code TailscaleRuntimeErrorCode Error category

TailscaleLogLevel — controls native log verbosity.

Value Description
silent No native logs
error Errors only
info Informational and error logs

Architecture #

┌─────────────┐       FFI       ┌──────────────┐      Tailscale       ┌─────────────┐
│  Dart app   │ <─────────────> │  Go (tsnet)  │ <─── WireGuard ────> │  Peers      │
│             │  Isolate.run()  │  C exports   │      tunnel          │             │
│             │  NativePort     │  WatchIPNBus │                      │             │
└─────────────┘                 └──────────────┘                      └─────────────┘

The Go layer wraps tailscale.com/tsnet and compiles to a platform-specific native library. A Dart build hook handles compilation — detecting the target OS/architecture, finding the Go toolchain, cross-compiling with the appropriate flags, and registering the result as a native code asset.

Dart -> Go calls use @Native FFI annotations and run on background isolates so the main isolate is never blocked.

Go -> Dart notifications use NativePort to push state transitions from Go's WatchIPNBus goroutine directly to Dart's event loop.

Testing #

dart test test/tailscale_test.dart              # Unit tests
cd go && go test -v ./...                       # Go tests
dart test test/ffi_integration_test.dart         # FFI integration tests
test/e2e/run_e2e.sh                             # E2E against Headscale in Docker

The E2E suite starts a Headscale server in Docker, creates an ephemeral auth key, connects a real embedded node, verifies IP assignment and peer discovery, then cleans up. No Tailscale account needed.

License #

MIT

0
likes
0
points
277
downloads

Publisher

unverified uploader

Weekly Downloads

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.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

code_assets, ffi, hooks, http, meta, path

More

Packages that depend on tailscale