import Foundation import Network import Observation import SwiftUI import TailscaleKit #if os(iOS) import SafariServices #endif 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(deviceName))" private let socks = TailnetProxy() 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)) } static func check(_ sd: tailscale, _ what: String, _ result: Int32) { if result != 0 { fatalError("tsnet \(what): \(errorMessage(sd))") } } static func errorMessage(_ sd: tailscale) -> 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: .ipv4(.loopback), port: NWEndpoint.Port(rawValue: socks.port)!)) proxy.applyCredential(username: "byre", password: socks.credential) return proxy } func status() -> (state: String, authURL: URL?, servers: [URL], peers: Set)? { var out: UnsafeMutablePointer? guard unsafe tailscale_status_json(sd, &out) == 0, let out = unsafe out else { return nil } defer { unsafe free(out) } struct Node: Decodable { let DNSName: String let UserID: Int64 let Online: Bool } struct Status: Decodable { let BackendState: String let AuthURL: String let me: Node? let Peer: [String: Node]? enum CodingKeys: String, CodingKey { case BackendState, AuthURL, Peer case me = "Self" } } guard let status = try? JSONDecoder().decode(Status.self, from: unsafe Data(bytes: out, count: strlen(out))) else { return nil } let peers = (status.Peer ?? [:]).values let host = { (peer: Node) in (peer.DNSName.hasSuffix(".") ? String(peer.DNSName.dropLast()) : peer.DNSName).lowercased() } let servers = peers.compactMap { peer -> URL? in let name = host(peer).split(separator: ".").first ?? "" guard peer.Online, peer.UserID == status.me?.UserID, name.hasPrefix("byre-"), !name.hasPrefix("byre-app-") else { return nil } return parseOrigin("https://\(host(peer))") } return (status.BackendState, URL(string: status.AuthURL), servers, Set(peers.map(host))) } // 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? private(set) var service: TailnetInfo? private(set) var searched = false private(set) var peers: Set = [] @ObservationIgnored private var servers: [URL] = [] @ObservationIgnored var serviceWatchers = 0 var running: Bool { state == "Running" } // A node starting from its cached netmap reports Running before it knows any peer, and dials made then fail. func reachable(_ origin: URL) -> Bool { running && peers.contains(origin.host()!.lowercased()) } var needsLogin: Bool { state == "NeedsLogin" || state == "NeedsMachineAuth" || service?.state == "NeedsLogin" } private init() { Task { while true { if let status = tailnetNode.status() { state = status.state authURL = status.authURL servers = status.servers if peers != status.peers { peers = status.peers } } try? await Task.sleep(for: .seconds(1)) } } #if os(macOS) Task { await watchService() } #endif Task { await discover() } } #if os(macOS) private func watchService() async { var loopback: URL? var asked: ContinuousClock.Instant? while true { if serviceWatchers == 0, let asked, asked.duration(to: .now) < .seconds(30) { try? await Task.sleep(for: .seconds(2)) continue } asked = .now if loopback == nil { do { loopback = URL(string: try await Server.checks().loopback) } catch { log.error("find the local service: \(errorText(error), privacy: .public)") } } if let loopback { service = try? await Server.tailnet(loopback) if let service, service.state == "Running", let origin = parseOrigin(service.origin), !Servers.shared.origins.contains(origin.absoluteString) { Servers.shared.add(origin) } } try? await Task.sleep(for: .seconds(2)) } } #endif private func discover() async { while true { guard running else { try? await Task.sleep(for: .milliseconds(500)) continue } let candidates = servers.filter { !Servers.shared.origins.contains($0.absoluteString) } let found = await withTaskGroup(of: URL?.self) { group in for origin in candidates { group.addTask { await Self.probe(origin) ? origin : nil } } var found: [URL] = [] for await origin in group { if let origin { found.append(origin) } } return found } for origin in found { log.notice("found \(origin.absoluteString, privacy: .public) on the tailnet") Servers.shared.add(origin) } searched = true try? await Task.sleep(for: .seconds(10)) } } private nonisolated static func probe(_ origin: URL) async -> Bool { let api = API(origin: origin) defer { api.session.invalidateAndCancel() } return await withTaskGroup(of: Bool.self) { group in group.addTask { (try? await api.client.getStatus().ok) != nil } group.addTask { try? await Task.sleep(for: .seconds(5)) return false } let ok = await group.next() ?? false group.cancelAll() return ok } } } struct TailnetLoginView: View { var body: some View { let tailnet = Tailnet.shared VStack(alignment: .leading, spacing: 12) { Text("Sign in to Tailscale") .font(Fonts.font(17, weight: 700)) Text("byre reaches its servers through Tailscale. Sign each of these nodes in to the tailnet your byre servers are on.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 4) NodeLoginRow(hostname: tailnetNode.hostname, role: "this app", state: tailnet.state, authURL: tailnet.authURL) if let service = tailnet.service { NodeLoginRow(hostname: service.hostname, role: "this Mac's server", state: service.state, authURL: URL(string: service.authUrl)) } } .padding(20) .frame(maxWidth: .infinity, alignment: .leading) .background(Palette.raised, in: RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Palette.border)) .frame(maxWidth: 460) .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { Tailnet.shared.serviceWatchers += 1 } .onDisappear { Tailnet.shared.serviceWatchers -= 1 } } } private struct NodeLoginRow: View { let hostname: String let role: String let state: String let authURL: URL? @State private var signingIn = false var body: some View { HStack(alignment: .center, spacing: 12) { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(hostname).font(Fonts.font(13, weight: 700)) Text(role).foregroundStyle(Palette.muted) } if state == "Running" { Text("Signed in").foregroundStyle(Palette.ok) } else { Text(authURL == nil ? "Waiting for Tailscale (\(state))…" : "Waiting for the sign-in (\(state))…") .font(Fonts.font(12)) .foregroundStyle(Palette.muted) } } .frame(maxWidth: .infinity, alignment: .leading) if state != "Running" { Button("Sign in") { #if os(macOS) if let authURL { openExternal(authURL) } #else signingIn = true #endif } .buttonStyle(WebButtonStyle(primary: true)) .disabled(authURL == nil) .fixedSize() } } .padding(12) .background(Palette.bg, in: RoundedRectangle(cornerRadius: 8)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(Palette.border)) #if os(iOS) .sheet(isPresented: $signingIn) { if let authURL { SafariView(url: authURL) .ignoresSafeArea() } } .onChange(of: state) { if state == "Running" { signingIn = false } } #endif } } #if os(iOS) private struct SafariView: UIViewControllerRepresentable { let url: URL func makeUIViewController(context: Context) -> SFSafariViewController { SFSafariViewController(url: url) } func updateUIViewController(_ controller: SFSafariViewController, context: Context) {} } #endif