Files
archived-libtailscale/swift/TailscaleKit/TailscaleNode.swift
Jonathan Nobels 727bd68619 swift: add iOS simulator support
updates tailscale/tailscale#15802

This adds the required targets to build for the iOS
simulator.   We support both ios, ios-sim, and ios-fat
framework targets.

The sample app is updated to link in the ios-fat version
so it can be run on any target device or sim.
2025-05-22 10:50:34 -04:00

282 lines
8.2 KiB
Swift

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
public let kDefaultControlURL = "https://controlplane.tailscale.com"
/// Configuration for a tailscale application node
public struct Configuration: Sendable {
public let hostName: String ///< The hostname of the node/application instance
public let path: String
public let authKey: String? ///< An auth key. Leave empty to use web auth
public let controlURL: String ///< URL for Tailscale control
public let ephemeral: Bool
public init(hostName: String,
path: String,
authKey: String?,
controlURL: String,
ephemeral: Bool = false)
{
self.hostName = hostName
self.path = path
self.authKey = authKey
self.controlURL = controlURL
self.ephemeral = ephemeral
}
}
/// The layer 3 protocol to use
public enum NetProtocol: String {
case tcp = "tcp"
case udp = "udp"
}
public typealias IPAddresses = (ip4: String?, ip6: String?)
/// TSInterface creates and manages a single userspace Tailscale application
/// node. You may instantiate several "nodes" in a single application. Each
/// will get a unique IP address on the Tailnet.
///
/// The provided wrapper abstract away the C code and allow the writing of proper,
/// compiler checked thread-safe Swift 6.
public actor TailscaleNode {
/// Handle to the underlying Tailscale server. Use this when instantiating
/// new IncomingConnections or OutgoingConnections
public let tailscale: TailscaleHandle?
private let logger: LogSink?
/// Instantiate a new TailscaleNode with the given configuration and
/// and optional LogSink. If no LogSink is provided, logs will be
/// discarded.
///
/// @See tailscale_set_* in Tailscale.h
/// @See tailscale_start in Tailscale.h
///
/// @throws TailscaleError on failure
public init(config: Configuration, logger: LogSink?) throws {
self.logger = logger ?? BlackholeLogger()
tailscale = tailscale_new()
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
logger?.log("Tailscale starting: \(tailscale)")
if let fd = logger?.logFileHandle {
tailscale_set_logfd(tailscale, fd)
}
if let authKey = config.authKey {
tailscale_set_authkey(tailscale, authKey)
}
tailscale_set_hostname(tailscale, config.hostName)
tailscale_set_dir(tailscale, config.path)
tailscale_set_control_url(tailscale, config.controlURL)
tailscale_set_ephemeral(tailscale, config.ephemeral ? 1 : 0)
let res = tailscale_start(tailscale)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
logger?.log("Tailscale started... \(tailscale)")
}
deinit {
if let tailscale {
tailscale_close(tailscale)
}
}
/// Closes/stops the Tailscale server
///
/// @See tailscale_close in Tailscale.h
///
/// @Throws TailscaleError on failure
public func close() async throws {
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
logger?.log("Closing Tailscale: \(tailscale)")
let res = tailscale_close(tailscale)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
logger?.log("Closed Tailscale:\(tailscale)")
}
/// Brings up the Tailscale server
///
/// @See tailscale_up in Tailscale.h
///
/// @throws TailscaleError on failure
public func up() async throws {
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
logger?.log("Bringing Tailscale up :\(tailscale)")
let res = tailscale_up(tailscale)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
logger?.log("Brought Tailscale up:\(tailscale)")
}
/// Tears down the Tailscale server.
///
/// @See tailscale_down in Tailscale.h
///
/// @throws TailscaleError on failure
public func down() throws {
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
logger?.log("Taking Tailscale down :\(tailscale)")
let res = tailscale_up(tailscale)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
logger?.log("Took Tailscale down:\(tailscale)")
}
/// Returns the addresses on the Tailscale server
///
/// @See tailscale_getips in Tailscale.h
///
/// @returns An ipV4 and ipV5 address tuple
/// @throws TailscaleError on failure
public func addrs() async throws -> IPAddresses {
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
let buf = UnsafeMutablePointer<Int8>.allocate(capacity: 128)
defer {
buf.deallocate()
}
let res = tailscale_getips(tailscale, buf, 128)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
let ipList = String(cString: buf)
return ipList.toIPPair()
}
public struct LoopbackConfig: Sendable {
public let address: String
public let proxyCredential: String
public let localAPIKey: String
public var ip: String? {
let parts = address.split(separator: ":")
let addr = parts.first
guard parts.count == 2, let addr else {
return nil
}
return String(addr)
}
public var port: Int? {
let parts = address.split(separator: ":")
let port = parts.last
guard parts.count == 2, let port else {
return nil
}
return Int(port)
}
}
private var loopbackConfig: LoopbackConfig?
/// Starts and returns the address and credentials of a SOCKS5 proxy which can also
/// be used to query the localAPI
public func loopback() throws -> LoopbackConfig {
guard let tailscale else {
throw TailscaleError.badInterfaceHandle
}
if let loopbackConfig = loopbackConfig {
return loopbackConfig
}
let addrBuf = UnsafeMutablePointer<Int8>.allocate(capacity: 64)
let proxyCredBuf = UnsafeMutablePointer<Int8>.allocate(capacity: 33)
let apiCredBuf = UnsafeMutablePointer<Int8>.allocate(capacity: 33)
defer {
addrBuf.deallocate()
proxyCredBuf.deallocate()
apiCredBuf.deallocate()
}
let res = tailscale_loopback(tailscale, addrBuf, 64, proxyCredBuf, apiCredBuf)
guard res == 0 else {
throw TailscaleError.fromPosixErrCode(res, tailscale.getErrorMessage())
}
loopbackConfig = LoopbackConfig(address: String(cString: addrBuf),
proxyCredential: String(cString: proxyCredBuf),
localAPIKey: String(cString: apiCredBuf))
return loopbackConfig!
}
}
// MARK: - IP String list to IPAddresses tuple
enum IPAddrType {
case v4
case v6
case none
}
extension String {
// tailscale.go sends us the tailnetIPs as a comma separated list. This will
// turn them into an IPAddresses tuple
func toIPPair() -> IPAddresses {
let ips = self.split(separator: ",").map { String($0) }
var result: IPAddresses = (nil, nil)
for ip in ips {
let type = ip.tsNetIPAddrType()
switch type {
case .v4:
result.ip4 = ip
case .v6:
result.ip6 = ip
case .none:
break
}
}
return result
}
// This can be naive since the backend is only vending well
// formed IPs to us.
func tsNetIPAddrType() -> IPAddrType {
if self.contains(".") {
return .v4
} else if self.contains(":") {
return .v6
}
return .none
}
}