Update README with full platform docs, API reference, and binary sizes
- Document all 4 platforms with status and architecture details - Add API reference table - Add platform setup sections (iOS, macOS, Android, Linux) - Document build commands for all platforms - Add binary size table - Note Android Tailscale tunnel limitation and path forward - Add Linux test file Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f070bcdb86
commit
623407d16b
82
README.md
82
README.md
@ -5,14 +5,19 @@ Embed [Tailscale's tsnet](https://pkg.go.dev/tailscale.com/tsnet) in Flutter app
|
|||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
```
|
```
|
||||||
Flutter (Dart) → MethodChannel → Swift Plugin → Go static library (tsnet)
|
Flutter App (Dart)
|
||||||
↓
|
└── tsnet_flutter plugin
|
||||||
WireGuard tunnel (userspace)
|
├── iOS/macOS: Swift MethodChannel → C bridge → Go static library (.a)
|
||||||
↓
|
└── Android/Linux: Dart FFI → Go shared library (.so)
|
||||||
Remote device (100.x.x.x)
|
↓
|
||||||
|
Go tsnet (WireGuard + userspace netstack)
|
||||||
|
↓
|
||||||
|
localhost TCP proxy (127.0.0.1:PORT)
|
||||||
|
↓
|
||||||
|
WireGuard tunnel → remote device (100.x.x.x)
|
||||||
```
|
```
|
||||||
|
|
||||||
The Go layer runs a local TCP proxy: your app connects to `localhost:PORT`, and traffic is forwarded through a WireGuard tunnel to the target device's Tailscale IP. Flutter doesn't know about Tailscale — it just sees a localhost port.
|
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
|
## Usage
|
||||||
|
|
||||||
@ -35,13 +40,27 @@ await tsnet.stopProxy();
|
|||||||
await tsnet.stop();
|
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 support
|
||||||
|
|
||||||
| Platform | Status | Min version |
|
| Platform | Tailscale tunnel | Binary type | Architectures |
|
||||||
|----------|--------|-------------|
|
|----------|-----------------|-------------|---------------|
|
||||||
| iOS | Supported (arm64 device + simulator) | 14.0 |
|
| iOS | Supported | c-archive (.a in xcframework) | arm64 device + arm64 simulator |
|
||||||
| macOS | Supported (arm64 + x86_64 universal) | 12.0 |
|
| macOS | Supported | c-archive (.a in xcframework) | arm64 + x86_64 universal |
|
||||||
| Android | Planned | — |
|
| 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](https://github.com/tailscale/tailscale-android).
|
||||||
|
|
||||||
## Platform setup
|
## Platform setup
|
||||||
|
|
||||||
@ -60,23 +79,54 @@ macOS apps run sandboxed. Add these entitlements to both `DebugProfile.entitleme
|
|||||||
<true/>
|
<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.
|
`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`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
No special setup needed. The shared library is bundled automatically.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Tailscale auth key (generate at [login.tailscale.com](https://login.tailscale.com/admin/settings/keys))
|
- Tailscale auth key — generate at [login.tailscale.com/admin/settings/keys](https://login.tailscale.com/admin/settings/keys)
|
||||||
|
- Auth keys can be reusable and tag-scoped for ACL isolation
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
The pre-built xcframeworks are included in the package. To rebuild from Go source:
|
The pre-built binaries are included in the package. To rebuild from Go source:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Prerequisites: Go 1.23+, Xcode with iOS + macOS SDKs
|
# Prerequisites: Go 1.23+, Xcode (for Apple platforms), Android NDK (for Android)
|
||||||
|
|
||||||
./build_go.sh # build all platforms
|
./build_go.sh # build all platforms
|
||||||
./build_go.sh ios # iOS only
|
./build_go.sh ios # iOS only
|
||||||
./build_go.sh macos # macOS 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
|
## License
|
||||||
|
|
||||||
BSD-3-Clause. See [LICENSE](LICENSE).
|
BSD-3-Clause — compatible with Tailscale's license. See [LICENSE](LICENSE).
|
||||||
|
|||||||
116
test/linux_e2e.go
Normal file
116
test/linux_e2e.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Linux test for tsnet — loads the shared library via CGo and tests the tunnel.
|
||||||
|
//
|
||||||
|
// Run from macOS via Docker:
|
||||||
|
// docker run --rm --platform linux/amd64 \
|
||||||
|
// -v /path/to/flutter-tsnet:/src -w /src/test \
|
||||||
|
// golang:latest go run linux_test.go \
|
||||||
|
// -authkey=tskey-auth-xxx \
|
||||||
|
// -heater-ip=100.x.x.x
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -L${SRCDIR}/../linux -ltailscale -lresolv -lpthread
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
extern char* TailscaleStart(char*, char*, char*);
|
||||||
|
extern char* TailscaleStartProxy(char*, int, int*);
|
||||||
|
extern char* TailscaleStopProxy();
|
||||||
|
extern char* TailscaleStop();
|
||||||
|
extern char* TailscaleStatus(char**);
|
||||||
|
extern char* TailscaleIP(char**);
|
||||||
|
extern void TailscaleFreeString(char*);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func goString(cs *C.char) string {
|
||||||
|
if cs == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
s := C.GoString(cs)
|
||||||
|
C.TailscaleFreeString(cs)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
authKey := flag.String("authkey", "", "Tailscale auth key")
|
||||||
|
heaterIP := flag.String("heater-ip", "", "Heater's Tailscale IP")
|
||||||
|
heaterPort := flag.Int("heater-port", 5050, "Heater's port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *authKey == "" || *heaterIP == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: go run linux_test.go -authkey=tskey-auth-xxx -heater-ip=100.x.x.x\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State dir
|
||||||
|
stateDir := "/tmp/tsnet-linux-test"
|
||||||
|
os.MkdirAll(stateDir, 0o700)
|
||||||
|
|
||||||
|
// 1. Start
|
||||||
|
fmt.Println("[1/4] Starting tsnet...")
|
||||||
|
start := time.Now()
|
||||||
|
dirC := C.CString(stateDir)
|
||||||
|
keyC := C.CString(*authKey)
|
||||||
|
hostC := C.CString("tsnet-linux-test")
|
||||||
|
err := goString(C.TailscaleStart(dirC, keyC, hostC))
|
||||||
|
C.free(unsafe.Pointer(dirC))
|
||||||
|
C.free(unsafe.Pointer(keyC))
|
||||||
|
C.free(unsafe.Pointer(hostC))
|
||||||
|
if err != "" {
|
||||||
|
fmt.Printf("[1/4] FAIL: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("[1/4] Started in %dms\n", time.Since(start).Milliseconds())
|
||||||
|
|
||||||
|
// 2. Get IP (with retry)
|
||||||
|
fmt.Println("[2/4] Getting Tailscale IP...")
|
||||||
|
var ip string
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
var errOut *C.char
|
||||||
|
ipC := C.TailscaleIP(&errOut)
|
||||||
|
e := goString(errOut)
|
||||||
|
if e == "" {
|
||||||
|
ip = goString(ipC)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
C.TailscaleFreeString(ipC)
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
fmt.Printf("[2/4] Our IP: %s\n", ip)
|
||||||
|
|
||||||
|
// 3. Start proxy
|
||||||
|
fmt.Println("[3/4] Starting proxy...")
|
||||||
|
ipC := C.CString(*heaterIP)
|
||||||
|
var localPort C.int
|
||||||
|
errStr := goString(C.TailscaleStartProxy(ipC, C.int(*heaterPort), &localPort))
|
||||||
|
C.free(unsafe.Pointer(ipC))
|
||||||
|
if errStr != "" {
|
||||||
|
fmt.Printf("[3/4] FAIL: %s\n", errStr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("[3/4] Proxy: localhost:%d → %s:%d\n", int(localPort), *heaterIP, *heaterPort)
|
||||||
|
|
||||||
|
// 4. Test TCP
|
||||||
|
fmt.Println("[4/4] Testing TCP connection...")
|
||||||
|
conn, connErr := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", int(localPort)), 10*time.Second)
|
||||||
|
if connErr != nil {
|
||||||
|
fmt.Printf("[4/4] TCP FAILED: %v\n", connErr)
|
||||||
|
} else {
|
||||||
|
fmt.Println("[4/4] TCP SUCCESS")
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
C.TailscaleStopProxy()
|
||||||
|
C.TailscaleStop()
|
||||||
|
|
||||||
|
fmt.Println("\nALL TESTS PASSED")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user