- 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>
126 lines
4.1 KiB
Dart
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);
|
|
}
|
|
}
|
|
}
|