import AppKit import Foundation import Network import Observation import SwiftUI import TailscaleKit func slug(_ text: String) -> String { var out = "" var dash = false for c in text.lowercased() { if c.isASCII, c.isLetter || c.isNumber { if dash, !out.isEmpty { out += "-" } out.append(c) dash = false continue } dash = true } return out } final class TailnetNode: Sendable { let sd: tailscale let hostname = "byre-app-\(slug(macName))" private let proxyHost: String private let proxyPort: UInt16 private let proxyCredential: String init() { let dir = URL.applicationSupportDirectory.appending(path: "run.fc.byre.mac/tsnet", directoryHint: .isDirectory) do { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } catch { fatalError("create tsnet dir: \(error)") } sd = tailscale_new() Self.check(sd, "set hostname", unsafe tailscale_set_hostname(sd, hostname)) Self.check(sd, "set dir", unsafe tailscale_set_dir(sd, dir.path)) Self.check(sd, "set ephemeral", tailscale_set_ephemeral(sd, 0)) Self.check(sd, "start", tailscale_start(sd)) var addr = [CChar](repeating: 0, count: 64) var proxyCred = [CChar](repeating: 0, count: 33) var apiCred = [CChar](repeating: 0, count: 33) Self.check(sd, "loopback", unsafe tailscale_loopback(sd, &addr, addr.count, &proxyCred, &apiCred)) let address = String(decoding: addr.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self) guard let colon = address.lastIndex(of: ":"), let port = UInt16(address[address.index(after: colon)...]) else { fatalError("tsnet loopback address \(address)") } proxyHost = String(address[.. String { var buf = [CChar](repeating: 0, count: 1024) _ = unsafe tailscale_errmsg(sd, &buf, buf.count) return String(decoding: buf.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self) } func proxy() -> ProxyConfiguration { let proxy = ProxyConfiguration(socksv5Proxy: .hostPort(host: NWEndpoint.Host(proxyHost), port: NWEndpoint.Port(rawValue: proxyPort)!)) proxy.applyCredential(username: "tsnet", password: proxyCredential) return proxy } func status() -> (state: String, authURL: URL?)? { var out: UnsafeMutablePointer? guard unsafe tailscale_status_json(sd, &out) == 0, let out = unsafe out else { return nil } defer { unsafe free(out) } struct Status: Decodable { let BackendState: String let AuthURL: String } guard let status = try? JSONDecoder().decode(Status.self, from: unsafe Data(bytes: out, count: strlen(out))) else { return nil } return (status.BackendState, URL(string: status.AuthURL)) } // Blocks until the connection is made or fails. func dial(_ address: String) throws -> Int32 { var conn: tailscale_conn = 0 guard unsafe tailscale_dial(sd, "tcp", address, &conn) == 0 else { throw ProcessFailure(errorDescription: "dial \(address) on the tailnet: \(Self.errorMessage(sd))") } return conn } } let tailnetNode = TailnetNode() @MainActor @Observable final class Tailnet { static let shared = Tailnet() private(set) var state = "Starting" private(set) var authURL: URL? var running: Bool { state == "Running" } private init() { Task { while true { if let status = tailnetNode.status() { state = status.state authURL = status.authURL } try? await Task.sleep(for: .seconds(1)) } } } } struct TailnetLoginView: View { var body: some View { let tailnet = Tailnet.shared VStack(alignment: .leading, spacing: 10) { Text("byre reaches its servers through its own Tailscale node, \(tailnetNode.hostname). Sign it in to the tailnet your byre servers are on.") .fixedSize(horizontal: false, vertical: true) Button("Sign in to Tailscale") { if let url = tailnet.authURL { NSWorkspace.shared.open(url) } } .disabled(tailnet.authURL == nil) Text(tailnet.authURL == nil ? "Waiting for Tailscale (\(tailnet.state))…" : "Waiting for the sign-in (\(tailnet.state))…") .foregroundStyle(Palette.muted) } .frame(width: 460) .padding(.vertical, 24) .frame(maxWidth: .infinity, maxHeight: .infinity) } }