flutter-tsnet/ios/Classes/TsnetFlutterPlugin.swift
Mathias Beaulieu-Duncan 63f0315ee7 Rename package to tsnet_flutter for pub.dev publishing under Svrnty
- Rename Dart package: tailscale_kit → tsnet_flutter
- Rename Swift plugin class: TailscaleKitPlugin → TsnetFlutterPlugin
- Rename podspec: tailscale_kit → tsnet_flutter
- Update MethodChannel name to tsnet_flutter
- Add BSD-3-Clause LICENSE (compatible with Tailscale's license)
- Update metadata for pub.dev: homepage, repository, author

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 06:10:34 -04:00

181 lines
7.0 KiB
Swift

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<CChar>?) -> 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<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)
}
}
}
}
// TailscaleIP(&errOut) IP string or NULL
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)
}
}
}
}
}