libtailscale 1.0.1
libtailscale: ^1.0.1 copied to clipboard
Headless Tailscale / Headscale node for Dart and Flutter, embedded through dart:ffi bindings to the official libtailscale C library.
libtailscale #
A headless Tailscale / Headscale node for Dart and Flutter. It embeds the
official libtailscale C library
through dart:ffi, joins a tailnet with the credential your app supplies, and
lets the app open and accept connections over it. No UI, no browser, no
platform channels.
Supported: macOS 15+, iOS 15+, Android 15+ (API 35), Linux with glibc 2.39+. Not supported: Windows and web, because libtailscale is Unix-only.
Quick start #
dependencies:
libtailscale: ^1.0.0
import 'package:libtailscale/libtailscale.dart';
final node = TailscaleNode(TailscaleConfig(
controlUrl: Uri.parse('https://headscale.example.com'), // omit for Tailscale
credential: const TailscaleCredential.authKey('tskey-auth-…'),
hostname: 'inventory-agent',
stateDir: '/path/to/writable/state',
ephemeral: true,
));
await node.start();
await node.waitUntilRunning(timeout: const Duration(seconds: 60));
print(node.addresses.ipv4); // 100.x.y.z
final server = await node.listen(port: 8080); // Stream<TailscaleSocket>
server.listen((c) => c.addStream(c)); // echo service on the tailnet
final socket = await node.connect('files', 443); // dart:io Socket via SOCKS5
final http = node.httpClient(); // HttpClient over the tailnet
final peers = (await node.status()).peers; // name, IPs, online, tags, OS
node.stateChanges.listen((s) => print('tailscale: $s'));
await node.close();
The prebuilt native library is downloaded and checksum-verified when the app is built. See Build configuration for the alternatives.
What is covered #
- Configure: control URL (Tailscale or Headscale), credential, hostname, state directory, ephemeral node.
- Control:
start(),waitUntilRunning(),logout(),close(), and thestateChanges,health,authUrlsandlogsstreams. - Observe:
addresses,status()with peers and users,whoIs(ip). - Operate:
connect(),connectSecure()andhttpClient()give you a realdart:ioSocketorHttpClientover the tailnet;listen()anddial()give youTailscaleSockets.
Deliberately left out: UDP, Taildrop, Serve, SSH, exit-node switching and any
kind of UI. Anything else in tailscaled's LocalAPI is reachable through
node.localApi.raw(...), without compatibility guarantees.
Credentials #
| Credential | Works with | Notes |
|---|---|---|
TailscaleCredential.authKey(key) |
Tailscale auth keys, Headscale pre-auth keys | Reusable, ephemeral and tags are properties of the key. |
TailscaleCredential.oauthClient(...) |
Tailscale only | Mints a short-lived tagged auth key through the Tailscale API before starting. |
TailscaleCredential.existingState() |
both | Reuses the node key in stateDir; throws TailscaleAuthRequiredException if a login is needed. |
TailscaleCredential.interactive() |
both | Publishes the login URL on node.authUrls; your app decides what to do with it. |
Headscale #
Point controlUrl at the Headscale base URL; nothing else changes. Headscale
has no OAuth client API (use pre-auth keys), issues no TLS certificates and does
not support Funnel or Serve. For interactive logins an admin approves the node
with headscale auth register --auth-id <id from the URL> --user <user>.
An unreachable or wrong control URL produces no error; the node just stays in
Starting or NeedsLogin. waitUntilRunning therefore always takes a timeout
and reports the last state and health messages in its exception.
Sockets and TLS #
connect()returns aSocks5Socket, a realdart:ioSockettunnelled through the node's loopback SOCKS5 proxy. For TLS usesocket.secure(...)ornode.connectSecure(), notSecureSocket.secure.httpClient()returns anHttpClientwhose connections go over the tailnet, sopackage:http,dio, gRPC andWebSocket.connect(customClient:)work unchanged.listen()anddial()use libtailscale's file-descriptor path and yieldTailscaleSocket(aStream<Uint8List>plus anIOSink). TLS is not available on that path.
Nothing blocks the calling isolate: blocking calls run on helper isolates, and a
dedicated isolate drives every file descriptor with one poll(2) loop.
Build configuration #
The pub package contains no binaries. A build hook provides the native library when the app is built, trying these in order:
prebuilt_dir: a directory you supply containinglibtailscale.{dylib,so}or the architecture-specificlibtailscale-<os>-<arch>[-<sdk>].<ext>.build_from_source:go buildfrom a pinned upstream checkout. Needs Go (the hook selects the version upstream builds with throughGOTOOLCHAIN) and the NDK or Xcode for mobile targets.- Download of the prebuilt library from this repository's GitHub release, verified against the SHA-256 pinned in the package.
Option 3 is the default and needs no configuration, only network access to
github.com at build time. To use one of the others, add to the
application's pubspec.yaml:
hooks:
user_defines:
libtailscale:
build_from_source: true
# prebuilt_dir: native/libs
# source_dir: ../libtailscale
# go: /usr/local/go/bin/go
Android #
Minimum API 35; add the INTERNET permission. The library is built with 16 KB
page alignment. Two Android quirks are handled for you: apps may not list
network interfaces through netlink, so the library ships its own interface
getter, and app processes have no $HOME or $TMPDIR, so start() points
both at the state directory.
iOS #
Minimum iOS 15. Flutter bundles the library as tailscale.framework. Set
NSLocalNetworkUsageDescription (peers on the same LAN are reached directly)
and ship a privacy manifest; the one in examples/node_console/ios/Runner
declares the API categories the Go runtime and tsnet use. WireGuard stops while
the app is suspended; if the loopback listener is stale afterwards, status()
falls back to the C status call and dial() keeps working.
Linux #
Built on Ubuntu 24.04; requires glibc 2.39 or newer.
Flutter and Dart SDKs #
The hook works with the hooks and code_assets versions pinned by Flutter
stable as well as the newer releases used with a plain Dart SDK.
Examples #
example/example.dart: the whole API in one short program.examples/tsnode/: a headless command-line node withjoin,info,peers,echo,sendandfetch.examples/node_console/: a Flutter app for macOS, iOS, Android and Linux; its README lists the platform settings an embedding app needs.
Known limitations #
- UDP is not exposed yet.
- One pipe write end is intentionally leaked per node started with
captureLogs: true, because Go owns that file descriptor.
Contributing #
Development setup, the test suites, cutting a native release and the repository layout are described in CONTRIBUTING.md.
License #
BSD-3-Clause, like libtailscale itself.