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