Add macOS support, idempotent start(), bump to 0.2.0
- macOS plugin: Swift bridge + universal xcframework (arm64 + x86_64) - macOS podspec with direct force_load (no script phase needed — single slice) - Make TailscaleStart() idempotent — return success if already started - Document macOS entitlements (network.client + network.server) - Build script: ./build_go.sh [ios|macos|all] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5c9f318f5a
commit
ae105a2bb0
@ -1,3 +1,10 @@
|
||||
## 0.2.0
|
||||
|
||||
- Add macOS support (arm64 + x86_64 universal binary)
|
||||
- Make `start()` idempotent — calling it when already started returns success
|
||||
- Document macOS entitlement requirements (network.client + network.server)
|
||||
- Build script now supports `./build_go.sh [ios|macos|all]`
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- Fix: xcframework linking when installed from pub.dev (use DERIVED_FILE_DIR instead of symlinks)
|
||||
|
||||
37
README.md
37
README.md
@ -37,23 +37,44 @@ await tsnet.stop();
|
||||
|
||||
## Platform support
|
||||
|
||||
| Platform | Status |
|
||||
|----------|--------|
|
||||
| iOS | Supported (arm64 device + simulator) |
|
||||
| Android | Planned |
|
||||
| Platform | Status | Min version |
|
||||
|----------|--------|-------------|
|
||||
| iOS | Supported (arm64 device + simulator) | 14.0 |
|
||||
| macOS | Supported (arm64 + x86_64 universal) | 12.0 |
|
||||
| Android | Planned | — |
|
||||
|
||||
## 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`:
|
||||
|
||||
```xml
|
||||
<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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- iOS 14.0+
|
||||
- Tailscale auth key (generate at [login.tailscale.com](https://login.tailscale.com/admin/settings/keys))
|
||||
|
||||
## Building from source
|
||||
|
||||
The pre-built xcframework is included in the package. To rebuild from Go source:
|
||||
The pre-built xcframeworks are included in the package. To rebuild from Go source:
|
||||
|
||||
```bash
|
||||
# Prerequisites: Go 1.23+, Xcode
|
||||
./build_go.sh
|
||||
# Prerequisites: Go 1.23+, Xcode with iOS + macOS SDKs
|
||||
./build_go.sh # build all platforms
|
||||
./build_go.sh ios # iOS only
|
||||
./build_go.sh macos # macOS only
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
203
build_go.sh
203
build_go.sh
@ -1,26 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Go static library for iOS using go build -buildmode=c-archive.
|
||||
#
|
||||
# This is the production-grade approach (same as Tailscale's own iOS app).
|
||||
# No gomobile dependency — just the standard Go compiler + Xcode SDK.
|
||||
# Build Go static libraries for iOS and macOS using go build -buildmode=c-archive.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Go 1.23+
|
||||
# - Xcode with iOS SDK
|
||||
# - Xcode with iOS + macOS SDKs
|
||||
#
|
||||
# Usage:
|
||||
# cd /path/to/tailscale_kit
|
||||
# ./build_go.sh
|
||||
#
|
||||
# Output: ios/TailscaleKit.xcframework (static library xcframework)
|
||||
# ./build_go.sh # build all platforms
|
||||
# ./build_go.sh ios # iOS only
|
||||
# ./build_go.sh macos # macOS only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="${GOPATH:-$HOME/go}/bin:$PATH"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
GO_DIR="$SCRIPT_DIR/ios/Go"
|
||||
BUILD_DIR="$SCRIPT_DIR/ios/build"
|
||||
OUTPUT="$SCRIPT_DIR/ios/TailscaleKit.xcframework"
|
||||
MIN_IOS="14.0"
|
||||
MIN_MACOS="12.0"
|
||||
|
||||
# Omit Tailscale features we don't need — we only use tsnet.Dial() for proxying.
|
||||
OMIT_TAGS="ts_omit_ssh,ts_omit_drive,ts_omit_taildrop,ts_omit_serve"
|
||||
@ -35,32 +32,12 @@ OMIT_TAGS="$OMIT_TAGS,ts_omit_usermetrics,ts_omit_desktop_sessions,ts_omit_ace"
|
||||
OMIT_TAGS="$OMIT_TAGS,ts_omit_acme,ts_omit_hujsonconf,ts_omit_tailnetlock"
|
||||
OMIT_TAGS="$OMIT_TAGS,ts_omit_netlog,ts_omit_syspolicy,ts_omit_cachenetmap"
|
||||
|
||||
rm -rf "$BUILD_DIR" "$OUTPUT"
|
||||
mkdir -p "$BUILD_DIR/ios-arm64/Headers" "$BUILD_DIR/ios-arm64-simulator/Headers"
|
||||
PLATFORM="${1:-all}"
|
||||
|
||||
cd "$GO_DIR"
|
||||
|
||||
echo "==> Tidying Go modules..."
|
||||
go mod tidy
|
||||
|
||||
# --- Build for iOS device (arm64) ---
|
||||
echo "==> Building for iOS device (arm64)..."
|
||||
IPHONEOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
IPHONEOS_CC=$(xcrun --sdk iphoneos --find clang)
|
||||
|
||||
CGO_ENABLED=1 \
|
||||
GOOS=ios \
|
||||
GOARCH=arm64 \
|
||||
CC="$IPHONEOS_CC" \
|
||||
CGO_CFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \
|
||||
CGO_LDFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \
|
||||
go build \
|
||||
-buildmode=c-archive \
|
||||
-tags "$OMIT_TAGS" \
|
||||
-ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/ios-arm64/libtailscale.a" \
|
||||
.
|
||||
|
||||
# Helper: create a framework bundle from a static library
|
||||
create_framework() {
|
||||
local lib_path="$1"
|
||||
@ -68,14 +45,9 @@ create_framework() {
|
||||
local framework_dir="$3"
|
||||
|
||||
mkdir -p "$framework_dir/Headers" "$framework_dir/Modules"
|
||||
|
||||
# Copy the static library as the framework binary
|
||||
cp "$lib_path" "$framework_dir/TailscaleKit"
|
||||
|
||||
# Copy and clean the header (remove Go-internal types)
|
||||
cp "$header_path" "$framework_dir/Headers/tailscale.h"
|
||||
|
||||
# Module map
|
||||
cat > "$framework_dir/Modules/module.modulemap" <<'MODMAP'
|
||||
framework module TailscaleKit {
|
||||
header "tailscale.h"
|
||||
@ -83,14 +55,13 @@ framework module TailscaleKit {
|
||||
}
|
||||
MODMAP
|
||||
|
||||
# Info.plist
|
||||
cat > "$framework_dir/Info.plist" <<PLIST
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.constellation-heating.TailscaleKit</string>
|
||||
<string>io.svrnty.TailscaleKit</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>TailscaleKit</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@ -102,48 +73,126 @@ MODMAP
|
||||
PLIST
|
||||
}
|
||||
|
||||
create_framework \
|
||||
"$BUILD_DIR/ios-arm64/libtailscale.a" \
|
||||
"$BUILD_DIR/ios-arm64/libtailscale.h" \
|
||||
"$BUILD_DIR/ios-arm64/TailscaleKit.framework"
|
||||
# ============================================================
|
||||
# iOS
|
||||
# ============================================================
|
||||
build_ios() {
|
||||
local BUILD_DIR="$SCRIPT_DIR/ios/build"
|
||||
local OUTPUT="$SCRIPT_DIR/ios/TailscaleKit.xcframework"
|
||||
|
||||
# --- Build for iOS simulator (arm64) ---
|
||||
echo "==> Building for iOS simulator (arm64)..."
|
||||
SIMULATOR_SDK=$(xcrun --sdk iphonesimulator --show-sdk-path)
|
||||
SIMULATOR_CC=$(xcrun --sdk iphonesimulator --find clang)
|
||||
rm -rf "$BUILD_DIR" "$OUTPUT"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
CGO_ENABLED=1 \
|
||||
GOOS=ios \
|
||||
GOARCH=arm64 \
|
||||
CC="$SIMULATOR_CC" \
|
||||
CGO_CFLAGS="-isysroot $SIMULATOR_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \
|
||||
CGO_LDFLAGS="-isysroot $SIMULATOR_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \
|
||||
go build \
|
||||
-buildmode=c-archive \
|
||||
-tags "$OMIT_TAGS" \
|
||||
-ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/ios-arm64-simulator/libtailscale.a" \
|
||||
.
|
||||
# iOS device (arm64)
|
||||
echo "==> [iOS] Building for device (arm64)..."
|
||||
local IPHONEOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
local IPHONEOS_CC=$(xcrun --sdk iphoneos --find clang)
|
||||
|
||||
create_framework \
|
||||
"$BUILD_DIR/ios-arm64-simulator/libtailscale.a" \
|
||||
"$BUILD_DIR/ios-arm64-simulator/libtailscale.h" \
|
||||
"$BUILD_DIR/ios-arm64-simulator/TailscaleKit.framework"
|
||||
CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \
|
||||
CC="$IPHONEOS_CC" \
|
||||
CGO_CFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \
|
||||
CGO_LDFLAGS="-isysroot $IPHONEOS_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS" \
|
||||
go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/ios-arm64/libtailscale.a" .
|
||||
|
||||
# --- Create xcframework from framework bundles ---
|
||||
echo "==> Creating xcframework..."
|
||||
xcodebuild -create-xcframework \
|
||||
-framework "$BUILD_DIR/ios-arm64/TailscaleKit.framework" \
|
||||
-framework "$BUILD_DIR/ios-arm64-simulator/TailscaleKit.framework" \
|
||||
-output "$OUTPUT"
|
||||
create_framework \
|
||||
"$BUILD_DIR/ios-arm64/libtailscale.a" \
|
||||
"$BUILD_DIR/ios-arm64/libtailscale.h" \
|
||||
"$BUILD_DIR/ios-arm64/TailscaleKit.framework"
|
||||
|
||||
# Clean up build artifacts
|
||||
rm -rf "$BUILD_DIR"
|
||||
# iOS simulator (arm64)
|
||||
echo "==> [iOS] Building for simulator (arm64)..."
|
||||
local SIM_SDK=$(xcrun --sdk iphonesimulator --show-sdk-path)
|
||||
local SIM_CC=$(xcrun --sdk iphonesimulator --find clang)
|
||||
|
||||
# Also copy the header to Classes/ for CocoaPods umbrella header
|
||||
cp "$OUTPUT/ios-arm64/TailscaleKit.framework/Headers/tailscale.h" "$SCRIPT_DIR/ios/Classes/tailscale.h"
|
||||
CGO_ENABLED=1 GOOS=ios GOARCH=arm64 \
|
||||
CC="$SIM_CC" \
|
||||
CGO_CFLAGS="-isysroot $SIM_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \
|
||||
CGO_LDFLAGS="-isysroot $SIM_SDK -arch arm64 -miphoneos-version-min=$MIN_IOS -target arm64-apple-ios${MIN_IOS}-simulator" \
|
||||
go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/ios-sim-arm64/libtailscale.a" .
|
||||
|
||||
echo "==> Built: $OUTPUT"
|
||||
du -sh "$OUTPUT"
|
||||
echo "==> Device framework:"
|
||||
ls -lh "$OUTPUT/ios-arm64/TailscaleKit.framework/TailscaleKit"
|
||||
create_framework \
|
||||
"$BUILD_DIR/ios-sim-arm64/libtailscale.a" \
|
||||
"$BUILD_DIR/ios-sim-arm64/libtailscale.h" \
|
||||
"$BUILD_DIR/ios-sim-arm64/TailscaleKit.framework"
|
||||
|
||||
# Create xcframework
|
||||
echo "==> [iOS] Creating xcframework..."
|
||||
xcodebuild -create-xcframework \
|
||||
-framework "$BUILD_DIR/ios-arm64/TailscaleKit.framework" \
|
||||
-framework "$BUILD_DIR/ios-sim-arm64/TailscaleKit.framework" \
|
||||
-output "$OUTPUT"
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
cp "$OUTPUT/ios-arm64/TailscaleKit.framework/Headers/tailscale.h" "$SCRIPT_DIR/ios/Classes/tailscale.h"
|
||||
|
||||
echo "==> [iOS] Done: $OUTPUT"
|
||||
du -sh "$OUTPUT"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# macOS
|
||||
# ============================================================
|
||||
build_macos() {
|
||||
local BUILD_DIR="$SCRIPT_DIR/macos/build"
|
||||
local OUTPUT="$SCRIPT_DIR/macos/TailscaleKit.xcframework"
|
||||
|
||||
rm -rf "$BUILD_DIR" "$OUTPUT"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
local MACOS_SDK=$(xcrun --sdk macosx --show-sdk-path)
|
||||
local MACOS_CC=$(xcrun --sdk macosx --find clang)
|
||||
|
||||
# macOS arm64
|
||||
echo "==> [macOS] Building for arm64..."
|
||||
CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \
|
||||
CC="$MACOS_CC" \
|
||||
CGO_CFLAGS="-isysroot $MACOS_SDK -arch arm64 -mmacosx-version-min=$MIN_MACOS" \
|
||||
CGO_LDFLAGS="-isysroot $MACOS_SDK -arch arm64 -mmacosx-version-min=$MIN_MACOS" \
|
||||
go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/macos-arm64/libtailscale.a" .
|
||||
|
||||
# macOS amd64
|
||||
echo "==> [macOS] Building for amd64..."
|
||||
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 \
|
||||
CC="$MACOS_CC" \
|
||||
CGO_CFLAGS="-isysroot $MACOS_SDK -arch x86_64 -mmacosx-version-min=$MIN_MACOS" \
|
||||
CGO_LDFLAGS="-isysroot $MACOS_SDK -arch x86_64 -mmacosx-version-min=$MIN_MACOS" \
|
||||
go build -buildmode=c-archive -tags "$OMIT_TAGS" -ldflags="-s -w" \
|
||||
-o "$BUILD_DIR/macos-amd64/libtailscale.a" .
|
||||
|
||||
# Create universal binary with lipo
|
||||
echo "==> [macOS] Creating universal binary (arm64 + x86_64)..."
|
||||
mkdir -p "$BUILD_DIR/macos-universal"
|
||||
lipo -create \
|
||||
"$BUILD_DIR/macos-arm64/libtailscale.a" \
|
||||
"$BUILD_DIR/macos-amd64/libtailscale.a" \
|
||||
-output "$BUILD_DIR/macos-universal/libtailscale.a"
|
||||
|
||||
create_framework \
|
||||
"$BUILD_DIR/macos-universal/libtailscale.a" \
|
||||
"$BUILD_DIR/macos-arm64/libtailscale.h" \
|
||||
"$BUILD_DIR/macos-universal/TailscaleKit.framework"
|
||||
|
||||
# Create xcframework (single macOS slice)
|
||||
echo "==> [macOS] Creating xcframework..."
|
||||
xcodebuild -create-xcframework \
|
||||
-framework "$BUILD_DIR/macos-universal/TailscaleKit.framework" \
|
||||
-output "$OUTPUT"
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
|
||||
echo "==> [macOS] Done: $OUTPUT"
|
||||
du -sh "$OUTPUT"
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
case "$PLATFORM" in
|
||||
ios) build_ios ;;
|
||||
macos) build_macos ;;
|
||||
all) build_ios; build_macos ;;
|
||||
*) echo "Usage: $0 [ios|macos|all]"; exit 1 ;;
|
||||
esac
|
||||
|
||||
@ -47,7 +47,7 @@ func TailscaleStart(stateDir, authKey, hostname *C.char) *C.char {
|
||||
defer mu.Unlock()
|
||||
|
||||
if server != nil {
|
||||
return C.CString("already started")
|
||||
return nil // already started — idempotent
|
||||
}
|
||||
|
||||
s := &tsnet.Server{
|
||||
|
||||
1
macos/.gitignore
vendored
Normal file
1
macos/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
TailscaleKit.xcframework/
|
||||
1
macos/.pubignore
Normal file
1
macos/.pubignore
Normal file
@ -0,0 +1 @@
|
||||
# Only ignore build artifacts — the xcframework ships with the package
|
||||
162
macos/Classes/TsnetFlutterPlugin.swift
Normal file
162
macos/Classes/TsnetFlutterPlugin.swift
Normal file
@ -0,0 +1,162 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
|
||||
public class TsnetFlutterPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
private lazy var stateDir: String = {
|
||||
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||
let dir = appSupport.appendingPathComponent("tailscale_state", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
return dir.path
|
||||
}()
|
||||
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "tsnet_flutter", binaryMessenger: registrar.messenger)
|
||||
let instance = TsnetFlutterPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "start":
|
||||
handleStart(call, result: result)
|
||||
case "startProxy":
|
||||
handleStartProxy(call, result: result)
|
||||
case "stopProxy":
|
||||
handleStopProxy(result: result)
|
||||
case "stop":
|
||||
handleStop(result: result)
|
||||
case "status":
|
||||
handleStatus(result: result)
|
||||
case "tailscaleIP":
|
||||
handleTailscaleIP(result: result)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
private func consumeCString(_ ptr: UnsafeMutablePointer<CChar>?) -> String? {
|
||||
guard let ptr = ptr else { return nil }
|
||||
let str = String(cString: ptr)
|
||||
TailscaleFreeString(ptr)
|
||||
return str
|
||||
}
|
||||
|
||||
private func handleStart(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let authKey = args["authKey"] as? String,
|
||||
let hostname = args["hostname"] as? String else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing authKey or hostname", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
let dir = self.stateDir
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
let errPtr = dir.withCString { dirC in
|
||||
authKey.withCString { keyC in
|
||||
hostname.withCString { hostC in
|
||||
TailscaleStart(
|
||||
UnsafeMutablePointer(mutating: dirC),
|
||||
UnsafeMutablePointer(mutating: keyC),
|
||||
UnsafeMutablePointer(mutating: hostC)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
let err = self.consumeCString(errPtr)
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "START_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStartProxy(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? [String: Any],
|
||||
let ip = args["ip"] as? String,
|
||||
let port = args["port"] as? Int else {
|
||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing ip or port", details: nil))
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
var localPort: CInt = 0
|
||||
let errPtr = ip.withCString { ipC in
|
||||
TailscaleStartProxy(
|
||||
UnsafeMutablePointer(mutating: ipC),
|
||||
CInt(port),
|
||||
&localPort
|
||||
)
|
||||
}
|
||||
let err = self.consumeCString(errPtr)
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "PROXY_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(Int(localPort))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStopProxy(result: @escaping FlutterResult) {
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
let err = self.consumeCString(TailscaleStopProxy())
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "STOP_PROXY_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStop(result: @escaping FlutterResult) {
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
let err = self.consumeCString(TailscaleStop())
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "STOP_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleStatus(result: @escaping FlutterResult) {
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
var errPtr: UnsafeMutablePointer<CChar>? = nil
|
||||
let jsonPtr = TailscaleStatus(&errPtr)
|
||||
let err = self.consumeCString(errPtr)
|
||||
let json = self.consumeCString(jsonPtr)
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "STATUS_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleTailscaleIP(result: @escaping FlutterResult) {
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
var errPtr: UnsafeMutablePointer<CChar>? = nil
|
||||
let ipPtr = TailscaleIP(&errPtr)
|
||||
let err = self.consumeCString(errPtr)
|
||||
let ip = self.consumeCString(ipPtr)
|
||||
DispatchQueue.main.async {
|
||||
if let err = err {
|
||||
result(FlutterError(code: "IP_FAILED", message: err, details: nil))
|
||||
} else {
|
||||
result(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
macos/Classes/tailscale.h
Normal file
100
macos/Classes/tailscale.h
Normal file
@ -0,0 +1,100 @@
|
||||
/* Code generated by cmd/cgo; DO NOT EDIT. */
|
||||
|
||||
/* package github.com/constellation-heating/tailscale-kit */
|
||||
|
||||
|
||||
#line 1 "cgo-builtin-export-prolog"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef GO_CGO_EXPORT_PROLOGUE_H
|
||||
#define GO_CGO_EXPORT_PROLOGUE_H
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
|
||||
extern size_t _GoStringLen(_GoString_ s);
|
||||
extern const char *_GoStringPtr(_GoString_ s);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/* Start of preamble from import "C" comments. */
|
||||
|
||||
|
||||
#line 13 "bridge.go"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#line 1 "cgo-generated-wrapper"
|
||||
|
||||
|
||||
/* End of preamble from import "C" comments. */
|
||||
|
||||
|
||||
/* Start of boilerplate cgo prologue. */
|
||||
#line 1 "cgo-gcc-export-header-prolog"
|
||||
|
||||
#ifndef GO_CGO_PROLOGUE_H
|
||||
#define GO_CGO_PROLOGUE_H
|
||||
|
||||
typedef signed char GoInt8;
|
||||
typedef unsigned char GoUint8;
|
||||
typedef short GoInt16;
|
||||
typedef unsigned short GoUint16;
|
||||
typedef int GoInt32;
|
||||
typedef unsigned int GoUint32;
|
||||
typedef long long GoInt64;
|
||||
typedef unsigned long long GoUint64;
|
||||
typedef GoInt64 GoInt;
|
||||
typedef GoUint64 GoUint;
|
||||
typedef size_t GoUintptr;
|
||||
typedef float GoFloat32;
|
||||
typedef double GoFloat64;
|
||||
#ifdef _MSC_VER
|
||||
#if !defined(__cplusplus) || _MSVC_LANG <= 201402L
|
||||
#include <complex.h>
|
||||
typedef _Fcomplex GoComplex64;
|
||||
typedef _Dcomplex GoComplex128;
|
||||
#else
|
||||
#include <complex>
|
||||
typedef std::complex<float> GoComplex64;
|
||||
typedef std::complex<double> GoComplex128;
|
||||
#endif
|
||||
#else
|
||||
typedef float _Complex GoComplex64;
|
||||
typedef double _Complex GoComplex128;
|
||||
#endif
|
||||
|
||||
/*
|
||||
static assertion to make sure the file is being used on architecture
|
||||
at least with matching size of GoInt.
|
||||
*/
|
||||
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];
|
||||
|
||||
#ifndef GO_CGO_GOSTRING_TYPEDEF
|
||||
typedef _GoString_ GoString;
|
||||
#endif
|
||||
typedef void *GoMap;
|
||||
typedef void *GoChan;
|
||||
typedef struct { void *t; void *v; } GoInterface;
|
||||
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
|
||||
|
||||
#endif
|
||||
|
||||
/* End of boilerplate cgo prologue. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern char* TailscaleStart(char* stateDir, char* authKey, char* hostname);
|
||||
extern char* TailscaleStartProxy(char* remoteIP, int remotePort, int* localPort);
|
||||
extern char* TailscaleStopProxy(void);
|
||||
extern char* TailscaleStop(void);
|
||||
extern char* TailscaleStatus(char** errOut);
|
||||
extern char* TailscaleIP(char** errOut);
|
||||
extern void TailscaleFreeString(char* s);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
2
macos/Classes/tailscale_kit.h
Normal file
2
macos/Classes/tailscale_kit.h
Normal file
@ -0,0 +1,2 @@
|
||||
// Expose Go C functions to Swift via the pod's Clang module
|
||||
#include "tailscale.h"
|
||||
26
macos/tsnet_flutter.podspec
Normal file
26
macos/tsnet_flutter.podspec
Normal file
@ -0,0 +1,26 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'tsnet_flutter'
|
||||
s.version = '0.2.0'
|
||||
s.summary = 'Embedded Tailscale tsnet for Flutter (macOS)'
|
||||
s.description = <<-DESC
|
||||
Embed Tailscale's tsnet in Flutter apps. Provides a userspace WireGuard
|
||||
tunnel with a localhost TCP proxy. Built with go build -buildmode=c-archive.
|
||||
DESC
|
||||
s.homepage = 'https://github.com/svrnty/tsnet_flutter'
|
||||
s.license = { :type => 'BSD-3-Clause', :file => '../LICENSE' }
|
||||
s.author = { 'Svrnty' => 'mathias@svrnty.io' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*.{swift,h}'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.preserve_paths = 'TailscaleKit.xcframework'
|
||||
s.dependency 'FlutterMacOS'
|
||||
s.platform = :osx, '12.0'
|
||||
s.swift_version = '5.0'
|
||||
|
||||
s.libraries = 'resolv'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'OTHER_LDFLAGS' => '$(inherited) -force_load "$(PODS_TARGET_SRCROOT)/TailscaleKit.xcframework/macos-arm64_x86_64/TailscaleKit.framework/TailscaleKit"',
|
||||
}
|
||||
end
|
||||
@ -1,6 +1,6 @@
|
||||
name: tsnet_flutter
|
||||
description: Embed Tailscale's tsnet in Flutter apps. Provides a userspace WireGuard tunnel with a localhost TCP proxy — no VPN entitlement needed on iOS.
|
||||
version: 0.1.1
|
||||
version: 0.2.0
|
||||
homepage: https://github.com/svrnty/tsnet_flutter
|
||||
repository: https://github.com/svrnty/tsnet_flutter
|
||||
issue_tracker: https://github.com/svrnty/tsnet_flutter/issues
|
||||
@ -23,3 +23,5 @@ flutter:
|
||||
platforms:
|
||||
ios:
|
||||
pluginClass: TsnetFlutterPlugin
|
||||
macos:
|
||||
pluginClass: TsnetFlutterPlugin
|
||||
|
||||
Loading…
Reference in New Issue
Block a user