From ae105a2bb06f60d26021154c6102168f6d1de68b Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Sat, 14 Mar 2026 07:11:01 -0400 Subject: [PATCH] Add macOS support, idempotent start(), bump to 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- CHANGELOG.md | 7 + README.md | 37 ++++- build_go.sh | 203 +++++++++++++++---------- ios/Go/bridge.go | 2 +- macos/.gitignore | 1 + macos/.pubignore | 1 + macos/Classes/TsnetFlutterPlugin.swift | 162 ++++++++++++++++++++ macos/Classes/tailscale.h | 100 ++++++++++++ macos/Classes/tailscale_kit.h | 2 + macos/tsnet_flutter.podspec | 26 ++++ pubspec.yaml | 4 +- 11 files changed, 458 insertions(+), 87 deletions(-) create mode 100644 macos/.gitignore create mode 100644 macos/.pubignore create mode 100644 macos/Classes/TsnetFlutterPlugin.swift create mode 100644 macos/Classes/tailscale.h create mode 100644 macos/Classes/tailscale_kit.h create mode 100644 macos/tsnet_flutter.podspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 74da0e5..6355b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 6865170..344ed8e 100644 --- a/README.md +++ b/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 +com.apple.security.network.client + +com.apple.security.network.server + +``` + +`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 diff --git a/build_go.sh b/build_go.sh index 8430611..02c41af 100755 --- a/build_go.sh +++ b/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" < CFBundleIdentifier - com.constellation-heating.TailscaleKit + io.svrnty.TailscaleKit CFBundleName TailscaleKit CFBundleVersion @@ -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 diff --git a/ios/Go/bridge.go b/ios/Go/bridge.go index dfaea49..b6ad49b 100644 --- a/ios/Go/bridge.go +++ b/ios/Go/bridge.go @@ -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{ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..5170fe3 --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1 @@ +TailscaleKit.xcframework/ diff --git a/macos/.pubignore b/macos/.pubignore new file mode 100644 index 0000000..afffc1a --- /dev/null +++ b/macos/.pubignore @@ -0,0 +1 @@ +# Only ignore build artifacts — the xcframework ships with the package diff --git a/macos/Classes/TsnetFlutterPlugin.swift b/macos/Classes/TsnetFlutterPlugin.swift new file mode 100644 index 0000000..8e813d6 --- /dev/null +++ b/macos/Classes/TsnetFlutterPlugin.swift @@ -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?) -> 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? = 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? = 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) + } + } + } + } +} diff --git a/macos/Classes/tailscale.h b/macos/Classes/tailscale.h new file mode 100644 index 0000000..a792d00 --- /dev/null +++ b/macos/Classes/tailscale.h @@ -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 + +#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 + +#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 +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex 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 diff --git a/macos/Classes/tailscale_kit.h b/macos/Classes/tailscale_kit.h new file mode 100644 index 0000000..8e2e366 --- /dev/null +++ b/macos/Classes/tailscale_kit.h @@ -0,0 +1,2 @@ +// Expose Go C functions to Swift via the pod's Clang module +#include "tailscale.h" diff --git a/macos/tsnet_flutter.podspec b/macos/tsnet_flutter.podspec new file mode 100644 index 0000000..b27582d --- /dev/null +++ b/macos/tsnet_flutter.podspec @@ -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 diff --git a/pubspec.yaml b/pubspec.yaml index ff2b75b..1b3fcb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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