flutter-tsnet/lib/tsnet_ffi.dart
Mathias Beaulieu-Duncan f070bcdb86 Add Android and Linux platform support (local gRPC working, tsnet WIP)
- Android: Dart FFI → Go c-shared (.so) in jniLibs (arm64-v8a + x86_64)
- Linux: Dart FFI → Go c-shared (.so) via Docker cross-compilation (amd64)
- Dart API: TsnetFlutter uses MethodChannel on iOS/macOS, FFI on Android/Linux
- Add ffi package dependency for native function bindings
- Build script: ./build_go.sh [ios|macos|android|linux|apple|all]
- Android RegisterInterfaceGetter to bypass netlink CAP_NET_ADMIN restriction
- Make TailscaleStart() idempotent and add GODEBUG=netdns=go for Android

Known: Android tsnet tunnel blocked by Go stdlib net.Interfaces() netlink
call — local gRPC works, Tailscale fallback needs libtailscale integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 07:49:11 -04:00

126 lines
4.1 KiB
Dart

import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart';
// C function typedefs from bridge.go
typedef _StartNative = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>);
typedef _StartProxyNative = Pointer<Utf8> Function(Pointer<Utf8>, Int32, Pointer<Int32>);
typedef _StartProxyDart = Pointer<Utf8> Function(Pointer<Utf8>, int, Pointer<Int32>);
typedef _SimpleNative = Pointer<Utf8> Function();
typedef _OutParamNative = Pointer<Utf8> Function(Pointer<Pointer<Utf8>>);
typedef _FreeNative = Void Function(Pointer<Utf8>);
typedef _FreeDart = void Function(Pointer<Utf8>);
/// FFI-based implementation for Android and Linux.
/// Loads the Go shared library and calls C functions directly.
class TsnetFfi {
late final DynamicLibrary _lib;
late final _StartNative _start;
late final _StartProxyDart _startProxy;
late final _SimpleNative _stopProxy;
late final _SimpleNative _stop;
late final _OutParamNative _status;
late final _OutParamNative _tailscaleIP;
late final _FreeDart _free;
TsnetFfi() {
_lib = _loadLibrary();
_start = _lib.lookupFunction<_StartNative, _StartNative>('TailscaleStart');
_startProxy = _lib.lookupFunction<_StartProxyNative, _StartProxyDart>('TailscaleStartProxy');
_stopProxy = _lib.lookupFunction<_SimpleNative, _SimpleNative>('TailscaleStopProxy');
_stop = _lib.lookupFunction<_SimpleNative, _SimpleNative>('TailscaleStop');
_status = _lib.lookupFunction<_OutParamNative, _OutParamNative>('TailscaleStatus');
_tailscaleIP = _lib.lookupFunction<_OutParamNative, _OutParamNative>('TailscaleIP');
_free = _lib.lookupFunction<_FreeNative, _FreeDart>('TailscaleFreeString');
}
static DynamicLibrary _loadLibrary() {
if (Platform.isAndroid) {
return DynamicLibrary.open('libtailscale.so');
} else if (Platform.isLinux) {
// Look in the app's lib directory (where Flutter bundles native libs)
return DynamicLibrary.open('libtailscale.so');
}
throw UnsupportedError('FFI not supported on ${Platform.operatingSystem}');
}
String? _consumeString(Pointer<Utf8> ptr) {
if (ptr == nullptr) return null;
final str = ptr.toDartString();
_free(ptr);
return str;
}
void _checkError(Pointer<Utf8> errPtr, String code) {
final err = _consumeString(errPtr);
if (err != null) {
throw PlatformException(code: code, message: err);
}
}
Future<void> start(String stateDir, String authKey, String hostname) async {
final dirC = stateDir.toNativeUtf8();
final keyC = authKey.toNativeUtf8();
final hostC = hostname.toNativeUtf8();
try {
final err = _start(dirC, keyC, hostC);
_checkError(err, 'START_FAILED');
} finally {
malloc.free(dirC);
malloc.free(keyC);
malloc.free(hostC);
}
}
Future<int> startProxy(String remoteIP, int remotePort) async {
final ipC = remoteIP.toNativeUtf8();
final portOut = malloc<Int32>();
try {
final err = _startProxy(ipC, remotePort, portOut);
_checkError(err, 'PROXY_FAILED');
return portOut.value;
} finally {
malloc.free(ipC);
malloc.free(portOut);
}
}
Future<void> stopProxy() async {
final err = _stopProxy();
_checkError(err, 'STOP_PROXY_FAILED');
}
Future<void> stop() async {
final err = _stop();
_checkError(err, 'STOP_FAILED');
}
Future<String?> status() async {
final errOut = malloc<Pointer<Utf8>>();
try {
errOut.value = nullptr;
final json = _status(errOut);
final err = _consumeString(errOut.value);
if (err != null) throw PlatformException(code: 'STATUS_FAILED', message: err);
return _consumeString(json);
} finally {
malloc.free(errOut);
}
}
Future<String> tailscaleIP() async {
final errOut = malloc<Pointer<Utf8>>();
try {
errOut.value = nullptr;
final ip = _tailscaleIP(errOut);
final err = _consumeString(errOut.value);
if (err != null) throw PlatformException(code: 'IP_FAILED', message: err);
return _consumeString(ip) ?? '';
} finally {
malloc.free(errOut);
}
}
}