import Flutter import UIKit /// Swift bridge between Flutter MethodChannel and Go static library. /// /// Calls C functions exported from Go via c-archive: /// TailscaleStart(stateDir, authKey, hostname) → NULL or error /// TailscaleStartProxy(ip, port, &localPort) → NULL or error /// TailscaleStopProxy() → NULL or error /// TailscaleStop() → NULL or error /// TailscaleStatus(&err) → JSON string or NULL /// TailscaleIP(&err) → IP string or NULL /// TailscaleFreeString(ptr) → frees returned strings public class TsnetFlutterPlugin: NSObject, FlutterPlugin { private lazy var stateDir: String = { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let dir = docs.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) } } /// Consume a C string returned by Go: copy to Swift String, then free. private func consumeCString(_ ptr: UnsafeMutablePointer?) -> String? { guard let ptr = ptr else { return nil } let str = String(cString: ptr) TailscaleFreeString(ptr) return str } // TailscaleStart(stateDir, authKey, hostname) → NULL or error 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 // Go's C.GoString() copies immediately, so withCString lifetimes are fine 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) } } } } // TailscaleStartProxy(ip, port, &localPort) → NULL or error 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)) } } } } // TailscaleStopProxy() → NULL or error 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) } } } } // TailscaleStop() → NULL or error 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) } } } } // TailscaleStatus(&errOut) → JSON string or NULL 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) } } } } // TailscaleIP(&errOut) → IP string or NULL 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) } } } } }