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>
This commit is contained in:
parent
ae105a2bb0
commit
f070bcdb86
36
android/build.gradle
Normal file
36
android/build.gradle
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
group 'io.svrnty.tsnet_flutter'
|
||||||
|
version '1.0'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.1.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'io.svrnty.tsnet_flutter'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk 21
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
android/src/main/AndroidManifest.xml
Normal file
4
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="io.svrnty.tsnet_flutter">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
</manifest>
|
||||||
BIN
android/src/main/jniLibs/arm64-v8a/libtailscale.so
Normal file
BIN
android/src/main/jniLibs/arm64-v8a/libtailscale.so
Normal file
Binary file not shown.
BIN
android/src/main/jniLibs/x86_64/libtailscale.so
Normal file
BIN
android/src/main/jniLibs/x86_64/libtailscale.so
Normal file
Binary file not shown.
77
build_go.sh
77
build_go.sh
@ -187,12 +187,85 @@ build_macos() {
|
|||||||
du -sh "$OUTPUT"
|
du -sh "$OUTPUT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Android (c-shared → .so in jniLibs)
|
||||||
|
# ============================================================
|
||||||
|
build_android() {
|
||||||
|
local NDK_HOME="${ANDROID_NDK_HOME:-$HOME/Library/Android/sdk/ndk/$(ls $HOME/Library/Android/sdk/ndk/ 2>/dev/null | sort -V | tail -1)}"
|
||||||
|
|
||||||
|
if [ ! -d "$NDK_HOME" ]; then
|
||||||
|
echo "Error: Android NDK not found. Set ANDROID_NDK_HOME."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64"
|
||||||
|
local OUTPUT_DIR="$SCRIPT_DIR/android/src/main/jniLibs"
|
||||||
|
mkdir -p "$OUTPUT_DIR/arm64-v8a" "$OUTPUT_DIR/x86_64"
|
||||||
|
|
||||||
|
# Android needs the "android" build tag for proper platform detection
|
||||||
|
# (e.g., netmon_polling.go instead of netmon_linux.go which requires CAP_NET_ADMIN)
|
||||||
|
local ANDROID_TAGS="android,$OMIT_TAGS"
|
||||||
|
|
||||||
|
# Android arm64
|
||||||
|
echo "==> [Android] Building for arm64-v8a..."
|
||||||
|
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
|
||||||
|
CC="$TOOLCHAIN/bin/aarch64-linux-android21-clang" \
|
||||||
|
go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \
|
||||||
|
-o "$OUTPUT_DIR/arm64-v8a/libtailscale.so" .
|
||||||
|
|
||||||
|
# Android x86_64 (emulator)
|
||||||
|
echo "==> [Android] Building for x86_64..."
|
||||||
|
CGO_ENABLED=1 GOOS=android GOARCH=amd64 \
|
||||||
|
CC="$TOOLCHAIN/bin/x86_64-linux-android21-clang" \
|
||||||
|
go build -buildmode=c-shared -tags "$ANDROID_TAGS" -ldflags="-s -w" \
|
||||||
|
-o "$OUTPUT_DIR/x86_64/libtailscale.so" .
|
||||||
|
|
||||||
|
# Clean up generated headers (not needed for .so)
|
||||||
|
rm -f "$OUTPUT_DIR"/*/libtailscale.h
|
||||||
|
|
||||||
|
echo "==> [Android] Done:"
|
||||||
|
ls -lh "$OUTPUT_DIR/arm64-v8a/libtailscale.so"
|
||||||
|
ls -lh "$OUTPUT_DIR/x86_64/libtailscale.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Linux (c-shared → .so)
|
||||||
|
# ============================================================
|
||||||
|
build_linux() {
|
||||||
|
local OUTPUT_DIR="$SCRIPT_DIR/linux"
|
||||||
|
|
||||||
|
# Linux amd64 — requires a Linux cross-compiler or building on Linux.
|
||||||
|
# On macOS, use Docker: docker run --rm -v $PWD:/src -w /src golang:1.23 ./build_go.sh linux
|
||||||
|
echo "==> [Linux] Building for amd64..."
|
||||||
|
if [ "$(uname)" = "Linux" ]; then
|
||||||
|
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -buildmode=c-shared -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||||
|
-o "$OUTPUT_DIR/libtailscale.so" .
|
||||||
|
else
|
||||||
|
echo " Cross-compiling Linux from $(uname) via Docker..."
|
||||||
|
docker run --rm --platform linux/amd64 -v "$SCRIPT_DIR:/src" -w /src/ios/Go \
|
||||||
|
golang:latest bash -c "
|
||||||
|
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -buildmode=c-shared -tags '$OMIT_TAGS' -ldflags='-s -w' \
|
||||||
|
-o /src/linux/libtailscale.so .
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$OUTPUT_DIR/libtailscale.h"
|
||||||
|
|
||||||
|
echo "==> [Linux] Done:"
|
||||||
|
ls -lh "$OUTPUT_DIR/libtailscale.so"
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Main
|
# Main
|
||||||
# ============================================================
|
# ============================================================
|
||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
ios) build_ios ;;
|
ios) build_ios ;;
|
||||||
macos) build_macos ;;
|
macos) build_macos ;;
|
||||||
all) build_ios; build_macos ;;
|
android) build_android ;;
|
||||||
*) echo "Usage: $0 [ios|macos|all]"; exit 1 ;;
|
linux) build_linux ;;
|
||||||
|
all) build_ios; build_macos; build_android; build_linux ;;
|
||||||
|
apple) build_ios; build_macos ;;
|
||||||
|
*) echo "Usage: $0 [ios|macos|android|linux|apple|all]"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
39
ios/Go/android_interfaces.go
Normal file
39
ios/Go/android_interfaces.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"tailscale.com/net/netmon"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Register an alternate interface getter for Android.
|
||||||
|
// Go's net.Interfaces() uses netlink which requires CAP_NET_ADMIN,
|
||||||
|
// and /sys/class/net is restricted on modern Android.
|
||||||
|
// Provide a minimal loopback interface to satisfy tsnet's startup.
|
||||||
|
// tsnet uses userspace netstack and doesn't need real interface data.
|
||||||
|
netmon.RegisterInterfaceGetter(func() ([]netmon.Interface, error) {
|
||||||
|
return []netmon.Interface{
|
||||||
|
{
|
||||||
|
Interface: &net.Interface{
|
||||||
|
Index: 1,
|
||||||
|
MTU: 65536,
|
||||||
|
Name: "lo",
|
||||||
|
HardwareAddr: nil,
|
||||||
|
Flags: net.FlagUp | net.FlagLoopback | net.FlagRunning,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Interface: &net.Interface{
|
||||||
|
Index: 2,
|
||||||
|
MTU: 1500,
|
||||||
|
Name: "wlan0",
|
||||||
|
HardwareAddr: net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x01},
|
||||||
|
Flags: net.FlagUp | net.FlagRunning | net.FlagMulticast,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -20,6 +20,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -50,6 +52,13 @@ func TailscaleStart(stateDir, authKey, hostname *C.char) *C.char {
|
|||||||
return nil // already started — idempotent
|
return nil // already started — idempotent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Android, Go's net.Interfaces() uses netlink which requires
|
||||||
|
// CAP_NET_ADMIN. Set GODEBUG to use Go's pure DNS resolver and
|
||||||
|
// suppress network interface enumeration errors.
|
||||||
|
if runtime.GOOS == "android" {
|
||||||
|
os.Setenv("GODEBUG", "netdns=go")
|
||||||
|
}
|
||||||
|
|
||||||
s := &tsnet.Server{
|
s := &tsnet.Server{
|
||||||
Dir: C.GoString(stateDir),
|
Dir: C.GoString(stateDir),
|
||||||
Hostname: C.GoString(hostname),
|
Hostname: C.GoString(hostname),
|
||||||
|
|||||||
125
lib/tsnet_ffi.dart
Normal file
125
lib/tsnet_ffi.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,16 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:tsnet_flutter/tsnet_ffi.dart';
|
||||||
|
|
||||||
/// Embedded Tailscale tsnet client for Flutter.
|
/// Embedded Tailscale tsnet client for Flutter.
|
||||||
///
|
///
|
||||||
/// Provides a userspace WireGuard tunnel via Tailscale's tsnet library,
|
/// Provides a userspace WireGuard tunnel via Tailscale's tsnet library,
|
||||||
/// without requiring the user to install the Tailscale app or a VPN entitlement.
|
/// without requiring the user to install the Tailscale app or a VPN entitlement.
|
||||||
///
|
///
|
||||||
/// Architecture: Go static library (tsnet) ↔ Swift C bridge ↔ MethodChannel ↔ Dart
|
/// - iOS/macOS: Swift MethodChannel → C bridge → Go static library
|
||||||
|
/// - Android/Linux: Dart FFI → Go shared library (.so)
|
||||||
///
|
///
|
||||||
/// Usage:
|
/// Usage:
|
||||||
/// ```dart
|
/// ```dart
|
||||||
@ -19,52 +22,93 @@ import 'package:flutter/services.dart';
|
|||||||
class TsnetFlutter {
|
class TsnetFlutter {
|
||||||
static const _channel = MethodChannel('tsnet_flutter');
|
static const _channel = MethodChannel('tsnet_flutter');
|
||||||
|
|
||||||
|
/// FFI backend for Android/Linux — initialized lazily.
|
||||||
|
TsnetFfi? _ffi;
|
||||||
|
|
||||||
|
bool get _useFfi => Platform.isAndroid || Platform.isLinux;
|
||||||
|
|
||||||
|
TsnetFfi get _ensureFfi => _ffi ??= TsnetFfi();
|
||||||
|
|
||||||
|
/// State directory for persisting Tailscale identity.
|
||||||
|
String? _stateDir;
|
||||||
|
|
||||||
|
Future<String> _getStateDir() async {
|
||||||
|
if (_stateDir != null) return _stateDir!;
|
||||||
|
|
||||||
|
// Use a directory relative to the current working directory or temp
|
||||||
|
// For proper production use, pass a path from path_provider
|
||||||
|
late final Directory dir;
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
// Android: use the app's internal storage via the temp directory parent
|
||||||
|
final temp = Directory.systemTemp;
|
||||||
|
dir = Directory('${temp.parent.path}/tailscale_state');
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
final home = Platform.environment['HOME'] ?? '/tmp';
|
||||||
|
dir = Directory('$home/.local/share/tailscale_state');
|
||||||
|
} else {
|
||||||
|
dir = Directory('${Directory.systemTemp.path}/tailscale_state');
|
||||||
|
}
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
_stateDir = dir.path;
|
||||||
|
return _stateDir!;
|
||||||
|
}
|
||||||
|
|
||||||
/// Join the Tailnet with the given auth key.
|
/// Join the Tailnet with the given auth key.
|
||||||
///
|
|
||||||
/// [authKey] is a Tailscale auth key (reusable, tag-scoped).
|
|
||||||
/// [hostname] is the name this device will appear as in the Tailnet.
|
|
||||||
///
|
|
||||||
/// The auth key is typically received from the heater during BLE provisioning.
|
|
||||||
/// State is persisted to the app's documents directory so the device
|
|
||||||
/// identity survives app restarts.
|
|
||||||
Future<void> start({
|
Future<void> start({
|
||||||
required String authKey,
|
required String authKey,
|
||||||
String hostname = 'constellation-phone',
|
String hostname = 'tsnet-flutter',
|
||||||
}) async {
|
}) async {
|
||||||
|
if (_useFfi) {
|
||||||
|
final dir = await _getStateDir();
|
||||||
|
await _ensureFfi.start(dir, authKey, hostname);
|
||||||
|
} else {
|
||||||
await _channel.invokeMethod('start', {
|
await _channel.invokeMethod('start', {
|
||||||
'authKey': authKey,
|
'authKey': authKey,
|
||||||
'hostname': hostname,
|
'hostname': hostname,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a localhost TCP proxy to a heater's Tailscale IP.
|
/// Create a localhost TCP proxy to a remote Tailscale IP.
|
||||||
///
|
/// Returns the local port number.
|
||||||
/// Returns the local port number. Connect gRPC to `127.0.0.1:<port>`.
|
Future<int> startProxy(String remoteIP, {int port = 5050}) async {
|
||||||
/// Traffic is forwarded through the Tailscale WireGuard tunnel to
|
if (_useFfi) {
|
||||||
/// [heaterTailscaleIP]:[port] on the heater.
|
return await _ensureFfi.startProxy(remoteIP, port);
|
||||||
///
|
} else {
|
||||||
/// Uses userspace netstack — no VPN entitlement needed on iOS.
|
|
||||||
Future<int> startProxy(String heaterTailscaleIP, {int port = 5050}) async {
|
|
||||||
final localPort = await _channel.invokeMethod<int>('startProxy', {
|
final localPort = await _channel.invokeMethod<int>('startProxy', {
|
||||||
'ip': heaterTailscaleIP,
|
'ip': remoteIP,
|
||||||
'port': port,
|
'port': port,
|
||||||
});
|
});
|
||||||
return localPort!;
|
return localPort!;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stop the local TCP proxy.
|
/// Stop the local TCP proxy.
|
||||||
Future<void> stopProxy() async {
|
Future<void> stopProxy() async {
|
||||||
|
if (_useFfi) {
|
||||||
|
await _ensureFfi.stopProxy();
|
||||||
|
} else {
|
||||||
await _channel.invokeMethod('stopProxy');
|
await _channel.invokeMethod('stopProxy');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Disconnect from the Tailnet and clean up.
|
/// Disconnect from the Tailnet and clean up.
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
|
if (_useFfi) {
|
||||||
|
await _ensureFfi.stop();
|
||||||
|
} else {
|
||||||
await _channel.invokeMethod('stop');
|
await _channel.invokeMethod('stop');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the current Tailscale connection status.
|
/// Get the current Tailscale connection status.
|
||||||
Future<TailscaleStatus> status() async {
|
Future<TailscaleStatus> status() async {
|
||||||
final json = await _channel.invokeMethod<String>('status');
|
String? json;
|
||||||
|
if (_useFfi) {
|
||||||
|
json = await _ensureFfi.status();
|
||||||
|
} else {
|
||||||
|
json = await _channel.invokeMethod<String>('status');
|
||||||
|
}
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
return TailscaleStatus(state: TailscaleState.stopped);
|
return TailscaleStatus(state: TailscaleState.stopped);
|
||||||
}
|
}
|
||||||
@ -73,9 +117,13 @@ class TsnetFlutter {
|
|||||||
|
|
||||||
/// Get this device's Tailscale IPv4 address (100.x.x.x).
|
/// Get this device's Tailscale IPv4 address (100.x.x.x).
|
||||||
Future<String> tailscaleIP() async {
|
Future<String> tailscaleIP() async {
|
||||||
|
if (_useFfi) {
|
||||||
|
return await _ensureFfi.tailscaleIP();
|
||||||
|
} else {
|
||||||
final ip = await _channel.invokeMethod<String>('tailscaleIP');
|
final ip = await _channel.invokeMethod<String>('tailscaleIP');
|
||||||
return ip!;
|
return ip!;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TailscaleState {
|
enum TailscaleState {
|
||||||
|
|||||||
8
linux/CMakeLists.txt
Normal file
8
linux/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(tsnet_flutter_library VERSION 0.2.0 LANGUAGES C)
|
||||||
|
|
||||||
|
# Bundle the pre-built Go shared library with the Flutter app.
|
||||||
|
# The .so is built by build_go.sh and placed in this directory.
|
||||||
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/libtailscale.so"
|
||||||
|
DESTINATION "${CMAKE_INSTALL_PREFIX}/lib"
|
||||||
|
COMPONENT Runtime)
|
||||||
BIN
linux/libtailscale.so
Normal file
BIN
linux/libtailscale.so
Normal file
Binary file not shown.
@ -49,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
|
ffi:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@ -12,6 +12,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
ffi: ^2.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -25,3 +26,8 @@ flutter:
|
|||||||
pluginClass: TsnetFlutterPlugin
|
pluginClass: TsnetFlutterPlugin
|
||||||
macos:
|
macos:
|
||||||
pluginClass: TsnetFlutterPlugin
|
pluginClass: TsnetFlutterPlugin
|
||||||
|
android:
|
||||||
|
package: io.svrnty.tsnet_flutter
|
||||||
|
ffiPlugin: true
|
||||||
|
linux:
|
||||||
|
ffiPlugin: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user