Run byre and the app on their own embedded Tailscale nodes
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
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[..<colon])
|
||||
proxyPort = port
|
||||
proxyCredential = String(decoding: proxyCred.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self)
|
||||
}
|
||||
|
||||
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: 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<CChar>?
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user