tsnet_flutter

Embed Tailscale's tsnet in Flutter apps. Provides a userspace WireGuard tunnel with a localhost TCP proxy — no VPN entitlement needed on iOS.

How it works

Flutter App (Dart)
  └── tsnet_flutter plugin
        ├── iOS/macOS: Swift MethodChannel → C bridge → Go static library (.a)
        └── Android/Linux: Dart FFI → Go shared library (.so)
              ↓
         Go tsnet (WireGuard + userspace netstack)
              ↓
         localhost TCP proxy (127.0.0.1:PORT)
              ↓
         WireGuard tunnel → remote device (100.x.x.x)

Your app connects to localhost:PORT. Traffic is forwarded through a WireGuard tunnel to the target device's Tailscale IP. Your app doesn't know about Tailscale — it just sees a localhost port.

Usage

import 'package:tsnet_flutter/tsnet_flutter.dart';

final tsnet = TsnetFlutter();

// Join the Tailnet
await tsnet.start(authKey: 'tskey-auth-...');

// Create a local proxy to the remote device
final localPort = await tsnet.startProxy('100.64.0.5', port: 5050);

// Connect your client to the proxy
yourClient.connect('127.0.0.1', port: localPort);

// Clean up
await tsnet.stopProxy();
await tsnet.stop();

API

Method Description
start(authKey, hostname) Join a Tailnet with an auth key. Idempotent — safe to call multiple times.
startProxy(ip, port) Create a localhost TCP proxy to a remote Tailscale IP. Returns the local port.
stopProxy() Stop the localhost proxy.
stop() Disconnect from the Tailnet.
status() Get the current Tailscale connection status (state, peers, IPs).
tailscaleIP() Get this device's Tailscale IPv4 address (100.x.x.x).

Platform support

Platform Tailscale tunnel Binary type Architectures
iOS Supported c-archive (.a in xcframework) arm64 device + arm64 simulator
macOS Supported c-archive (.a in xcframework) arm64 + x86_64 universal
Linux Supported c-shared (.so) amd64
Android Local only (WIP) c-shared (.so in jniLibs) arm64-v8a + x86_64

Android note: Local TCP connections work. Tailscale tunnel is blocked by Go's net.Interfaces() requiring CAP_NET_ADMIN on Android. Full tunnel support will require libtailscale integration from tailscale-android.

Platform setup

iOS

No special entitlements or permissions needed. No VPN entitlement required.

macOS

macOS apps run sandboxed. Add these entitlements to both DebugProfile.entitlements and Release.entitlements:

<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>

network.client allows the app to connect to the localhost proxy and external networks. network.server allows the Go layer to open a localhost listener for the proxy.

Android

Add the INTERNET permission to your AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Linux

No special setup needed. The shared library is bundled automatically.

Requirements

Building from source

The pre-built binaries are included in the package. To rebuild from Go source:

# Prerequisites: Go 1.23+, Xcode (for Apple platforms), Android NDK (for Android)

./build_go.sh          # build all platforms
./build_go.sh ios      # iOS only
./build_go.sh macos    # macOS only
./build_go.sh android  # Android only (requires NDK)
./build_go.sh linux    # Linux only (uses Docker on macOS)
./build_go.sh apple    # iOS + macOS

Binary sizes

Platform Size Notes
iOS (device) ~23 MB After App Store compression: ~14 MB
iOS (simulator) ~23 MB Development only
macOS (universal) ~49 MB arm64 + x86_64
Android (arm64) ~20 MB
Android (x86_64) ~22 MB Emulator only
Linux (amd64) ~24 MB

Size is dominated by WireGuard + gVisor netstack + Go runtime. Feature tags strip ~35 unused Tailscale subsystems (SSH, Drive, Serve, etc.).

License

BSD-3-Clause — compatible with Tailscale's license. See LICENSE.