diff --git a/Apple/App/AppDelegate.swift b/Apple/App/AppDelegate.swift index c3cb4cb..12fe52c 100644 --- a/Apple/App/AppDelegate.swift +++ b/Apple/App/AppDelegate.swift @@ -55,7 +55,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let statusBar = NSStatusBar.system let statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) if let button = statusItem.button { - button.image = NSImage(systemSymbolName: "pipe.and.drop.fill", accessibilityDescription: nil) + button.image = NSImage(systemSymbolName: "network.badge.shield.half.filled", accessibilityDescription: nil) } return statusItem }() diff --git a/Apple/AppUITests/BurrowUITests.swift b/Apple/AppUITests/BurrowUITests.swift index b7d8111..f9dbeae 100644 --- a/Apple/AppUITests/BurrowUITests.swift +++ b/Apple/AppUITests/BurrowUITests.swift @@ -1,31 +1,15 @@ import XCTest -import UIKit @MainActor final class BurrowTailnetLoginUITests: XCTestCase { - private enum TailnetLoginMode: String, Decodable { - case tailscale - case discovered - } - - private struct TestConfig: Decodable { - let email: String - let username: String - let password: String - let mode: TailnetLoginMode? - } - override func setUpWithError() throws { continueAfterFailure = false } func testTailnetLoginThroughAuthentikWebSession() throws { - let config = try loadTestConfig() - let email = config.email - let username = config.username - let password = config.password - let mode = config.mode ?? .tailscale - let browserIdentity = mode == .tailscale ? email : username + let email = try requiredEnvironment("BURROW_UI_TEST_EMAIL") + let username = ProcessInfo.processInfo.environment["BURROW_UI_TEST_USERNAME"] ?? email + let password = try requiredEnvironment("BURROW_UI_TEST_PASSWORD") let app = XCUIApplication() app.launch() @@ -34,90 +18,51 @@ final class BurrowTailnetLoginUITests: XCTestCase { XCTAssertTrue(tailnetButton.waitForExistence(timeout: 15), "Tailnet add button did not appear") tailnetButton.tap() - configureTailnetIfNeeded(in: app, mode: mode) - let discoveryField = app.textFields["tailnet-discovery-email"] XCTAssertTrue(discoveryField.waitForExistence(timeout: 10), "Tailnet discovery email field did not appear") replaceText(in: discoveryField, with: email) - let serverCard = app.descendants(matching: .any) - .matching(identifier: "tailnet-server-card") - .firstMatch - XCTAssertTrue(serverCard.waitForExistence(timeout: 5), "Tailnet server card did not appear") + let findServerButton = app.buttons["tailnet-find-server"] + XCTAssertTrue(findServerButton.waitForExistence(timeout: 5), "Find Server button did not appear") + findServerButton.tap() + + let discoveryCard = app.otherElements["tailnet-discovery-card"] + XCTAssertTrue(discoveryCard.waitForExistence(timeout: 20), "Tailnet discovery result did not appear") + + let authorityField = app.textFields["tailnet-authority"] + XCTAssertTrue(authorityField.waitForExistence(timeout: 10), "Tailnet authority field did not appear") + XCTAssertTrue( + waitForFieldValue(authorityField, containing: "ts.burrow.net", timeout: 20), + "Tailnet authority was not populated from discovery" + ) + + let probeButton = app.buttons["tailnet-check-connection"] + XCTAssertTrue(probeButton.waitForExistence(timeout: 5), "Check Connection button did not appear") + probeButton.tap() + + let probeCard = app.otherElements["tailnet-authority-probe-card"] + XCTAssertTrue(probeCard.waitForExistence(timeout: 20), "Tailnet connection probe did not complete") let signInButton = app.buttons["tailnet-start-sign-in"] XCTAssertTrue(signInButton.waitForExistence(timeout: 10), "Tailnet sign-in button did not appear") signInButton.tap() - acceptAuthenticationPromptIfNeeded(in: app, timeout: 20) + acceptAuthenticationPromptIfNeeded(in: app) let webSession = webAuthenticationSession() XCTAssertTrue(webSession.waitForExistence(timeout: 20), "Safari authentication session did not appear") - signIntoAuthentik(in: webSession, username: browserIdentity, password: password) + signIntoAuthentik(in: webSession, username: username, password: password) app.activate() XCTAssertTrue( - waitForTailnetSignedIn(in: app, timeout: 60), + waitForButtonLabel(app.buttons["tailnet-start-sign-in"], equals: "Signed In", timeout: 60), "Tailnet sign-in never reached the running state" ) } - private func configureTailnetIfNeeded(in app: XCUIApplication, mode: TailnetLoginMode) { - guard mode == .discovered else { return } - - openTailnetMenu(in: app) - tapMenuButton(named: "Edit Custom Server", in: app) - - openTailnetMenu(in: app) - tapMenuButton(named: "Show Advanced Settings", in: app) - - let authorityField = app.textFields["tailnet-authority"] - XCTAssertTrue(authorityField.waitForExistence(timeout: 10), "Tailnet authority field did not appear") - replaceText(in: authorityField, with: "") - } - - private func openTailnetMenu(in app: XCUIApplication) { - let moreButton = app.buttons["More"] - XCTAssertTrue(moreButton.waitForExistence(timeout: 5), "Tailnet menu button did not appear") - moreButton.tap() - } - - private func tapMenuButton(named title: String, in app: XCUIApplication) { - let menuButton = firstExistingElement( - from: [ - app.buttons[title], - app.descendants(matching: .button)[title], - ], - timeout: 5 - ) - XCTAssertTrue(menuButton.exists, "Menu action \(title) did not appear") - menuButton.tap() - } - - private func acceptAuthenticationPromptIfNeeded( - in app: XCUIApplication, - timeout: TimeInterval - ) { + private func acceptAuthenticationPromptIfNeeded(in app: XCUIApplication) { let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") - let deadline = Date().addingTimeInterval(timeout) - - repeat { - let promptCandidates = [ - springboard.buttons["Continue"], - springboard.buttons["Allow"], - app.buttons["Continue"], - app.buttons["Allow"], - ] - - for button in promptCandidates where button.exists && button.isHittable { - button.tap() - return - } - - RunLoop.current.run(until: Date().addingTimeInterval(0.25)) - } while Date() < deadline - let promptCandidates = [ springboard.buttons["Continue"], springboard.buttons["Allow"], @@ -125,7 +70,7 @@ final class BurrowTailnetLoginUITests: XCTestCase { app.buttons["Allow"], ] - for button in promptCandidates where button.exists { + for button in promptCandidates where button.waitForExistence(timeout: 3) { button.tap() return } @@ -143,19 +88,6 @@ final class BurrowTailnetLoginUITests: XCTestCase { } private func signIntoAuthentik(in webSession: XCUIApplication, username: String, password: String) { - followTailnetRedirectIfNeeded(in: webSession) - - if !webSession.exists { - return - } - - let immediatePasswordField = firstExistingSecureField(in: webSession, timeout: 2) - if immediatePasswordField.exists { - replaceSecureText(in: immediatePasswordField, within: webSession, with: password) - submitAuthenticationForm(in: webSession, focusedField: immediatePasswordField) - return - } - let usernameField = firstExistingElement( in: webSession, queries: [ @@ -167,12 +99,21 @@ final class BurrowTailnetLoginUITests: XCTestCase { { $0.webViews.textFields["Email or Username"] }, { $0.descendants(matching: .textField).firstMatch }, ], - timeout: 12 + timeout: 25 ) - if !usernameField.exists { + XCTAssertTrue(usernameField.exists, "Authentik username field did not appear") + replaceText(in: usernameField, with: username) + + let immediatePasswordField = firstExistingSecureField(in: webSession, timeout: 2) + if immediatePasswordField.exists { + replaceSecureText(in: immediatePasswordField, with: password) + tapFirstExistingButton( + in: webSession, + titles: ["Continue", "Sign In", "Log in", "Login"], + timeout: 5 + ) return } - replaceText(in: usernameField, with: username) tapFirstExistingButton( in: webSession, @@ -182,31 +123,21 @@ final class BurrowTailnetLoginUITests: XCTestCase { let passwordField = firstExistingSecureField(in: webSession, timeout: 20) XCTAssertTrue(passwordField.exists, "Authentik password field did not appear") - replaceSecureText(in: passwordField, within: webSession, with: password) - submitAuthenticationForm(in: webSession, focusedField: passwordField) - } - - private func followTailnetRedirectIfNeeded(in webSession: XCUIApplication) { - let redirectCandidates = [ - webSession.links["Found"], - webSession.webViews.links["Found"], - webSession.buttons["Found"], - webSession.webViews.buttons["Found"], - ] - - let redirectLink = firstExistingElement(from: redirectCandidates, timeout: 8) - if redirectLink.exists { - redirectLink.tap() - } + replaceSecureText(in: passwordField, with: password) + tapFirstExistingButton( + in: webSession, + titles: ["Continue", "Sign In", "Log in", "Login"], + timeout: 5 + ) } private func firstExistingSecureField(in app: XCUIApplication, timeout: TimeInterval) -> XCUIElement { let candidates = [ - app.descendants(matching: .secureTextField).firstMatch, app.secureTextFields["Password"], app.secureTextFields["Password or Token"], app.webViews.secureTextFields["Password"], app.webViews.secureTextFields["Password or Token"], + app.descendants(matching: .secureTextField).firstMatch, ] return firstExistingElement(from: candidates, timeout: timeout) @@ -229,92 +160,11 @@ final class BurrowTailnetLoginUITests: XCTestCase { button.tap() } - private func submitAuthenticationForm(in app: XCUIApplication, focusedField: XCUIElement) { - focus(focusedField) - focusedField.typeText("\n") - if waitForAny( - [ - { !focusedField.exists }, - { !app.staticTexts["Burrow Tailnet Authentication"].exists }, - ], - timeout: 1.5 - ) { - return - } - - let keyboard = app.keyboards.firstMatch - if keyboard.waitForExistence(timeout: 2) { - let keyboardCandidates = [ - "Return", - "return", - "Go", - "go", - "Continue", - "continue", - "Done", - "done", - "Join", - "join", - "Sign In", - "Log In", - "Login", - ] - for title in keyboardCandidates { - let key = keyboard.buttons[title] - if key.exists && key.isHittable { - key.tap() - return - } - } - - if let lastKey = keyboard.buttons.allElementsBoundByIndex.last, - lastKey.exists, - lastKey.isHittable - { - lastKey.tap() - return - } - } - - tapFirstExistingButton( - in: app, - titles: ["Continue", "Sign In", "Log in", "Login"], - timeout: 5 - ) - } - - private func loadTestConfig() throws -> TestConfig { - let environment = ProcessInfo.processInfo.environment - if let email = nonEmptyEnvironment("BURROW_UI_TEST_EMAIL"), - let password = nonEmptyEnvironment("BURROW_UI_TEST_PASSWORD") - { - return TestConfig( - email: email, - username: nonEmptyEnvironment("BURROW_UI_TEST_USERNAME") ?? email, - password: password, - mode: nonEmptyEnvironment("BURROW_UI_TEST_TAILNET_MODE") - .flatMap(TailnetLoginMode.init(rawValue:)) - ) - } - - let configPath = environment["BURROW_UI_TEST_CONFIG_PATH"] ?? "/tmp/burrow-ui-test-config.json" - let configURL = URL(fileURLWithPath: configPath) - guard FileManager.default.fileExists(atPath: configURL.path) else { - throw XCTSkip( - "Missing UI test configuration. Expected env vars or config file at \(configURL.path)" - ) - } - - let data = try Data(contentsOf: configURL) - return try JSONDecoder().decode(TestConfig.self, from: data) - } - - private func nonEmptyEnvironment(_ key: String) -> String? { - guard let value = ProcessInfo.processInfo.environment[key]? - .trimmingCharacters(in: .whitespacesAndNewlines), - !value.isEmpty + private func requiredEnvironment(_ key: String) throws -> String { + guard let value = ProcessInfo.processInfo.environment[key], + !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - return nil + throw XCTSkip("Missing required UI test environment variable \(key)") } return value } @@ -339,32 +189,6 @@ final class BurrowTailnetLoginUITests: XCTestCase { return XCTWaiter.wait(for: [expectation], timeout: timeout) == .completed } - private func waitForTailnetSignedIn(in app: XCUIApplication, timeout: TimeInterval) -> Bool { - let button = app.buttons["tailnet-start-sign-in"] - let deadline = Date().addingTimeInterval(timeout) - - repeat { - acceptAuthenticationPromptIfNeeded(in: app, timeout: 1) - if button.exists, button.label == "Signed In" { - return true - } - RunLoop.current.run(until: Date().addingTimeInterval(0.3)) - } while Date() < deadline - - return button.exists && button.label == "Signed In" - } - - private func waitForAny(_ conditions: [() -> Bool], timeout: TimeInterval) -> Bool { - let deadline = Date().addingTimeInterval(timeout) - repeat { - if conditions.contains(where: { $0() }) { - return true - } - RunLoop.current.run(until: Date().addingTimeInterval(0.2)) - } while Date() < deadline - return conditions.contains(where: { $0() }) - } - private func firstExistingElement( in app: XCUIApplication, queries: [(XCUIApplication) -> XCUIElement], @@ -386,27 +210,14 @@ final class BurrowTailnetLoginUITests: XCTestCase { } private func replaceText(in element: XCUIElement, with value: String) { - focus(element) + element.tap() clearText(in: element) element.typeText(value) } - private func replaceSecureText(in element: XCUIElement, within app: XCUIApplication, with value: String) { - UIPasteboard.general.string = value - focus(element) - for revealMenu in [ - { element.doubleTap() }, - { element.press(forDuration: 1.2) }, - ] { - revealMenu() - let pasteButton = firstExistingElement(from: pasteCandidates(in: app), timeout: 3) - if pasteButton.exists { - pasteButton.tap() - return - } - } - - focus(element) + private func replaceSecureText(in element: XCUIElement, with value: String) { + element.tap() + clearText(in: element) element.typeText(value) } @@ -418,22 +229,4 @@ final class BurrowTailnetLoginUITests: XCTestCase { let deleteSequence = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count) element.typeText(deleteSequence) } - - private func focus(_ element: XCUIElement) { - element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() - RunLoop.current.run(until: Date().addingTimeInterval(0.3)) - } - - private func pasteCandidates(in app: XCUIApplication) -> [XCUIElement] { - let pasteLabels = ["Paste", "Incolla", "Paste from Clipboard"] - return pasteLabels.flatMap { label in - [ - app.menuItems[label], - app.buttons[label], - app.webViews.buttons[label], - app.descendants(matching: .button).matching(NSPredicate(format: "label == %@", label)).firstMatch, - app.descendants(matching: .menuItem).matching(NSPredicate(format: "label == %@", label)).firstMatch, - ] - } - } } diff --git a/Apple/Configuration/Constants/Constants.swift b/Apple/Configuration/Constants/Constants.swift index 95d8c78..8844564 100644 --- a/Apple/Configuration/Constants/Constants.swift +++ b/Apple/Configuration/Constants/Constants.swift @@ -36,9 +36,13 @@ public enum Constants { private static func fallbackContainerURL() -> Result { #if targetEnvironment(simulator) Result { - // The simulator app's Application Support path lives inside its sandbox container, - // so the host daemon cannot reach it. Use a shared host temp location instead. - let url = URL(filePath: "/tmp", directoryHint: .isDirectory) + let baseURL = try FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + let url = baseURL .appending(component: bundleIdentifier, directoryHint: .isDirectory) .appending(component: "SimulatorFallback", directoryHint: .isDirectory) try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) diff --git a/Apple/Core/Client.swift b/Apple/Core/Client.swift index 7d4cfc7..e44ebcd 100644 --- a/Apple/Core/Client.swift +++ b/Apple/Core/Client.swift @@ -108,13 +108,6 @@ public struct Burrow_TailnetLoginStatusResponse: Sendable { public init() {} } -public struct Burrow_TunnelPacket: Sendable { - public var payload = Data() - public var unknownFields = SwiftProtobuf.UnknownStorage() - - public init() {} -} - extension Burrow_TailnetDiscoverRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = "burrow.TailnetDiscoverRequest" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -394,29 +387,6 @@ extension Burrow_TailnetLoginStatusResponse: SwiftProtobuf.Message, SwiftProtobu } } -extension Burrow_TunnelPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - public static let protoMessageName: String = "burrow.TunnelPacket" - public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "payload") - ] - - public mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - switch fieldNumber { - case 1: try decoder.decodeSingularBytesField(value: &self.payload) - default: break - } - } - } - - public func traverse(visitor: inout V) throws { - if !self.payload.isEmpty { - try visitor.visitSingularBytesField(value: self.payload, fieldNumber: 1) - } - try unknownFields.traverse(visitor: &visitor) - } -} - public struct TailnetClient: Client, GRPCClient { public let channel: GRPCChannel public var defaultCallOptions: CallOptions @@ -486,23 +456,3 @@ public struct TailnetClient: Client, GRPCClient { ) } } - -public struct TunnelPacketClient: Client, GRPCClient { - public let channel: GRPCChannel - public var defaultCallOptions: CallOptions - - public init(channel: any GRPCChannel) { - self.channel = channel - self.defaultCallOptions = .init() - } - - public func makeTunnelPacketsCall( - callOptions: CallOptions? = nil - ) -> GRPCAsyncBidirectionalStreamingCall { - self.makeAsyncBidirectionalStreamingCall( - path: "/burrow.Tunnel/TunnelPackets", - callOptions: callOptions ?? self.defaultCallOptions, - interceptors: [] - ) - } -} diff --git a/Apple/Core/Client/Generated/burrow.pb.swift b/Apple/Core/Client/Generated/burrow.pb.swift index fccd769..bba0f16 100644 --- a/Apple/Core/Client/Generated/burrow.pb.swift +++ b/Apple/Core/Client/Generated/burrow.pb.swift @@ -215,14 +215,6 @@ public struct Burrow_TunnelConfigurationResponse: Sendable { public var mtu: Int32 = 0 - public var routes: [String] = [] - - public var dnsServers: [String] = [] - - public var searchDomains: [String] = [] - - public var includeDefaultRoute: Bool = false - public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -540,10 +532,6 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "addresses"), 2: .same(proto: "mtu"), - 3: .same(proto: "routes"), - 4: .standard(proto: "dns_servers"), - 5: .standard(proto: "search_domains"), - 6: .standard(proto: "include_default_route"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -554,10 +542,6 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob switch fieldNumber { case 1: try { try decoder.decodeRepeatedStringField(value: &self.addresses) }() case 2: try { try decoder.decodeSingularInt32Field(value: &self.mtu) }() - case 3: try { try decoder.decodeRepeatedStringField(value: &self.routes) }() - case 4: try { try decoder.decodeRepeatedStringField(value: &self.dnsServers) }() - case 5: try { try decoder.decodeRepeatedStringField(value: &self.searchDomains) }() - case 6: try { try decoder.decodeSingularBoolField(value: &self.includeDefaultRoute) }() default: break } } @@ -570,28 +554,12 @@ extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtob if self.mtu != 0 { try visitor.visitSingularInt32Field(value: self.mtu, fieldNumber: 2) } - if !self.routes.isEmpty { - try visitor.visitRepeatedStringField(value: self.routes, fieldNumber: 3) - } - if !self.dnsServers.isEmpty { - try visitor.visitRepeatedStringField(value: self.dnsServers, fieldNumber: 4) - } - if !self.searchDomains.isEmpty { - try visitor.visitRepeatedStringField(value: self.searchDomains, fieldNumber: 5) - } - if self.includeDefaultRoute { - try visitor.visitSingularBoolField(value: self.includeDefaultRoute, fieldNumber: 6) - } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Burrow_TunnelConfigurationResponse, rhs: Burrow_TunnelConfigurationResponse) -> Bool { if lhs.addresses != rhs.addresses {return false} if lhs.mtu != rhs.mtu {return false} - if lhs.routes != rhs.routes {return false} - if lhs.dnsServers != rhs.dnsServers {return false} - if lhs.searchDomains != rhs.searchDomains {return false} - if lhs.includeDefaultRoute != rhs.includeDefaultRoute {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Apple/NetworkExtension/PacketTunnelProvider.swift b/Apple/NetworkExtension/PacketTunnelProvider.swift index 3f3d8b4..4f29543 100644 --- a/Apple/NetworkExtension/PacketTunnelProvider.swift +++ b/Apple/NetworkExtension/PacketTunnelProvider.swift @@ -1,7 +1,6 @@ import AsyncAlgorithms import BurrowConfiguration import BurrowCore -import GRPC import libburrow import NetworkExtension import os @@ -20,9 +19,6 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } private let logger = Logger.logger(for: PacketTunnelProvider.self) - private var packetCall: GRPCAsyncBidirectionalStreamingCall? - private var inboundPacketTask: Task? - private var outboundPacketTask: Task? private var client: TunnelClient { get throws { try _client.get() } @@ -49,18 +45,16 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { let completion = SendableCallbackBox(completionHandler) Task { do { - _ = try await client.tunnelStart(.init()) let configuration = try await Array(client.tunnelConfiguration(.init()).prefix(1)).first guard let settings = configuration?.settings else { throw Error.missingTunnelConfiguration } try await setTunnelNetworkSettings(settings) - try startPacketBridge() + _ = try await client.tunnelStart(.init()) logger.log("Started tunnel with network settings: \(settings)") completion.callback(nil) } catch { logger.error("Failed to start tunnel: \(error)") - stopPacketBridge() completion.callback(error) } } @@ -72,7 +66,6 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) { let completion = SendableCallbackBox(completionHandler) Task { - stopPacketBridge() do { _ = try await client.tunnelStop(.init()) logger.log("Stopped client") @@ -84,243 +77,20 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } } -extension PacketTunnelProvider { - private func startPacketBridge() throws { - stopPacketBridge() - - let packetClient = TunnelPacketClient.unix(socketURL: try Constants.socketURL) - let call = packetClient.makeTunnelPacketsCall() - self.packetCall = call - - inboundPacketTask = Task { [weak self] in - guard let self else { return } - do { - for try await packet in call.responseStream { - let payload = packet.payload - self.packetFlow.writePackets( - [payload], - withProtocols: [Self.protocolNumber(for: payload)] - ) - } - } catch { - guard !Task.isCancelled else { return } - self.logger.error("Tunnel packet receive loop failed: \(error)") - } - } - - outboundPacketTask = Task { [weak self] in - guard let self else { return } - defer { call.requestStream.finish() } - do { - while !Task.isCancelled { - let packets = await self.readPacketsBatch() - for (payload, _) in packets { - var packet = Burrow_TunnelPacket() - packet.payload = payload - try await call.requestStream.send(packet) - } - } - } catch { - guard !Task.isCancelled else { return } - self.logger.error("Tunnel packet send loop failed: \(error)") - } - } - } - - private func stopPacketBridge() { - inboundPacketTask?.cancel() - inboundPacketTask = nil - outboundPacketTask?.cancel() - outboundPacketTask = nil - packetCall?.cancel() - packetCall = nil - } - - private func readPacketsBatch() async -> [(Data, NSNumber)] { - await withCheckedContinuation { continuation in - packetFlow.readPackets { packets, protocols in - continuation.resume(returning: Array(zip(packets, protocols))) - } - } - } - - private static func protocolNumber(for payload: Data) -> NSNumber { - guard let version = payload.first.map({ $0 >> 4 }) else { - return NSNumber(value: AF_INET) - } - switch version { - case 6: - return NSNumber(value: AF_INET6) - default: - return NSNumber(value: AF_INET) - } - } -} - extension Burrow_TunnelConfigurationResponse { fileprivate var settings: NEPacketTunnelNetworkSettings { - let parsedAddresses = addresses.compactMap(ParsedTunnelAddress.init(rawValue:)) - let ipv4Addresses = parsedAddresses.compactMap(\.ipv4Address) - let ipv6Addresses = parsedAddresses.compactMap(\.ipv6Address) - let parsedRoutes = routes.compactMap(ParsedTunnelRoute.init(rawValue:)) - var ipv4Routes = parsedRoutes.compactMap(\.ipv4Route) - var ipv6Routes = parsedRoutes.compactMap(\.ipv6Route) - if includeDefaultRoute { - ipv4Routes.append(.default()) - ipv6Routes.append(.default()) - } + let ipv6Addresses = addresses.filter { IPv6Address($0) != nil } let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "1.1.1.1") settings.mtu = NSNumber(value: mtu) - if !ipv4Addresses.isEmpty { - let ipv4Settings = NEIPv4Settings( - addresses: ipv4Addresses.map(\.address), - subnetMasks: ipv4Addresses.map(\.subnetMask) - ) - if !ipv4Routes.isEmpty { - ipv4Settings.includedRoutes = ipv4Routes - } - settings.ipv4Settings = ipv4Settings - } - if !ipv6Addresses.isEmpty { - let ipv6Settings = NEIPv6Settings( - addresses: ipv6Addresses.map(\.address), - networkPrefixLengths: ipv6Addresses.map(\.prefixLength) - ) - if !ipv6Routes.isEmpty { - ipv6Settings.includedRoutes = ipv6Routes - } - settings.ipv6Settings = ipv6Settings - } - if !dnsServers.isEmpty { - let dnsSettings = NEDNSSettings(servers: dnsServers) - if !searchDomains.isEmpty { - dnsSettings.matchDomains = searchDomains - } - settings.dnsSettings = dnsSettings - } + settings.ipv4Settings = NEIPv4Settings( + addresses: addresses.filter { IPv4Address($0) != nil }, + subnetMasks: ["255.255.255.0"] + ) + settings.ipv6Settings = NEIPv6Settings( + addresses: ipv6Addresses, + networkPrefixLengths: ipv6Addresses.map { _ in 64 } + ) return settings } } - -private struct ParsedTunnelAddress { - struct IPv4AddressSetting { - let address: String - let subnetMask: String - } - - struct IPv6AddressSetting { - let address: String - let prefixLength: NSNumber - } - - let ipv4Address: IPv4AddressSetting? - let ipv6Address: IPv6AddressSetting? - - init?(rawValue: String) { - let components = rawValue.split(separator: "/", maxSplits: 1).map(String.init) - let address = components.first?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !address.isEmpty else { - return nil - } - - let prefix = components.count == 2 ? Int(components[1]) : nil - if IPv4Address(address) != nil { - let prefixLength = prefix ?? 32 - guard (0 ... 32).contains(prefixLength) else { - return nil - } - ipv4Address = IPv4AddressSetting( - address: address, - subnetMask: Self.ipv4SubnetMask(prefixLength: prefixLength) - ) - ipv6Address = nil - return - } - - if IPv6Address(address) != nil { - let prefixLength = prefix ?? 128 - guard (0 ... 128).contains(prefixLength) else { - return nil - } - ipv4Address = nil - ipv6Address = IPv6AddressSetting( - address: address, - prefixLength: NSNumber(value: prefixLength) - ) - return - } - - return nil - } - - private static func ipv4SubnetMask(prefixLength: Int) -> String { - guard prefixLength > 0 else { - return "0.0.0.0" - } - let mask = UInt32.max << (32 - prefixLength) - let octets = [ - (mask >> 24) & 0xff, - (mask >> 16) & 0xff, - (mask >> 8) & 0xff, - mask & 0xff, - ] - return octets.map(String.init).joined(separator: ".") - } -} - -private struct ParsedTunnelRoute { - let ipv4Route: NEIPv4Route? - let ipv6Route: NEIPv6Route? - - init?(rawValue: String) { - let components = rawValue.split(separator: "/", maxSplits: 1).map(String.init) - let address = components.first?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !address.isEmpty else { - return nil - } - - let prefix = components.count == 2 ? Int(components[1]) : nil - if IPv4Address(address) != nil { - let prefixLength = prefix ?? 32 - guard (0 ... 32).contains(prefixLength) else { - return nil - } - ipv4Route = NEIPv4Route( - destinationAddress: address, - subnetMask: Self.ipv4SubnetMask(prefixLength: prefixLength) - ) - ipv6Route = nil - return - } - - if IPv6Address(address) != nil { - let prefixLength = prefix ?? 128 - guard (0 ... 128).contains(prefixLength) else { - return nil - } - ipv4Route = nil - ipv6Route = NEIPv6Route( - destinationAddress: address, - networkPrefixLength: NSNumber(value: prefixLength) - ) - return - } - - return nil - } - - private static func ipv4SubnetMask(prefixLength: Int) -> String { - var mask = UInt32.max << (32 - prefixLength) - if prefixLength == 0 { - mask = 0 - } - let octets = [ - String((mask >> 24) & 0xff), - String((mask >> 16) & 0xff), - String((mask >> 8) & 0xff), - String(mask & 0xff), - ] - return octets.joined(separator: ".") - } -} diff --git a/Apple/UI/BurrowView.swift b/Apple/UI/BurrowView.swift index e15d3f7..2128ec3 100644 --- a/Apple/UI/BurrowView.swift +++ b/Apple/UI/BurrowView.swift @@ -83,7 +83,7 @@ public struct BurrowView: View { ContentUnavailableView( "No Accounts Yet", systemImage: "person.crop.circle.badge.plus", - description: Text("Save a Tor account or sign in to Tailnet to keep network identities ready on this device.") + description: Text("Save a Tor account or sign in to a Tailnet provider to keep network identities ready on this device.") ) .frame(maxWidth: .infinity, minHeight: 180) } else { @@ -135,7 +135,7 @@ public struct BurrowView: View { private func runAutomationIfNeeded() { guard !didRunAutomation, let automation = BurrowAutomationConfig.current, - automation.action == .tailnetLogin || automation.action == .tailnetProbe + automation.action == .tailnetLogin || automation.action == .headscaleProbe else { return } @@ -340,12 +340,8 @@ private struct ConfigurationSheetView: View { @State private var isStartingTailnetLogin = false @State private var tailnetPresentedAuthURL: URL? @State private var preserveTailnetLoginSession = false - @State private var usesCustomTailnetAuthority = false - @State private var showsAdvancedTailnetSettings = false @State private var browserAuthenticator = TailnetBrowserAuthenticator() @State private var tailnetLoginPollTask: Task? - @State private var tailnetDiscoveryTask: Task? - @State private var tailnetProbeTask: Task? @State private var didRunAutomation = false init( @@ -368,9 +364,14 @@ private struct ConfigurationSheetView: View { .listRowInsets(.init(top: 4, leading: 0, bottom: 4, trailing: 0)) .listRowBackground(Color.clear) - if showsIdentitySection { - Section("Identity") { - identityFields + Section("Identity") { + TextField("Title", text: $draft.title) + TextField("Account", text: $draft.accountName) + TextField("Identity", text: $draft.identityName) + if sheet == .tailnet { + TextField("Hostname", text: $draft.hostname) + .burrowLoginField() + .autocorrectionDisabled() } } @@ -457,15 +458,9 @@ private struct ConfigurationSheetView: View { } .onChange(of: draft.authority) { _, _ in resetAuthorityProbe() - if sheet == .tailnet, usesCustomTailnetAuthority { - scheduleTailnetAuthorityProbe() - } } .onChange(of: draft.discoveryEmail) { _, _ in resetTailnetDiscoveryFeedback() - if sheet == .tailnet, !usesCustomTailnetAuthority { - scheduleTailnetDiscovery() - } } .onChange(of: draft.authMode) { _, newMode in guard newMode != .web else { return } @@ -475,8 +470,6 @@ private struct ConfigurationSheetView: View { } .onDisappear { tailnetLoginPollTask?.cancel() - tailnetDiscoveryTask?.cancel() - tailnetProbeTask?.cancel() browserAuthenticator.cancel() if !preserveTailnetLoginSession { Task { @MainActor in @@ -486,18 +479,6 @@ private struct ConfigurationSheetView: View { } } - @ViewBuilder - private var identityFields: some View { - TextField("Title", text: $draft.title) - TextField("Account", text: $draft.accountName) - TextField("Identity", text: $draft.identityName) - if sheet == .tailnet { - TextField("Hostname", text: $draft.hostname) - .burrowLoginField() - .autocorrectionDisabled() - } - } - @ViewBuilder private var tailnetSections: some View { Section("Connection") { @@ -506,39 +487,67 @@ private struct ConfigurationSheetView: View { .burrowLoginField() .autocorrectionDisabled() .accessibilityIdentifier("tailnet-discovery-email") - .submitLabel(.continue) - .onSubmit { - if !usesCustomTailnetAuthority { - scheduleTailnetDiscovery(immediate: true) + + Button { + discoverTailnetAuthority() + } label: { + Label { + Text(isDiscoveringTailnet ? "Finding Server" : "Find Server") + } icon: { + Image(systemName: isDiscoveringTailnet ? "hourglass" : "at.circle") } } + .buttonStyle(.borderless) + .disabled(isDiscoveringTailnet || normalizedOptional(draft.discoveryEmail) == nil) + .accessibilityIdentifier("tailnet-find-server") - tailnetServerCard + if let discoveryStatus { + tailnetDiscoveryCard(status: discoveryStatus, failure: nil) + } else if let discoveryError { + tailnetDiscoveryCard(status: nil, failure: discoveryError) + } - if showsAdvancedTailnetSettings { - if usesCustomTailnetAuthority { - TextField("Server URL", text: $draft.authority) - .burrowLoginField() - .autocorrectionDisabled() - .accessibilityIdentifier("tailnet-authority") - } else { - TextField("Tailnet", text: $draft.tailnet) - .burrowLoginField() - .autocorrectionDisabled() - .accessibilityIdentifier("tailnet-name") + TextField("Authority URL", text: $draft.authority) + .burrowLoginField() + .autocorrectionDisabled() + .accessibilityIdentifier("tailnet-authority") + + Text("Use the managed Tailnet authority or enter a custom Tailnet control server.") + .font(.footnote) + .foregroundStyle(.secondary) + + Button { + probeTailnetAuthority() + } label: { + Label { + Text(isProbingAuthority ? "Checking Connection" : "Check Connection") + } icon: { + Image(systemName: isProbingAuthority ? "hourglass" : "bolt.horizontal.circle") } } + .buttonStyle(.borderless) + .disabled(isProbingAuthority || normalizedOptional(draft.authority) == nil) + .accessibilityIdentifier("tailnet-check-connection") + + if let authorityProbeStatus { + tailnetAuthorityProbeCard(status: authorityProbeStatus, failure: nil) + } else if let authorityProbeError { + tailnetAuthorityProbeCard(status: nil, failure: authorityProbeError) + } + + TextField("Tailnet", text: $draft.tailnet) + .burrowLoginField() + .autocorrectionDisabled() + .accessibilityIdentifier("tailnet-name") } Section("Authentication") { - if showsAdvancedTailnetSettings { - Picker("Authentication", selection: $draft.authMode) { - ForEach(availableTailnetAuthModes) { mode in - Text(mode.title).tag(mode) - } + Picker("Authentication", selection: $draft.authMode) { + ForEach(availableTailnetAuthModes) { mode in + Text(mode.title).tag(mode) } - .pickerStyle(.menu) } + .pickerStyle(.menu) if draft.authMode == .web { Button { @@ -551,7 +560,7 @@ private struct ConfigurationSheetView: View { } } .buttonStyle(.borderless) - .disabled(isStartingTailnetLogin || tailnetLoginActionDisabled) + .disabled(isStartingTailnetLogin || normalizedOptional(draft.authority) == nil) .accessibilityIdentifier("tailnet-start-sign-in") if let tailnetLoginStatus { @@ -607,14 +616,32 @@ private struct ConfigurationSheetView: View { } if sheet == .tailnet { - labeledValue("Server", tailnetServerDisplayLabel) - if let connectionSummary = tailnetConnectionSummary { - Text(connectionSummary) + if let authorityProbeStatus { + Text(authorityProbeStatus.summary) .font(.footnote.weight(.medium)) - .foregroundStyle(tailnetConnectionSummaryColor) + .foregroundStyle(.primary) + if let detail = authorityProbeStatus.detail { + Text(detail) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(3) + } + } else if let authorityProbeError { + Text("Connection failed") + .font(.footnote.weight(.medium)) + .foregroundStyle(.red) + Text(authorityProbeError) + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(3) } - if tailnetLoginStatus?.running == true { - HStack(spacing: 8) { + } + + if sheet == .tailnet { + HStack(spacing: 8) { + summaryBadge(isManagedTailnetAuthority ? "Managed" : "Custom") + summaryBadge(draft.authMode.title) + if tailnetLoginStatus?.running == true { summaryBadge("Signed In") } } @@ -627,44 +654,6 @@ private struct ConfigurationSheetView: View { ) } - private var tailnetServerCard: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .top, spacing: 12) { - VStack(alignment: .leading, spacing: 4) { - Text(usesCustomTailnetAuthority ? "Custom Server" : "Server") - .font(.subheadline.weight(.medium)) - Text(tailnetServerDisplayLabel) - .font(.footnote.monospaced()) - .foregroundStyle(.secondary) - .textSelection(.enabled) - } - - Spacer() - - if isDiscoveringTailnet || isProbingAuthority { - ProgressView() - .controlSize(.small) - } else if let summary = tailnetConnectionSummary { - Text(summary) - .font(.caption.weight(.medium)) - .foregroundStyle(tailnetConnectionSummaryColor) - } - } - - if let detail = tailnetServerDetail { - Text(detail) - .font(.footnote) - .foregroundStyle(.secondary) - } - } - .padding(12) - .background( - RoundedRectangle(cornerRadius: 16) - .fill(.thinMaterial) - ) - .accessibilityIdentifier("tailnet-server-card") - } - private func tailnetAuthorityProbeCard( status: TailnetAuthorityProbeStatus?, failure: String? @@ -838,15 +827,11 @@ private struct ConfigurationSheetView: View { } case .tailnet: - Button(usesCustomTailnetAuthority ? "Use Automatic Server" : "Edit Custom Server") { - toggleTailnetAuthorityMode() + Button("Use Tailscale Managed Server") { + applyTailnetDefaults(for: .tailscale) } - Button(showsAdvancedTailnetSettings ? "Hide Advanced Settings" : "Show Advanced Settings") { - showsAdvancedTailnetSettings.toggle() - } - - if showsAdvancedTailnetSettings, availableTailnetAuthModes.count > 1 { + if availableTailnetAuthModes.count > 1 { Menu("Authentication") { ForEach(availableTailnetAuthModes) { mode in Button(mode.title) { @@ -859,10 +844,9 @@ private struct ConfigurationSheetView: View { } } - Button("Refresh Server Lookup") { - scheduleTailnetDiscovery(immediate: true) + Button("Clear Discovery Result") { + resetTailnetDiscoveryFeedback() } - .disabled(usesCustomTailnetAuthority || normalizedOptional(draft.discoveryEmail) == nil) } } @@ -901,21 +885,12 @@ private struct ConfigurationSheetView: View { private var showsBottomActionButton: Bool { #if os(iOS) - return true + true #else - return false + false #endif } - private var showsIdentitySection: Bool { - switch sheet { - case .wireGuard, .tor: - return true - case .tailnet: - return showsAdvancedTailnetSettings - } - } - private var wireGuardEditorHeight: CGFloat { #if os(iOS) 180 @@ -935,18 +910,6 @@ private struct ConfigurationSheetView: View { } } - private var tailnetLoginActionDisabled: Bool { - switch sheet { - case .tailnet: - if usesCustomTailnetAuthority { - return normalizedOptional(draft.authority) == nil - } - return false - case .wireGuard, .tor: - return true - } - } - private var submissionDisabled: Bool { switch sheet { case .wireGuard: @@ -970,50 +933,6 @@ private struct ConfigurationSheetView: View { } } - private var tailnetServerDisplayLabel: String { - if usesCustomTailnetAuthority { - return normalizedOptional(draft.authority) - ?? "Enter a custom Tailnet server" - } - return TailnetProvider.tailscale.defaultAuthority ?? "Tailscale managed" - } - - private var tailnetServerDetail: String? { - if usesCustomTailnetAuthority { - if let discovery = discoveryStatus { - return "Discovered from \(discovery.domain)." - } - if let discoveryError { - return discoveryError - } - return "Use a custom Tailnet authority when your domain does not advertise one." - } - return "Continue with Tailscale, or open advanced settings to use a custom server." - } - - private var tailnetConnectionSummary: String? { - if isDiscoveringTailnet { - return "Finding server" - } - if isProbingAuthority { - return "Checking" - } - if let authorityProbeStatus { - return authorityProbeStatus.summary - } - if authorityProbeError != nil { - return "Unavailable" - } - return nil - } - - private var tailnetConnectionSummaryColor: Color { - if authorityProbeError != nil { - return .red - } - return .secondary - } - private func submit() { isSubmitting = true errorMessage = nil @@ -1102,7 +1021,7 @@ private struct ConfigurationSheetView: View { guard !didRunAutomation, sheet == .tailnet, let automation = BurrowAutomationConfig.current, - automation.action == .tailnetLogin || automation.action == .tailnetProbe + automation.action == .tailnetLogin || automation.action == .headscaleProbe else { return } @@ -1118,9 +1037,7 @@ private struct ConfigurationSheetView: View { case .tailnetLogin: applyTailnetDefaults(for: .tailscale) startTailnetLogin() - case .tailnetProbe: - usesCustomTailnetAuthority = true - showsAdvancedTailnetSettings = true + case .headscaleProbe: draft.authority = automation.authority ?? TailnetProvider.headscale.defaultAuthority ?? draft.authority probeTailnetAuthority() } @@ -1143,13 +1060,10 @@ private struct ConfigurationSheetView: View { ) var noteParts: [String] = [ - "Server: \(hostnameFallback(from: payload.authority ?? "", fallback: "tailnet"))", + isManagedTailnetAuthority ? "Managed Tailnet" : "Custom Tailnet", + "Auth: \(draft.authMode.title)", ] - if showsAdvancedTailnetSettings || draft.authMode != .web { - noteParts.append("Auth: \(draft.authMode.title)") - } - if draft.authMode == .web, tailnetLoginStatus?.running == true { noteParts.append("Browser sign-in complete") } @@ -1205,7 +1119,6 @@ private struct ConfigurationSheetView: View { private func applyTailnetDefaults(for provider: TailnetProvider) { resetTailnetDiscoveryFeedback() - usesCustomTailnetAuthority = provider != .tailscale draft.authority = provider.defaultAuthority ?? "" if !availableTailnetAuthModes.contains(draft.authMode) { draft.authMode = .web @@ -1213,6 +1126,12 @@ private struct ConfigurationSheetView: View { } private func startTailnetLogin() { + guard let authority = normalizedOptional(draft.authority) else { + tailnetLoginStatus = nil + tailnetLoginError = "Enter a server URL first." + return + } + isStartingTailnetLogin = true tailnetLoginError = nil preserveTailnetLoginSession = false @@ -1220,7 +1139,6 @@ private struct ConfigurationSheetView: View { Task { @MainActor in defer { isStartingTailnetLogin = false } do { - let authority = try await resolveTailnetAuthorityForLogin() let status = try await networkViewModel.startTailnetLogin( accountName: normalized(draft.accountName, fallback: "default"), identityName: normalized(draft.identityName, fallback: "apple"), @@ -1258,14 +1176,12 @@ private struct ConfigurationSheetView: View { } private func resetAuthorityProbe() { - tailnetProbeTask?.cancel() authorityProbeStatus = nil authorityProbeError = nil tailnetLoginError = nil } private func resetTailnetDiscoveryFeedback() { - tailnetDiscoveryTask?.cancel() discoveryStatus = nil discoveryError = nil } @@ -1294,83 +1210,6 @@ private struct ConfigurationSheetView: View { } } - private func scheduleTailnetDiscovery(immediate: Bool = false) { - guard sheet == .tailnet else { return } - tailnetDiscoveryTask?.cancel() - - guard !usesCustomTailnetAuthority else { - discoveryStatus = nil - discoveryError = nil - return - } - - guard normalizedOptional(draft.discoveryEmail) != nil else { - discoveryStatus = nil - discoveryError = nil - draft.authority = TailnetProvider.tailscale.defaultAuthority ?? "" - return - } - - tailnetDiscoveryTask = Task { @MainActor in - if !immediate { - try? await Task.sleep(for: .milliseconds(450)) - } - guard !Task.isCancelled else { return } - discoverTailnetAuthority() - } - } - - private func scheduleTailnetAuthorityProbe() { - guard sheet == .tailnet else { return } - tailnetProbeTask?.cancel() - guard normalizedOptional(draft.authority) != nil else { return } - - tailnetProbeTask = Task { @MainActor in - try? await Task.sleep(for: .milliseconds(300)) - guard !Task.isCancelled else { return } - probeTailnetAuthority() - } - } - - private func toggleTailnetAuthorityMode() { - let discoveredAuthority = discoveryStatus?.authority - usesCustomTailnetAuthority.toggle() - resetTailnetDiscoveryFeedback() - resetAuthorityProbe() - if usesCustomTailnetAuthority { - draft.authority = discoveredAuthority ?? draft.authority - } else { - draft.authority = TailnetProvider.tailscale.defaultAuthority ?? "" - scheduleTailnetDiscovery(immediate: normalizedOptional(draft.discoveryEmail) != nil) - } - } - - private func resolveTailnetAuthorityForLogin() async throws -> String { - if !usesCustomTailnetAuthority { - let authority = TailnetProvider.tailscale.defaultAuthority ?? "" - draft.authority = authority - scheduleTailnetAuthorityProbe() - return authority - } - - if let authority = normalizedOptional(draft.authority) { - return authority - } - - if let email = normalizedOptional(draft.discoveryEmail) { - let discovery = try await networkViewModel.discoverTailnet(email: email) - discoveryStatus = discovery - discoveryError = nil - draft.authority = discovery.authority - scheduleTailnetAuthorityProbe() - return discovery.authority - } - - throw NSError(domain: "BurrowTailnet", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Enter an email address or a custom server URL first." - ]) - } - private func beginTailnetLoginPolling(sessionID: String) { tailnetLoginPollTask?.cancel() tailnetLoginPollTask = Task { @MainActor in @@ -1497,16 +1336,13 @@ private struct ConfigurationSheetView: View { if tailnetLoginSessionID != nil { return "Resume Sign-In" } - return "Continue with Tailscale" + return "Start Sign-In" } private var tailnetAuthenticationFootnote: String { switch draft.authMode { case .web: - if usesCustomTailnetAuthority { - return "Burrow signs in through the daemon using your custom Tailnet server." - } - return "Burrow signs in through the daemon using Tailscale's managed browser flow." + return "Burrow asks the daemon to start a Tailnet browser sign-in session, then closes it locally once the daemon reports the device is running." case .none: return "Save the authority only. Useful when the control plane handles authentication elsewhere." case .password, .preauthKey: @@ -1521,6 +1357,10 @@ private struct ConfigurationSheetView: View { ) } + private var isManagedTailnetAuthority: Bool { + TailnetProvider.isManagedTailscaleAuthority(normalizedOptional(draft.authority)) + } + @ViewBuilder private func labeledValue(_ label: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: 2) { @@ -1543,7 +1383,12 @@ private struct AccountRowView: View { VStack(alignment: .leading, spacing: 4) { Text(account.title) .font(.headline) - Text(account.kind.title) + HStack(spacing: 8) { + Text(account.kind.title) + if let provider = account.provider { + Text(provider.title) + } + } .font(.subheadline) .foregroundStyle(account.kind.accentColor) } @@ -1625,12 +1470,6 @@ private extension View { @MainActor private final class TailnetBrowserAuthenticator: NSObject { private var session: ASWebAuthenticationSession? - private static var prefersEphemeralSessionForCurrentProcess: Bool { - let rawValue = ProcessInfo.processInfo.environment["BURROW_UI_TEST_EPHEMERAL_AUTH"]? - .trimmingCharacters(in: .whitespacesAndNewlines) - .lowercased() - return rawValue == "1" || rawValue == "true" || rawValue == "yes" - } func start(url: URL, onDismiss: @escaping @Sendable () -> Void) { cancel() @@ -1638,7 +1477,7 @@ private final class TailnetBrowserAuthenticator: NSObject { onDismiss() } session.presentationContextProvider = self - session.prefersEphemeralWebBrowserSession = Self.prefersEphemeralSessionForCurrentProcess + session.prefersEphemeralWebBrowserSession = false self.session = session _ = session.start() } @@ -1677,7 +1516,7 @@ private final class TailnetBrowserAuthenticator { private struct BurrowAutomationConfig { enum Action: String { case tailnetLogin = "tailnet-login" - case tailnetProbe = "tailnet-probe" + case headscaleProbe = "headscale-probe" } let action: Action diff --git a/Apple/UI/Networks/Network.swift b/Apple/UI/Networks/Network.swift index 35bd0e1..32f0b8c 100644 --- a/Apple/UI/Networks/Network.swift +++ b/Apple/UI/Networks/Network.swift @@ -303,7 +303,7 @@ enum TailnetProvider: String, CaseIterable, Codable, Identifiable, Sendable { var title: String { switch self { case .tailscale: "Tailscale" - case .headscale: "Custom Tailnet" + case .headscale: "Headscale" case .burrow: "Burrow" } } @@ -375,7 +375,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable { switch self { case .wireGuard: "Import a tunnel and optional account metadata." case .tor: "Store Arti account and identity preferences." - case .tailnet: "Save Tailnet authority, identity defaults, and login material." + case .tailnet: "Save Tailnet authority, identity, and login material." } } @@ -402,7 +402,7 @@ enum AccountNetworkKind: String, CaseIterable, Codable, Identifiable, Sendable { case .tor: "Tor account preferences are stored on Apple now. The managed Tor runtime is not wired on Apple in this branch yet." case .tailnet: - "Tailnet accounts can sign in from Apple now. The managed Apple runtime is still pending, but Tailnet networks can already be stored in the daemon." + "Tailnet accounts can sign in from Apple now. The managed Apple runtime is still pending, but Tailnet networks can be stored in the daemon." } } } diff --git a/Scripts/check-forge-host.sh b/Scripts/check-forge-host.sh index 0f79bf4..f4d646d 100755 --- a/Scripts/check-forge-host.sh +++ b/Scripts/check-forge-host.sh @@ -164,14 +164,6 @@ if [[ "${EXPECT_TAILNET}" == "1" ]]; then test -s /run/agenix/burrowHeadscaleOidcClientSecret fi -if [[ "${EXPECT_NSC}" == "1" ]]; then - echo "== agenix-nsc ==" - ls -l /run/agenix || true - test -s /run/agenix/burrowForgejoNscToken - test -s /run/agenix/burrowForgejoNscDispatcherConfig - test -s /run/agenix/burrowForgejoNscAutoscalerConfig -fi - if command -v curl >/dev/null 2>&1; then echo "== http-local ==" curl -fsS -o /dev/null -w 'forgejo_login %{http_code}\n' http://127.0.0.1:3000/user/login diff --git a/Scripts/run-ios-tailnet-ui-tests.sh b/Scripts/run-ios-tailnet-ui-tests.sh index 5170a1e..5086bd1 100755 --- a/Scripts/run-ios-tailnet-ui-tests.sh +++ b/Scripts/run-ios-tailnet-ui-tests.sh @@ -5,18 +5,13 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" bundle_id="${BURROW_UI_TEST_APP_BUNDLE_ID:-com.hackclub.burrow}" simulator_name="${BURROW_UI_TEST_SIMULATOR_NAME:-iPhone 17 Pro}" simulator_os="${BURROW_UI_TEST_SIMULATOR_OS:-26.4}" -simulator_id="${BURROW_UI_TEST_SIMULATOR_ID:-}" derived_data_path="${BURROW_UI_TEST_DERIVED_DATA_PATH:-/tmp/burrow-ui-tests-deriveddata}" source_packages_path="${BURROW_UI_TEST_SOURCE_PACKAGES_PATH:-/tmp/burrow-ui-tests-sourcepackages}" -fallback_dir="/tmp/${bundle_id}/SimulatorFallback" +fallback_dir="${HOME}/Library/Application Support/${bundle_id}/SimulatorFallback" socket_path="${fallback_dir}/burrow.sock" -tailnet_state_root="/tmp/${bundle_id}/SimulatorTailnetState" daemon_log="${BURROW_UI_TEST_DAEMON_LOG:-/tmp/burrow-ui-test-daemon.log}" -ui_test_config_path="${BURROW_UI_TEST_CONFIG_PATH:-/tmp/burrow-ui-test-config.json}" -ui_test_runner_bundle_id="${bundle_id}.uitests.xctrunner" ui_test_email="${BURROW_UI_TEST_EMAIL:-ui-test@burrow.net}" ui_test_username="${BURROW_UI_TEST_USERNAME:-ui-test}" -ui_test_tailnet_mode="${BURROW_UI_TEST_TAILNET_MODE:-tailscale}" password_secret="${repo_root}/secrets/infra/authentik-ui-test-password.age" age_identity="${BURROW_UI_TEST_AGE_IDENTITY:-${HOME}/.ssh/id_ed25519}" @@ -30,60 +25,10 @@ if [[ -z "$ui_test_password" ]]; then fi fi -rm -rf "$fallback_dir" "$tailnet_state_root" -mkdir -p "$fallback_dir" "$tailnet_state_root" "$derived_data_path" "$source_packages_path" +mkdir -p "$fallback_dir" "$derived_data_path" "$source_packages_path" rm -f "$socket_path" -resolve_simulator_id() { - xcrun simctl list devices available -j | python3 -c ' -import json -import os -import sys - -target_name = sys.argv[1] -target_os = sys.argv[2] -target_runtime = "com.apple.CoreSimulator.SimRuntime.iOS-" + target_os.replace(".", "-") -devices = json.load(sys.stdin).get("devices", {}) -healthy = [] -for runtime, entries in devices.items(): - if runtime != target_runtime: - continue - for entry in entries: - if not entry.get("isAvailable", False): - continue - if not os.path.isdir(entry.get("dataPath", "")): - continue - healthy.append(entry) -for entry in healthy: - if entry.get("name") == target_name: - print(entry["udid"]) - raise SystemExit(0) -for entry in healthy: - if target_name in entry.get("name", ""): - print(entry["udid"]) - raise SystemExit(0) -raise SystemExit(1) -' "$simulator_name" "$simulator_os" -} - -if [[ -z "$simulator_id" ]]; then - simulator_id="$(resolve_simulator_id || true)" -fi - -if [[ -n "$simulator_id" ]]; then - xcrun simctl boot "$simulator_id" >/dev/null 2>&1 || true - xcrun simctl bootstatus "$simulator_id" -b - xcrun simctl terminate "$simulator_id" "$bundle_id" >/dev/null 2>&1 || true - xcrun simctl terminate "$simulator_id" "$ui_test_runner_bundle_id" >/dev/null 2>&1 || true - xcrun simctl uninstall "$simulator_id" "$bundle_id" >/dev/null 2>&1 || true - xcrun simctl uninstall "$simulator_id" "$ui_test_runner_bundle_id" >/dev/null 2>&1 || true - destination="id=${simulator_id}" -else - destination="platform=iOS Simulator,name=${simulator_name},OS=${simulator_os}" -fi - cleanup() { - rm -f "$ui_test_config_path" if [[ -n "${daemon_pid:-}" ]]; then kill "$daemon_pid" >/dev/null 2>&1 || true wait "$daemon_pid" >/dev/null 2>&1 || true @@ -91,33 +36,11 @@ cleanup() { } trap cleanup EXIT -umask 077 -python3 - <<'PY' "$ui_test_config_path" "$ui_test_email" "$ui_test_username" "$ui_test_password" "$ui_test_tailnet_mode" -import json -import pathlib -import sys - -config_path = pathlib.Path(sys.argv[1]) -config_path.write_text( - json.dumps( - { - "email": sys.argv[2], - "username": sys.argv[3], - "password": sys.argv[4], - "mode": sys.argv[5], - } - ), - encoding="utf-8", -) -PY - cargo build -p burrow --bin burrow ( cd "$fallback_dir" - RUST_LOG="${BURROW_UI_TEST_RUST_LOG:-info,burrow=debug}" \ BURROW_SOCKET_PATH="burrow.sock" \ - BURROW_TAILSCALE_STATE_ROOT="$tailnet_state_root" \ "${repo_root}/target/debug/burrow" daemon >"$daemon_log" 2>&1 ) & daemon_pid=$! @@ -133,31 +56,18 @@ if [[ ! -S "$socket_path" ]]; then exit 1 fi -common_xcodebuild_args=( - -quiet - -skipPackagePluginValidation - -project "${repo_root}/Apple/Burrow.xcodeproj" - -scheme App - -configuration Debug - -destination "$destination" - -derivedDataPath "$derived_data_path" - -clonedSourcePackagesDirPath "$source_packages_path" - -only-testing:BurrowUITests - -parallel-testing-enabled NO - -maximum-concurrent-test-simulator-destinations 1 - -maximum-parallel-testing-workers 1 - CODE_SIGNING_ALLOWED=NO -) - -xcodebuild \ - "${common_xcodebuild_args[@]}" \ - build-for-testing - BURROW_UI_TEST_EMAIL="$ui_test_email" \ BURROW_UI_TEST_USERNAME="$ui_test_username" \ BURROW_UI_TEST_PASSWORD="$ui_test_password" \ -BURROW_UI_TEST_CONFIG_PATH="$ui_test_config_path" \ -BURROW_UI_TEST_EPHEMERAL_AUTH=1 \ xcodebuild \ - "${common_xcodebuild_args[@]}" \ - test-without-building + -quiet \ + -skipPackagePluginValidation \ + -project "${repo_root}/Apple/Burrow.xcodeproj" \ + -scheme App \ + -configuration Debug \ + -destination "platform=iOS Simulator,name=${simulator_name},OS=${simulator_os}" \ + -derivedDataPath "$derived_data_path" \ + -clonedSourcePackagesDirPath "$source_packages_path" \ + -only-testing:BurrowUITests \ + CODE_SIGNING_ALLOWED=NO \ + test diff --git a/Scripts/seal-forgejo-nsc-secrets.sh b/Scripts/seal-forgejo-nsc-secrets.sh deleted file mode 100755 index a6b3918..0000000 --- a/Scripts/seal-forgejo-nsc-secrets.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" - -usage() { - cat <<'EOF' -Usage: Scripts/seal-forgejo-nsc-secrets.sh [options] - -Encrypt Burrow forgejo-nsc runtime inputs from intake/ into the agenix secrets -consumed by burrow-forge. - -Options: - --provision Re-render the local intake files before sealing. - --host SSH target forwarded to provision-forgejo-nsc.sh. - --ssh-key SSH private key forwarded to provision-forgejo-nsc.sh. - --nsc-bin Override the nsc binary for provisioning. - -h, --help Show this help text. -EOF -} - -PROVISION=0 -HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" -SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" -NSC_BIN="${NSC_BIN:-}" - -while [[ $# -gt 0 ]]; do - case "$1" in - --provision) - PROVISION=1 - shift - ;; - --host) - HOST="${2:?missing value for --host}" - shift 2 - ;; - --ssh-key) - SSH_KEY="${2:?missing value for --ssh-key}" - shift 2 - ;; - --nsc-bin) - NSC_BIN="${2:?missing value for --nsc-bin}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "unknown option: $1" >&2 - usage >&2 - exit 64 - ;; - esac -done - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - exit 1 - fi -} - -require_cmd age -require_cmd nix -require_cmd python3 - -if [[ "${PROVISION}" -eq 1 ]]; then - provision_args=(--host "${HOST}" --ssh-key "${SSH_KEY}") - if [[ -n "${NSC_BIN}" ]]; then - provision_args+=(--nsc-bin "${NSC_BIN}") - fi - "${SCRIPT_DIR}/provision-forgejo-nsc.sh" "${provision_args[@]}" -fi - -tmpdir="$(mktemp -d)" -cleanup() { - rm -rf "${tmpdir}" -} -trap cleanup EXIT - -seal_secret() { - local target="$1" - local source_path="$2" - recipients_file="${tmpdir}/$(basename "${target}").recipients" - if [[ ! -s "${source_path}" ]]; then - echo "required runtime input missing or empty: ${source_path}" >&2 - exit 1 - fi - nix eval --impure --json --expr "let s = import ${REPO_ROOT}/secrets.nix; in s.\"${target}\".publicKeys" \ - | python3 -c 'import json, sys; [print(item) for item in json.load(sys.stdin)]' \ - > "${recipients_file}" - - age -R "${recipients_file}" -o "${REPO_ROOT}/${target}" "${source_path}" -} - -seal_secret "secrets/infra/forgejo-nsc-token.age" "${REPO_ROOT}/intake/forgejo_nsc_token.txt" -seal_secret "secrets/infra/forgejo-nsc-dispatcher-config.age" "${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" -seal_secret "secrets/infra/forgejo-nsc-autoscaler-config.age" "${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml" - -chmod 600 \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-token.age" \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-dispatcher-config.age" \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-autoscaler-config.age" - -echo "Sealed forgejo-nsc runtime inputs into:" -printf ' %s\n' \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-token.age" \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-dispatcher-config.age" \ - "${REPO_ROOT}/secrets/infra/forgejo-nsc-autoscaler-config.age" -echo "Deploy burrow-forge to apply the new CI credentials." diff --git a/Scripts/sync-forgejo-nsc-config.sh b/Scripts/sync-forgejo-nsc-config.sh index 2ce7114..77581f8 100755 --- a/Scripts/sync-forgejo-nsc-config.sh +++ b/Scripts/sync-forgejo-nsc-config.sh @@ -1,7 +1,132 @@ #!/usr/bin/env bash set -euo pipefail -echo "Scripts/sync-forgejo-nsc-config.sh is obsolete." >&2 -echo "Burrow forgejo-nsc now consumes agenix-backed secrets instead of host-local intake files." >&2 -echo "Use Scripts/seal-forgejo-nsc-secrets.sh and deploy burrow-forge." >&2 -exit 1 +usage() { + cat <<'EOF' +Usage: Scripts/sync-forgejo-nsc-config.sh [options] + +Copy Burrow forgejo-nsc runtime inputs from intake/ onto the forge host and +restart the dispatcher/autoscaler units. + +Options: + --host SSH target (default: root@git.burrow.net) + --ssh-key SSH private key (default: intake/agent_at_burrow_net_ed25519) + --rotate-pat Re-render the intake files before syncing. + --no-restart Copy files only. + -h, --help Show this help text. +EOF +} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +HOST="${BURROW_FORGE_HOST:-root@git.burrow.net}" +SSH_KEY="${BURROW_FORGE_SSH_KEY:-${REPO_ROOT}/intake/agent_at_burrow_net_ed25519}" +KNOWN_HOSTS_FILE="${BURROW_FORGE_KNOWN_HOSTS_FILE:-${HOME}/.cache/burrow/forge-known_hosts}" +ROTATE_PAT=0 +NO_RESTART=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --host) + HOST="${2:?missing value for --host}" + shift 2 + ;; + --ssh-key) + SSH_KEY="${2:?missing value for --ssh-key}" + shift 2 + ;; + --rotate-pat) + ROTATE_PAT=1 + shift + ;; + --no-restart) + NO_RESTART=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown option: $1" >&2 + usage >&2 + exit 64 + ;; + esac +done + +mkdir -p "$(dirname "${KNOWN_HOSTS_FILE}")" + +burrow_require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command: $1" >&2 + exit 1 + fi +} + +burrow_require_cmd ssh +burrow_require_cmd scp + +if [[ ! -f "${SSH_KEY}" ]]; then + echo "forge SSH key not found: ${SSH_KEY}" >&2 + exit 1 +fi + +if [[ "${ROTATE_PAT}" -eq 1 ]]; then + "${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}" +fi + +token_file="${REPO_ROOT}/intake/forgejo_nsc_token.txt" +dispatcher_file="${REPO_ROOT}/intake/forgejo_nsc_dispatcher.yaml" +autoscaler_file="${REPO_ROOT}/intake/forgejo_nsc_autoscaler.yaml" + +for path in "${token_file}" "${dispatcher_file}" "${autoscaler_file}"; do + if [[ ! -s "${path}" ]]; then + echo "required runtime input missing or empty: ${path}" >&2 + exit 1 + fi +done + +ssh_opts=( + -i "${SSH_KEY}" + -o IdentitiesOnly=yes + -o UserKnownHostsFile="${KNOWN_HOSTS_FILE}" + -o StrictHostKeyChecking=accept-new +) + +remote_tmp="$(ssh "${ssh_opts[@]}" "${HOST}" "mktemp -d")" +cleanup() { + if [[ -n "${remote_tmp:-}" ]]; then + ssh "${ssh_opts[@]}" "${HOST}" "rm -rf '${remote_tmp}'" >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +scp "${ssh_opts[@]}" \ + "${token_file}" \ + "${dispatcher_file}" \ + "${autoscaler_file}" \ + "${HOST}:${remote_tmp}/" + +ssh "${ssh_opts[@]}" "${HOST}" " + set -euo pipefail + install -d -m 0755 /var/lib/burrow/intake + install -m 0400 -o forgejo-nsc -g forgejo-nsc '${remote_tmp}/$(basename "${token_file}")' /var/lib/burrow/intake/forgejo_nsc_token.txt + install -m 0400 -o forgejo-nsc -g forgejo-nsc '${remote_tmp}/$(basename "${dispatcher_file}")' /var/lib/burrow/intake/forgejo_nsc_dispatcher.yaml + install -m 0400 -o forgejo-nsc -g forgejo-nsc '${remote_tmp}/$(basename "${autoscaler_file}")' /var/lib/burrow/intake/forgejo_nsc_autoscaler.yaml +" + +if [[ "${NO_RESTART}" -eq 0 ]]; then + ssh "${ssh_opts[@]}" "${HOST}" " + set -euo pipefail + systemctl restart forgejo-nsc-dispatcher.service forgejo-nsc-autoscaler.service + systemctl is-active forgejo-nsc-dispatcher.service forgejo-nsc-autoscaler.service + ls -l \ + /var/lib/burrow/intake/forgejo_nsc_token.txt \ + /var/lib/burrow/intake/forgejo_nsc_dispatcher.yaml \ + /var/lib/burrow/intake/forgejo_nsc_autoscaler.yaml + " +fi + +echo "forgejo-nsc runtime sync complete (host=${HOST}, restarted=$((1 - NO_RESTART)))." diff --git a/burrow/src/auth/server/tailscale.rs b/burrow/src/auth/server/tailscale.rs index d08c807..55516e1 100644 --- a/burrow/src/auth/server/tailscale.rs +++ b/burrow/src/auth/server/tailscale.rs @@ -26,8 +26,6 @@ pub struct TailscaleLoginStartRequest { pub hostname: Option, #[serde(default)] pub control_url: Option, - #[serde(default)] - pub packet_socket: Option, } #[derive(Clone, Debug, Serialize, Deserialize, Default)] @@ -57,35 +55,23 @@ pub struct TailscaleLoginStartResponse { pub status: TailscaleLoginStatus, } -pub struct TailscaleLoginSession { - pub session_id: String, - pub helper: Arc, - pub status: TailscaleLoginStatus, -} - #[derive(Clone, Default)] pub struct TailscaleBridgeManager { client: Client, sessions: Arc>>>, } -pub struct TailscaleHelperProcess { +struct ManagedSession { session_id: String, listen_url: String, - packet_socket: Option, - control_url: Option, state_dir: PathBuf, child: Arc>, _stderr_task: JoinHandle<()>, } -type ManagedSession = TailscaleHelperProcess; - #[derive(Debug, Deserialize)] struct HelperHello { listen_addr: String, - #[serde(default)] - packet_socket: Option, } impl TailscaleBridgeManager { @@ -93,71 +79,76 @@ impl TailscaleBridgeManager { &self, request: TailscaleLoginStartRequest, ) -> Result { - let session = self.ensure_session(request).await?; - Ok(TailscaleLoginStartResponse { - session_id: session.session_id, - status: session.status, - }) - } - - pub async fn ensure_session( - &self, - request: TailscaleLoginStartRequest, - ) -> Result { - let key = session_key_for_request(&request); - let requested_packet_socket = request - .packet_socket - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()); - let requested_control_url = request - .control_url - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()); + let key = session_key(&request.account_name, &request.identity_name); if let Some(existing) = self.sessions.lock().await.get(&key).cloned() { - let needs_restart_for_socket = match (requested_packet_socket, existing.packet_socket()) - { - (Some(requested), Some(current)) => current != Path::new(requested), - (Some(_), None) => true, - _ => false, - }; - let needs_restart_for_control_url = - requested_control_url != existing.control_url().map(|value| value.trim()); - - if !needs_restart_for_socket && !needs_restart_for_control_url { - match self.fetch_status(existing.as_ref()).await { - Ok(status) => { - return Ok(TailscaleLoginSession { - session_id: existing.session_id.clone(), - helper: existing, - status, - }); - } - Err(err) => { - log::warn!( - "tailscale login session {} is stale, restarting: {err}", - existing.session_id - ); - } + match self.fetch_status(existing.as_ref()).await { + Ok(status) => { + return Ok(TailscaleLoginStartResponse { + session_id: existing.session_id.clone(), + status, + }); + } + Err(err) => { + log::warn!( + "tailscale login session {} is stale, restarting: {err}", + existing.session_id + ); + self.sessions.lock().await.remove(&key); + let _ = self.shutdown_session(existing.as_ref()).await; } - } else { - log::info!( - "tailscale login session {} no longer matches requested transport, restarting", - existing.session_id - ); } - - self.sessions.lock().await.remove(&key); - let _ = self.shutdown_session(existing.as_ref()).await; } - let session = Arc::new(spawn_tailscale_helper(&request).await?); + let state_dir = state_root().join(session_dir_name(&request)); + tokio::fs::create_dir_all(&state_dir) + .await + .with_context(|| format!("failed to create {}", state_dir.display()))?; + + let mut child = helper_command(&request, &state_dir)? + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("failed to spawn tailscale login helper")?; + + let stdout = child + .stdout + .take() + .context("tailscale helper stdout unavailable")?; + let stderr = child + .stderr + .take() + .context("tailscale helper stderr unavailable")?; + + let hello_line = tokio::time::timeout(Duration::from_secs(20), async move { + let mut lines = BufReader::new(stdout).lines(); + lines.next_line().await + }) + .await + .context("timed out waiting for tailscale helper startup")?? + .context("tailscale helper exited before reporting listen address")?; + + let hello: HelperHello = + serde_json::from_str(&hello_line).context("invalid tailscale helper startup line")?; + + let stderr_task = tokio::spawn(async move { + let mut lines = BufReader::new(stderr).lines(); + while let Ok(Some(line)) = lines.next_line().await { + log::info!("tailscale-login-bridge: {line}"); + } + }); + + let session = Arc::new(ManagedSession { + session_id: random_session_id(), + listen_url: format!("http://{}", hello.listen_addr), + state_dir, + child: Arc::new(Mutex::new(child)), + _stderr_task: stderr_task, + }); + let status = self.wait_for_status(session.as_ref()).await?; - let response = TailscaleLoginSession { + let response = TailscaleLoginStartResponse { session_id: session.session_id.clone(), - helper: session.clone(), status, }; @@ -201,7 +192,7 @@ impl TailscaleBridgeManager { let mut last_error = None; let mut last_status = None; for _ in 0..40 { - match session.status_with_client(&self.client).await { + match self.fetch_status(session).await { Ok(status) if status.running || status.auth_url.is_some() => return Ok(status), Ok(status) => last_status = Some(status), Err(err) => last_error = Some(err), @@ -215,7 +206,28 @@ impl TailscaleBridgeManager { } async fn fetch_status(&self, session: &ManagedSession) -> Result { - session.status_with_client(&self.client).await + let mut child = session.child.lock().await; + if let Some(status) = child.try_wait()? { + return Err(anyhow!( + "tailscale helper exited with status {status} for {}", + session.state_dir.display() + )); + } + drop(child); + + let response = self + .client + .get(format!("{}/status", session.listen_url)) + .send() + .await + .context("failed to query tailscale helper status")? + .error_for_status() + .context("tailscale helper status request failed")?; + + response + .json::() + .await + .context("invalid tailscale helper status response") } async fn remove_session_by_id(&self, session_id: &str) -> Option> { @@ -227,74 +239,14 @@ impl TailscaleBridgeManager { } async fn shutdown_session(&self, session: &ManagedSession) -> Result<()> { - session.shutdown_with_client(&self.client).await - } -} - -impl TailscaleHelperProcess { - pub fn session_id(&self) -> &str { - &self.session_id - } - - pub fn packet_socket(&self) -> Option<&Path> { - self.packet_socket.as_deref() - } - - pub fn control_url(&self) -> Option<&str> { - self.control_url.as_deref() - } - - pub fn state_dir(&self) -> &Path { - &self.state_dir - } - - pub async fn status(&self) -> Result { - self.status_with_client(&Client::new()).await - } - - pub async fn shutdown(&self) -> Result<()> { - self.shutdown_with_client(&Client::new()).await - } - - async fn status_with_client(&self, client: &Client) -> Result { - let mut child = self.child.lock().await; - if let Some(status) = child.try_wait()? { - return Err(anyhow!( - "tailscale helper exited with status {status} for {}", - self.state_dir.display() - )); - } - drop(child); - - let response = client - .get(format!("{}/status", self.listen_url)) + let _ = self + .client + .post(format!("{}/shutdown", session.listen_url)) .send() - .await - .context("failed to query tailscale helper status")? - .error_for_status() - .context("tailscale helper status request failed")?; - - let status = response - .json::() - .await - .context("invalid tailscale helper status response")?; - - log::info!( - "tailscale helper status session={} backend_state={} running={} needs_login={} auth_url={:?}", - self.session_id, - status.backend_state, - status.running, - status.needs_login, - status.auth_url - ); - Ok(status) - } - - async fn shutdown_with_client(&self, client: &Client) -> Result<()> { - let _ = client.post(format!("{}/shutdown", self.listen_url)).send().await; + .await; for _ in 0..10 { - let mut child = self.child.lock().await; + let mut child = session.child.lock().await; if child.try_wait()?.is_some() { return Ok(()); } @@ -302,7 +254,7 @@ impl TailscaleHelperProcess { tokio::time::sleep(Duration::from_millis(100)).await; } - let mut child = self.child.lock().await; + let mut child = session.child.lock().await; child .start_kill() .context("failed to kill tailscale helper")?; @@ -311,58 +263,6 @@ impl TailscaleHelperProcess { } } -pub async fn spawn_tailscale_helper( - request: &TailscaleLoginStartRequest, -) -> Result { - let state_dir = state_root().join(session_dir_name(request)); - tokio::fs::create_dir_all(&state_dir) - .await - .with_context(|| format!("failed to create {}", state_dir.display()))?; - - let mut child = helper_command(request, &state_dir)? - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("failed to spawn tailscale login helper")?; - - let stdout = child - .stdout - .take() - .context("tailscale helper stdout unavailable")?; - let stderr = child - .stderr - .take() - .context("tailscale helper stderr unavailable")?; - - let hello_line = tokio::time::timeout(Duration::from_secs(20), async move { - let mut lines = BufReader::new(stdout).lines(); - lines.next_line().await - }) - .await - .context("timed out waiting for tailscale helper startup")?? - .context("tailscale helper exited before reporting listen address")?; - - let hello: HelperHello = - serde_json::from_str(&hello_line).context("invalid tailscale helper startup line")?; - - let stderr_task = tokio::spawn(async move { - let mut lines = BufReader::new(stderr).lines(); - while let Ok(Some(line)) = lines.next_line().await { - log::info!("tailscale-login-bridge: {line}"); - } - }); - - Ok(TailscaleHelperProcess { - session_id: random_session_id(), - listen_url: format!("http://{}", hello.listen_addr), - packet_socket: hello.packet_socket.map(PathBuf::from), - control_url: request.control_url.clone(), - state_dir, - child: Arc::new(Mutex::new(child)), - _stderr_task: stderr_task, - }) -} - fn helper_command(request: &TailscaleLoginStartRequest, state_dir: &Path) -> Result { let mut command = if let Ok(path) = env::var("BURROW_TAILSCALE_HELPER") { Command::new(path) @@ -391,21 +291,10 @@ fn helper_command(request: &TailscaleLoginStartRequest, state_dir: &Path) -> Res } } - if let Some(packet_socket) = request.packet_socket.as_deref() { - let trimmed = packet_socket.trim(); - if !trimmed.is_empty() { - command.arg("--packet-socket").arg(trimmed); - } - } - Ok(command) } -pub(crate) fn packet_socket_path(request: &TailscaleLoginStartRequest) -> PathBuf { - state_root().join(session_dir_name(request)).join("packet.sock") -} - -pub(crate) fn state_root() -> PathBuf { +fn state_root() -> PathBuf { if let Ok(path) = env::var("BURROW_TAILSCALE_STATE_ROOT") { return PathBuf::from(path); } @@ -426,34 +315,19 @@ pub(crate) fn state_root() -> PathBuf { .join("tailscale") } -pub(crate) fn session_dir_name(request: &TailscaleLoginStartRequest) -> String { +fn session_dir_name(request: &TailscaleLoginStartRequest) -> String { format!( - "{}-{}-{}", + "{}-{}", slug(&request.account_name), - slug(&request.identity_name), - slug(control_scope(request)) + slug(&request.identity_name) ) } -fn session_key_for_request(request: &TailscaleLoginStartRequest) -> String { - format!( - "{}:{}:{}", - request.account_name, - request.identity_name, - control_scope(request) - ) +fn session_key(account_name: &str, identity_name: &str) -> String { + format!("{account_name}:{identity_name}") } -fn control_scope(request: &TailscaleLoginStartRequest) -> &str { - request - .control_url - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .unwrap_or("tailscale-managed") -} - -pub(crate) fn default_hostname(request: &TailscaleLoginStartRequest) -> String { +fn default_hostname(request: &TailscaleLoginStartRequest) -> String { request .hostname .as_deref() @@ -496,24 +370,14 @@ mod tests { } #[test] - fn state_dir_is_scoped_by_account_identity_and_control_plane() { + fn state_dir_is_stable_by_account_and_identity() { let request = TailscaleLoginStartRequest { account_name: "default".to_owned(), identity_name: "apple".to_owned(), hostname: None, control_url: None, - packet_socket: None, }; - assert_eq!(session_dir_name(&request), "default-apple-tailscale-managed"); + assert_eq!(session_dir_name(&request), "default-apple"); assert_eq!(default_hostname(&request), "burrow-apple"); - - let custom_request = TailscaleLoginStartRequest { - control_url: Some("https://ts.burrow.net".to_owned()), - ..request - }; - assert_eq!( - session_dir_name(&custom_request), - "default-apple-httpstsburrownet" - ); } } diff --git a/burrow/src/control/discovery.rs b/burrow/src/control/discovery.rs index d044a62..5fc7add 100644 --- a/burrow/src/control/discovery.rs +++ b/burrow/src/control/discovery.rs @@ -1,7 +1,6 @@ use anyhow::{anyhow, Context, Result}; use reqwest::{Client, StatusCode, Url}; use serde::{Deserialize, Serialize}; -use tracing::{debug, info}; use super::TailnetProvider; @@ -44,7 +43,6 @@ struct WebFingerLink { pub async fn discover_tailnet(email: &str) -> Result { let domain = email_domain(email)?; - info!(%email, %domain, "tailnet discovery requested"); let base_url = Url::parse(&format!("https://{domain}")) .with_context(|| format!("invalid discovery domain {domain}"))?; let client = Client::builder() @@ -118,21 +116,12 @@ pub async fn discover_tailnet_at( base_url: &Url, ) -> Result { let domain = email_domain(email)?; - debug!(%email, %domain, base_url = %base_url, "starting tailnet domain discovery"); if let Some(discovery) = discover_well_known(client, base_url).await? { - info!( - %email, - %domain, - authority = %discovery.authority, - provider = ?discovery.provider, - "resolved tailnet discovery from well-known document" - ); return Ok(TailnetDiscovery { domain, ..discovery }); } if let Some(authority) = discover_webfinger(client, email, base_url).await? { - info!(%email, %domain, %authority, "resolved tailnet discovery from webfinger"); return Ok(TailnetDiscovery { domain, provider: inferred_provider(Some(&authority), None), @@ -173,7 +162,6 @@ async fn discover_well_known(client: &Client, base_url: &Url) -> Result Res url.query_pairs_mut() .append_pair("resource", &format!("acct:{email}")) .append_pair("rel", TAILNET_DISCOVERY_REL); - debug!(%email, url = %url, "requesting tailnet webfinger document"); let response = client .get(url) diff --git a/burrow/src/daemon/instance.rs b/burrow/src/daemon/instance.rs index 9b2e138..0a23ddc 100644 --- a/burrow/src/daemon/instance.rs +++ b/burrow/src/daemon/instance.rs @@ -8,7 +8,7 @@ use rusqlite::Connection; use tokio::sync::{mpsc, watch, RwLock}; use tokio_stream::wrappers::ReceiverStream; use tonic::{Request, Response, Status as RspStatus}; -use tracing::{debug, info, warn}; +use tracing::warn; use tun::tokio::TunInterface; use super::{ @@ -16,15 +16,15 @@ use super::{ networks_server::Networks, tailnet_control_server::TailnetControl, tunnel_server::Tunnel, Empty, Network, NetworkDeleteRequest, NetworkListResponse, NetworkReorderRequest, State as RPCTunnelState, TailnetDiscoverRequest, TailnetDiscoverResponse, - TailnetProbeRequest, TailnetProbeResponse, TunnelConfigurationResponse, TunnelPacket, + TailnetProbeRequest, TailnetProbeResponse, TunnelConfigurationResponse, TunnelStatusResponse, }, - runtime::{tailnet_helper_request, ActiveTunnel, ResolvedTunnel}, + runtime::{ActiveTunnel, ResolvedTunnel}, }; use crate::{ auth::server::tailscale::{ - packet_socket_path, TailscaleBridgeManager, - TailscaleLoginStartRequest as BridgeLoginStartRequest, TailscaleLoginStatus, + TailscaleBridgeManager, TailscaleLoginStartRequest as BridgeLoginStartRequest, + TailscaleLoginStatus, }, control::discovery, daemon::rpc::ServerConfig, @@ -87,20 +87,11 @@ impl DaemonRPCServer { } async fn current_tunnel_configuration(&self) -> Result { - let config = { - let active = self.active_tunnel.read().await; - active - .as_ref() - .map(|tunnel| tunnel.server_config().clone()) - }; - let config = match config { - Some(config) => config, - None => self - .resolve_tunnel() - .await? - .server_config() - .map_err(proc_err)?, - }; + let config = self + .resolve_tunnel() + .await? + .server_config() + .map_err(proc_err)?; Ok(configuration_rsp(config)) } @@ -120,18 +111,8 @@ impl DaemonRPCServer { async fn replace_active_tunnel(&self, desired: ResolvedTunnel) -> Result<(), RspStatus> { let _ = self.stop_active_tunnel().await?; - let tailnet_helper = match &desired { - ResolvedTunnel::Tailnet { identity, config } => Some( - self.tailnet_login - .ensure_session(tailnet_helper_request(identity, config)) - .await - .map_err(proc_err)? - .helper, - ), - _ => None, - }; let active = desired - .start(self.tun_interface.clone(), tailnet_helper) + .start(self.tun_interface.clone()) .await .map_err(proc_err)?; self.active_tunnel.write().await.replace(active); @@ -156,23 +137,6 @@ impl DaemonRPCServer { Ok(()) } - fn tailnet_bridge_request( - account_name: String, - identity_name: String, - hostname: String, - authority: String, - ) -> BridgeLoginStartRequest { - let mut request = BridgeLoginStartRequest { - account_name, - identity_name, - hostname: (!hostname.trim().is_empty()).then_some(hostname), - control_url: Self::tailnet_control_url(&authority), - packet_socket: None, - }; - request.packet_socket = Some(packet_socket_path(&request).display().to_string()); - request - } - fn tailnet_control_url(authority: &str) -> Option { let authority = discovery::normalize_authority(authority); (!discovery::is_managed_tailscale_authority(&authority)).then_some(authority) @@ -182,7 +146,6 @@ impl DaemonRPCServer { #[tonic::async_trait] impl Tunnel for DaemonRPCServer { type TunnelConfigurationStream = ReceiverStream>; - type TunnelPacketsStream = ReceiverStream>; type TunnelStatusStream = ReceiverStream>; async fn tunnel_configuration( @@ -208,62 +171,6 @@ impl Tunnel for DaemonRPCServer { Ok(Response::new(ReceiverStream::new(rx))) } - async fn tunnel_packets( - &self, - request: Request>, - ) -> Result, RspStatus> { - let (packet_tx, mut packet_rx) = { - let guard = self.active_tunnel.read().await; - let Some(active) = guard.as_ref() else { - return Err(RspStatus::failed_precondition("no active tunnel")); - }; - active.packet_stream().ok_or_else(|| { - RspStatus::failed_precondition( - "active tunnel does not support packet streaming", - ) - })? - }; - - let (tx, rx) = mpsc::channel(128); - tokio::spawn(async move { - loop { - match packet_rx.recv().await { - Ok(payload) => { - if tx.send(Ok(TunnelPacket { payload })).await.is_err() { - break; - } - } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, - } - } - }); - - let mut inbound = request.into_inner(); - tokio::spawn(async move { - loop { - match inbound.message().await { - Ok(Some(packet)) => { - debug!( - "daemon tunnel packet stream received {} bytes from client", - packet.payload.len() - ); - if packet_tx.send(packet.payload).await.is_err() { - break; - } - } - Ok(None) => break, - Err(error) => { - warn!("tailnet packet stream receive error: {error}"); - break; - } - } - } - }); - - Ok(Response::new(ReceiverStream::new(rx))) - } - async fn tunnel_start(&self, _request: Request) -> Result, RspStatus> { let desired = self.resolve_tunnel().await?; let already_running = { @@ -380,16 +287,9 @@ impl TailnetControl for DaemonRPCServer { request: Request, ) -> Result, RspStatus> { let request = request.into_inner(); - info!(email = %request.email, "daemon tailnet discover RPC received"); let discovery = discovery::discover_tailnet(&request.email) .await .map_err(proc_err)?; - info!( - email = %request.email, - authority = %discovery.authority, - provider = ?discovery.provider, - "daemon tailnet discover RPC resolved" - ); Ok(Response::new(TailnetDiscoverResponse { domain: discovery.domain, @@ -425,32 +325,17 @@ impl TailnetControl for DaemonRPCServer { request: Request, ) -> Result, RspStatus> { let request = request.into_inner(); - info!( - account = %request.account_name, - identity = %request.identity_name, - authority = %request.authority, - "daemon tailnet login start RPC received" - ); let response = self .tailnet_login - .start_login(Self::tailnet_bridge_request( - request.account_name, - request.identity_name, - request.hostname, - request.authority, - )) + .start_login(BridgeLoginStartRequest { + account_name: request.account_name, + identity_name: request.identity_name, + hostname: (!request.hostname.trim().is_empty()).then_some(request.hostname), + control_url: Self::tailnet_control_url(&request.authority), + }) .await .map_err(proc_err)?; - info!( - session_id = %response.session_id, - backend_state = %response.status.backend_state, - running = response.status.running, - needs_login = response.status.needs_login, - auth_url = ?response.status.auth_url, - "daemon tailnet login start RPC resolved" - ); - Ok(Response::new(tailnet_login_rsp( response.session_id, response.status, @@ -462,7 +347,6 @@ impl TailnetControl for DaemonRPCServer { request: Request, ) -> Result, RspStatus> { let request = request.into_inner(); - info!(session_id = %request.session_id, "daemon tailnet login status RPC received"); let status = self .tailnet_login .status(&request.session_id) @@ -471,14 +355,6 @@ impl TailnetControl for DaemonRPCServer { let Some(status) = status else { return Err(RspStatus::not_found("tailnet login session not found")); }; - info!( - session_id = %request.session_id, - backend_state = %status.backend_state, - running = status.running, - needs_login = status.needs_login, - auth_url = ?status.auth_url, - "daemon tailnet login status RPC resolved" - ); Ok(Response::new(tailnet_login_rsp(request.session_id, status))) } @@ -505,12 +381,8 @@ fn proc_err(err: impl ToString) -> RspStatus { fn configuration_rsp(config: ServerConfig) -> TunnelConfigurationResponse { TunnelConfigurationResponse { - addresses: config.address, mtu: config.mtu.unwrap_or(1000), - routes: config.routes, - dns_servers: config.dns_servers, - search_domains: config.search_domains, - include_default_route: config.include_default_route, + addresses: config.address, } } diff --git a/burrow/src/daemon/rpc/response.rs b/burrow/src/daemon/rpc/response.rs index 6d03581..8948ca4 100644 --- a/burrow/src/daemon/rpc/response.rs +++ b/burrow/src/daemon/rpc/response.rs @@ -68,14 +68,6 @@ impl TryFrom<&TunInterface> for ServerInfo { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct ServerConfig { pub address: Vec, - #[serde(default)] - pub routes: Vec, - #[serde(default)] - pub dns_servers: Vec, - #[serde(default)] - pub search_domains: Vec, - #[serde(default)] - pub include_default_route: bool, pub name: Option, pub mtu: Option, } @@ -86,14 +78,6 @@ impl TryFrom<&Config> for ServerConfig { fn try_from(config: &Config) -> anyhow::Result { Ok(ServerConfig { address: config.interface.address.clone(), - routes: config - .peers - .iter() - .flat_map(|peer| peer.allowed_ips.iter().cloned()) - .collect(), - dns_servers: config.interface.dns.clone(), - search_domains: Vec::new(), - include_default_route: false, name: None, mtu: config.interface.mtu.map(|mtu| mtu as i32), }) @@ -104,10 +88,6 @@ impl Default for ServerConfig { fn default() -> Self { Self { address: vec!["10.13.13.2".to_string()], // Dummy remote address - routes: Vec::new(), - dns_servers: Vec::new(), - search_domains: Vec::new(), - include_default_route: false, name: None, mtu: None, } diff --git a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap index 68b4195..c40db25 100644 --- a/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap +++ b/burrow/src/daemon/rpc/snapshots/burrow__daemon__rpc__response__response_serialization-4.snap @@ -2,4 +2,4 @@ source: burrow/src/daemon/rpc/response.rs expression: "serde_json::to_string(&DaemonResponse::new(Ok::(DaemonResponseData::ServerConfig(ServerConfig::default()))))?" --- -{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"routes":[],"dns_servers":[],"search_domains":[],"include_default_route":false,"name":null,"mtu":null}},"id":0} +{"result":{"Ok":{"type":"ServerConfig","address":["10.13.13.2"],"name":null,"mtu":null}},"id":0} diff --git a/burrow/src/daemon/runtime.rs b/burrow/src/daemon/runtime.rs index 31821a2..84dfd2b 100644 --- a/burrow/src/daemon/runtime.rs +++ b/burrow/src/daemon/runtime.rs @@ -1,13 +1,7 @@ -use std::{path::PathBuf, sync::Arc}; +use std::sync::Arc; -use anyhow::{bail, Context, Result}; -use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, - net::UnixStream, - sync::{broadcast, mpsc, RwLock}, - task::JoinHandle, - time::{sleep, Duration}, -}; +use anyhow::{Context, Result}; +use tokio::{sync::RwLock, task::JoinHandle}; use tun::{tokio::TunInterface, TunOptions}; use super::rpc::{ @@ -15,11 +9,7 @@ use super::rpc::{ ServerConfig, }; use crate::{ - auth::server::tailscale::{ - default_hostname, packet_socket_path, spawn_tailscale_helper, TailscaleHelperProcess, - TailscaleLoginStartRequest, TailscaleLoginStatus, - }, - control::{discovery, TailnetConfig}, + control::TailnetConfig, wireguard::{Config, Interface as WireGuardInterface}, }; @@ -88,19 +78,11 @@ impl ResolvedTunnel { match self { Self::Passthrough { .. } => Ok(ServerConfig { address: Vec::new(), - routes: Vec::new(), - dns_servers: Vec::new(), - search_domains: Vec::new(), - include_default_route: false, name: None, mtu: Some(1500), }), Self::Tailnet { .. } => Ok(ServerConfig { address: Vec::new(), - routes: tailnet_routes(), - dns_servers: tailnet_dns_servers(), - search_domains: Vec::new(), - include_default_route: false, name: None, mtu: Some(1280), }), @@ -111,71 +93,21 @@ impl ResolvedTunnel { pub async fn start( self, tun_interface: Arc>>, - tailnet_helper: Option>, ) -> Result { match self { - Self::Passthrough { identity } => Ok(ActiveTunnel::Passthrough { - identity, - server_config: ServerConfig { - address: Vec::new(), - routes: Vec::new(), - dns_servers: Vec::new(), - search_domains: Vec::new(), - include_default_route: false, - name: None, - mtu: Some(1500), - }, - }), - Self::Tailnet { identity, config } => { - let (helper, shutdown_helper_on_stop) = match tailnet_helper { - Some(helper) => (helper, false), - None => { - let helper_request = tailnet_helper_request(&identity, &config); - let helper = Arc::new(spawn_tailscale_helper(&helper_request).await?); - (helper, true) - } - }; - let status = wait_for_tailnet_ready(helper.as_ref()).await?; - let server_config = tailnet_server_config(&status); - let packet_socket = helper - .packet_socket() - .map(PathBuf::from) - .ok_or_else(|| anyhow::anyhow!("tailnet helper did not report a packet socket"))?; - let packet_bridge = connect_tailnet_packet_bridge(packet_socket).await?; - #[cfg(target_vendor = "apple")] - let tun_task = None; - #[cfg(not(target_vendor = "apple"))] - let tun_task = { - let tun = TunOptions::new().open()?; - tun_interface.write().await.replace(tun); - Some(tokio::spawn(run_tailnet_tun_bridge( - tun_interface.clone(), - packet_bridge.outbound_sender(), - packet_bridge.subscribe(), - ))) - }; - - Ok(ActiveTunnel::Tailnet { - identity, - server_config, - helper, - shutdown_helper_on_stop, - packet_bridge, - tun_task, - }) - } + Self::Passthrough { identity } => Ok(ActiveTunnel::Passthrough { identity }), + Self::Tailnet { config, .. } => Err(anyhow::anyhow!( + "tailnet runtime is not wired in this checkout yet ({:?})", + config.provider + )), Self::WireGuard { identity, config } => { - let server_config = ServerConfig::try_from(&config)?; let tun = TunOptions::new().open()?; tun_interface.write().await.replace(tun); match start_wireguard_runtime(config, tun_interface.clone()).await { - Ok((interface, task)) => Ok(ActiveTunnel::WireGuard { - identity, - server_config, - interface, - task, - }), + Ok((interface, task)) => { + Ok(ActiveTunnel::WireGuard { identity, interface, task }) + } Err(err) => { tun_interface.write().await.take(); Err(err) @@ -189,19 +121,9 @@ impl ResolvedTunnel { pub enum ActiveTunnel { Passthrough { identity: RuntimeIdentity, - server_config: ServerConfig, - }, - Tailnet { - identity: RuntimeIdentity, - server_config: ServerConfig, - helper: Arc, - shutdown_helper_on_stop: bool, - packet_bridge: TailnetPacketBridge, - tun_task: Option>>, }, WireGuard { identity: RuntimeIdentity, - server_config: ServerConfig, interface: Arc>, task: JoinHandle>, }, @@ -210,69 +132,15 @@ pub enum ActiveTunnel { impl ActiveTunnel { pub fn identity(&self) -> &RuntimeIdentity { match self { - Self::Passthrough { identity, .. } - | Self::Tailnet { identity, .. } + Self::Passthrough { identity } | Self::WireGuard { identity, .. } => identity, } } - pub fn server_config(&self) -> &ServerConfig { - match self { - Self::Passthrough { server_config, .. } - | Self::Tailnet { server_config, .. } - | Self::WireGuard { server_config, .. } => server_config, - } - } - - pub fn packet_stream( - &self, - ) -> Option<(mpsc::Sender>, broadcast::Receiver>)> { - match self { - Self::Tailnet { packet_bridge, .. } => Some(( - packet_bridge.outbound_sender(), - packet_bridge.subscribe(), - )), - _ => None, - } - } - pub async fn shutdown(self, tun_interface: &Arc>>) -> Result<()> { match self { Self::Passthrough { .. } => Ok(()), - Self::Tailnet { - helper, - shutdown_helper_on_stop, - packet_bridge, - tun_task, - .. - } => { - if let Some(tun_task) = tun_task { - tun_task.abort(); - match tun_task.await { - Ok(Ok(())) => {} - Ok(Err(err)) => return Err(err), - Err(err) if err.is_cancelled() => {} - Err(err) => return Err(err.into()), - } - } - packet_bridge.task.abort(); - match packet_bridge.task.await { - Ok(Ok(())) => {} - Ok(Err(err)) => return Err(err), - Err(err) if err.is_cancelled() => {} - Err(err) => return Err(err.into()), - } - tun_interface.write().await.take(); - if shutdown_helper_on_stop { - helper.shutdown().await?; - } - Ok(()) - } - Self::WireGuard { - interface, - task, - .. - } => { + Self::WireGuard { interface, task, .. } => { interface.read().await.remove_tun().await; let task_result = task.await; tun_interface.write().await.take(); @@ -283,22 +151,6 @@ impl ActiveTunnel { } } -pub struct TailnetPacketBridge { - outbound: mpsc::Sender>, - inbound: broadcast::Sender>, - task: JoinHandle>, -} - -impl TailnetPacketBridge { - fn outbound_sender(&self) -> mpsc::Sender> { - self.outbound.clone() - } - - fn subscribe(&self) -> broadcast::Receiver> { - self.inbound.subscribe() - } -} - async fn start_wireguard_runtime( config: Config, tun_interface: Arc>>, @@ -314,279 +166,6 @@ async fn start_wireguard_runtime( Ok((interface, task)) } -pub(crate) fn tailnet_helper_request( - identity: &RuntimeIdentity, - config: &TailnetConfig, -) -> TailscaleLoginStartRequest { - let account_name = config - .account - .as_deref() - .filter(|value| !value.trim().is_empty()) - .unwrap_or("default") - .to_owned(); - let identity_name = config - .identity - .as_deref() - .filter(|value| !value.trim().is_empty()) - .map(ToOwned::to_owned) - .unwrap_or_else(|| match identity { - RuntimeIdentity::Network { id, .. } => format!("network-{id}"), - RuntimeIdentity::Passthrough => "apple".to_owned(), - }); - let control_url = config.authority.as_deref().and_then(|authority| { - let authority = discovery::normalize_authority(authority); - (!discovery::is_managed_tailscale_authority(&authority)).then_some(authority) - }); - - let mut request = TailscaleLoginStartRequest { - account_name, - identity_name, - hostname: config.hostname.clone(), - control_url, - packet_socket: None, - }; - request.packet_socket = Some(packet_socket_path(&request).display().to_string()); - if request - .hostname - .as_deref() - .map(|value| value.trim().is_empty()) - .unwrap_or(true) - { - request.hostname = Some(default_hostname(&request)); - } - request -} - -async fn wait_for_tailnet_ready(helper: &TailscaleHelperProcess) -> Result { - let mut last_status = None; - for _ in 0..120 { - let status = helper.status().await?; - if status.running && !status.tailscale_ips.is_empty() { - return Ok(status); - } - if status.needs_login || status.auth_url.is_some() { - bail!("tailnet runtime requires a completed login before the tunnel can start"); - } - last_status = Some(status); - sleep(Duration::from_millis(250)).await; - } - - if let Some(status) = last_status { - bail!( - "tailnet helper never became ready (backend_state={})", - status.backend_state - ); - } - bail!("tailnet helper never produced a status update") -} - -fn tailnet_server_config(status: &TailscaleLoginStatus) -> ServerConfig { - let mut search_domains = Vec::new(); - if let Some(suffix) = status.magic_dns_suffix.as_deref() { - let suffix = suffix.trim().trim_end_matches('.'); - if !suffix.is_empty() { - search_domains.push(suffix.to_owned()); - } - } - - ServerConfig { - address: status - .tailscale_ips - .iter() - .map(|ip| tailnet_cidr(ip)) - .collect(), - routes: tailnet_routes(), - dns_servers: tailnet_dns_servers(), - search_domains, - include_default_route: false, - name: status.self_dns_name.clone(), - mtu: Some(1280), - } -} - -fn tailnet_routes() -> Vec { - vec!["100.64.0.0/10".to_owned(), "fd7a:115c:a1e0::/48".to_owned()] -} - -fn tailnet_dns_servers() -> Vec { - vec!["100.100.100.100".to_owned()] -} - -fn tailnet_cidr(ip: &str) -> String { - if ip.contains('/') { - return ip.to_owned(); - } - if ip.contains(':') { - format!("{ip}/128") - } else { - format!("{ip}/32") - } -} - -async fn connect_tailnet_packet_bridge(packet_socket: PathBuf) -> Result { - let mut last_error = None; - let mut stream = None; - for _ in 0..50 { - match UnixStream::connect(&packet_socket).await { - Ok(connected) => { - stream = Some(connected); - break; - } - Err(err) => { - last_error = Some(err); - sleep(Duration::from_millis(100)).await; - } - } - } - let stream = if let Some(stream) = stream { - stream - } else { - return Err(last_error - .context("failed to connect to tailnet helper packet socket")? - .into()); - }; - - let (outbound_tx, outbound_rx) = mpsc::channel(128); - let (inbound_tx, _) = broadcast::channel(128); - let task = tokio::spawn(run_tailnet_socket_bridge( - stream, - outbound_rx, - inbound_tx.clone(), - )); - - Ok(TailnetPacketBridge { - outbound: outbound_tx, - inbound: inbound_tx, - task, - }) -} - -async fn run_tailnet_socket_bridge( - stream: UnixStream, - mut outbound_rx: mpsc::Receiver>, - inbound_tx: broadcast::Sender>, -) -> Result<()> { - let (mut reader, mut writer) = stream.into_split(); - - let inbound = tokio::spawn(async move { - loop { - let packet = read_packet_frame(&mut reader).await?; - tracing::debug!( - "tailnet packet bridge received {} bytes from helper socket", - packet.len() - ); - let _ = inbound_tx.send(packet); - } - #[allow(unreachable_code)] - Result::<()>::Ok(()) - }); - - let outbound = tokio::spawn(async move { - while let Some(packet) = outbound_rx.recv().await { - tracing::debug!( - "tailnet packet bridge writing {} bytes to helper socket", - packet.len() - ); - write_packet_frame(&mut writer, &packet).await?; - } - Result::<()>::Ok(()) - }); - - let (inbound_result, outbound_result) = tokio::try_join!(inbound, outbound)?; - inbound_result?; - outbound_result?; - Ok(()) -} - -#[cfg(not(target_vendor = "apple"))] -async fn run_tailnet_tun_bridge( - tun_interface: Arc>>, - outbound_tx: mpsc::Sender>, - mut inbound_rx: broadcast::Receiver>, -) -> Result<()> { - let inbound_tun = tun_interface.clone(); - let inbound = tokio::spawn(async move { - loop { - let packet = match inbound_rx.recv().await { - Ok(packet) => packet, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => break, - }; - let guard = inbound_tun.read().await; - let Some(tun) = guard.as_ref() else { - bail!("tailnet tun interface unavailable"); - }; - tun.send(&packet) - .await - .context("failed to write tailnet packet to tun")?; - } - Result::<()>::Ok(()) - }); - - let outbound_tun = tun_interface.clone(); - let outbound = tokio::spawn(async move { - let mut buf = vec![0u8; 65_535]; - loop { - let len = { - let guard = outbound_tun.read().await; - let Some(tun) = guard.as_ref() else { - bail!("tailnet tun interface unavailable"); - }; - tun.recv(&mut buf) - .await - .context("failed to read packet from tailnet tun")? - }; - outbound_tx - .send(buf[..len].to_vec()) - .await - .context("failed to forward packet to tailnet helper")?; - } - #[allow(unreachable_code)] - Result::<()>::Ok(()) - }); - - let (inbound_result, outbound_result) = tokio::try_join!(inbound, outbound)?; - inbound_result?; - outbound_result?; - Ok(()) -} - -async fn read_packet_frame(reader: &mut R) -> Result> -where - R: AsyncRead + Unpin, -{ - let mut len_buf = [0u8; 4]; - reader - .read_exact(&mut len_buf) - .await - .context("failed to read tailnet packet frame length")?; - let len = u32::from_be_bytes(len_buf) as usize; - let mut packet = vec![0u8; len]; - reader - .read_exact(&mut packet) - .await - .context("failed to read tailnet packet frame payload")?; - Ok(packet) -} - -async fn write_packet_frame(writer: &mut W, packet: &[u8]) -> Result<()> -where - W: AsyncWrite + Unpin, -{ - writer - .write_all(&(packet.len() as u32).to_be_bytes()) - .await - .context("failed to write tailnet packet frame length")?; - writer - .write_all(packet) - .await - .context("failed to write tailnet packet frame payload")?; - writer - .flush() - .await - .context("failed to flush tailnet packet frame") -} - #[cfg(test)] mod tests { use super::*; @@ -600,19 +179,4 @@ mod tests { Vec::::new() ); } - - #[test] - fn tailnet_server_config_uses_host_prefixes() { - let status = TailscaleLoginStatus { - running: true, - tailscale_ips: vec!["100.101.102.103".to_owned(), "fd7a:115c:a1e0::123".to_owned()], - ..Default::default() - }; - let config = tailnet_server_config(&status); - assert_eq!( - config.address, - vec!["100.101.102.103/32", "fd7a:115c:a1e0::123/128"] - ); - assert_eq!(config.mtu, Some(1280)); - } } diff --git a/burrow/src/main.rs b/burrow/src/main.rs index cfa2085..4ab7700 100644 --- a/burrow/src/main.rs +++ b/burrow/src/main.rs @@ -283,7 +283,9 @@ async fn try_tailnet_discover(email: &str) -> Result<()> { let mut client = BurrowClient::from_uds().await?; let response = client .tailnet_client - .discover(crate::daemon::rpc::grpc_defs::TailnetDiscoverRequest { email: email.to_owned() }) + .discover(crate::daemon::rpc::grpc_defs::TailnetDiscoverRequest { + email: email.to_owned(), + }) .await? .into_inner(); println!("Tailnet Discover Response: {:?}", response); @@ -368,9 +370,13 @@ async fn try_tailnet_ping(remote: &str, payload: &str, timeout_ms: u64) -> Resul "tailnet ping received {} bytes from daemon packet stream", packet.payload.len() ); - if let Some(reply) = - parse_icmp_echo_reply(&packet.payload, local_ip, remote_ip, identifier, sequence)? - { + if let Some(reply) = parse_icmp_echo_reply( + &packet.payload, + local_ip, + remote_ip, + identifier, + sequence, + )? { break Ok::<_, anyhow::Error>(reply); } } @@ -458,7 +464,8 @@ async fn try_tailnet_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> R let egress_task = tokio::spawn(async move { while let Some(packet) = stack_stream.next().await { - let payload = packet.context("failed to read outbound packet from userspace stack")?; + let payload = + packet.context("failed to read outbound packet from userspace stack")?; log::debug!( "tailnet udp echo sending {} bytes into daemon packet stream", payload.len() @@ -477,7 +484,9 @@ async fn try_tailnet_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> R .send((message.as_bytes().to_vec(), local_addr, remote_addr)) .await .context("failed to send UDP echo probe into userspace stack")?; - log::debug!("tailnet udp echo probe queued from {local_addr} to {remote_addr}"); + log::debug!( + "tailnet udp echo probe queued from {local_addr} to {remote_addr}" + ); let response = timeout(Duration::from_millis(timeout_ms), udp_reader.next()) .await @@ -507,10 +516,7 @@ async fn try_tailnet_udp_echo(remote: &str, message: &str, timeout_ms: u64) -> R } #[cfg(any(target_os = "linux", target_vendor = "apple"))] -fn select_tailnet_local_ip( - addresses: &[String], - remote_ip: std::net::IpAddr, -) -> Result { +fn select_tailnet_local_ip(addresses: &[String], remote_ip: std::net::IpAddr) -> Result { use anyhow::Context; let family_is_v4 = remote_ip.is_ipv4(); diff --git a/burrow/src/tracing.rs b/burrow/src/tracing.rs index 8a245ef..21e16ae 100644 --- a/burrow/src/tracing.rs +++ b/burrow/src/tracing.rs @@ -47,16 +47,10 @@ pub fn initialize() { #[cfg(target_os = "macos")] let subscriber = { - // `tracing_oslog` is crashing under Tokio/h2 span churn in the host daemon on - // current macOS. Keep logging on stderr by default and allow opt-in OSLog - // only when explicitly requested for local debugging. - let enable_oslog = matches!( - std::env::var("BURROW_ENABLE_OSLOG").as_deref(), - Ok("1" | "true" | "TRUE" | "yes" | "YES") - ); - let system_log = enable_oslog.then(|| { - tracing_oslog::OsLogger::new("com.hackclub.burrow", "tracing") - }); + let system_log = Some(tracing_oslog::OsLogger::new( + "com.hackclub.burrow", + "tracing", + )); let stderr = (console::user_attended_stderr() || system_log.is_none()).then(make_stderr); Registry::default().with(stderr).with(system_log) }; diff --git a/flake.nix b/flake.nix index 1974f17..1e91dcc 100644 --- a/flake.nix +++ b/flake.nix @@ -94,7 +94,6 @@ pkgs.stdenvNoCC.mkDerivation { pname = "nsc"; inherit version src; - meta.mainProgram = "nsc"; dontConfigure = true; dontBuild = true; unpackPhase = '' @@ -145,35 +144,6 @@ subPackages = [ "./cmd/forgejo-nsc-autoscaler" ]; vendorHash = "sha256-Kpr+5Q7Dy4JiLuJVZbFeJAzLR7PLPYxhtJqfxMEytcs="; }; - burrowSrc = lib.cleanSourceWith { - src = ./.; - filter = path: type: - let - p = toString path; - name = builtins.baseNameOf path; - hasDir = dir: lib.hasInfix "/${dir}/" p || lib.hasSuffix "/${dir}" p; - in - !(hasDir ".git" || hasDir "target" || hasDir "node_modules" || name == "result"); - }; - burrowPkg = pkgs.rustPlatform.buildRustPackage { - pname = "burrow"; - version = "0.1.0"; - src = burrowSrc; - cargoLock = { - lockFile = ./Cargo.lock; - outputHashes = { - "tracing-oslog-0.1.2" = "sha256-DjJDiPCTn43zJmmOfuRnyti8iQf9qoXICMKIx4bAG3I="; - }; - }; - cargoBuildFlags = [ - "-p" - "burrow" - "--bin" - "burrow" - ]; - nativeBuildInputs = [ pkgs.protobuf ]; - meta.mainProgram = "burrow"; - }; in { devShells.default = pkgs.mkShell { @@ -201,7 +171,6 @@ packages = { agenix = agenix.packages.${system}.agenix; - burrow = burrowPkg; hcloud-upload-image = hcloudUploadImagePkg; forgejo-nsc-dispatcher = forgejoNscDispatcher; forgejo-nsc-autoscaler = forgejoNscAutoscaler; @@ -214,6 +183,7 @@ nixosModules.burrow-forgejo-nsc = nsc-autoscaler.nixosModules.default; nixosModules.burrow-authentik = import ./nixos/modules/burrow-authentik.nix; nixosModules.burrow-headscale = import ./nixos/modules/burrow-headscale.nix; + nixosConfigurations.burrow-forge = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; specialArgs = { diff --git a/nixos/README.md b/nixos/README.md index 23907f3..c79d8ce 100644 --- a/nixos/README.md +++ b/nixos/README.md @@ -23,7 +23,7 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B - `../Scripts/cloudflare-upsert-a-record.sh`: upsert DNS-only Cloudflare `A` records for Burrow host cutovers - `../Scripts/forge-deploy.sh`: remote `nixos-rebuild` entrypoint for the forge host - `../Scripts/provision-forgejo-nsc.sh`: render Burrow Namespace dispatcher/autoscaler runtime inputs and ensure the default Forgejo scope exists -- `../Scripts/seal-forgejo-nsc-secrets.sh`: encrypt forgejo-nsc runtime inputs into the agenix secrets consumed by `burrow-forge` +- `../Scripts/sync-forgejo-nsc-config.sh`: copy intake-backed dispatcher/autoscaler inputs to the host ## Intended Flow @@ -32,17 +32,15 @@ Mail hosting is intentionally not part of this NixOS host in the current plan. B 3. Run `Scripts/bootstrap-forge-intake.sh` to place the Forgejo bootstrap password file and automation SSH key under `/var/lib/burrow/intake/`. 4. Let `burrow-forgejo-bootstrap.service` create or rotate the initial Forgejo admin account. 5. Let `burrow-forgejo-runner-bootstrap.service` register the self-hosted Forgejo runner and seed Git identity as `agent `. -6. Run `Scripts/provision-forgejo-nsc.sh` locally to refresh `intake/forgejo_nsc_token.txt`, `intake/forgejo_nsc_dispatcher.yaml`, and `intake/forgejo_nsc_autoscaler.yaml`. -7. Run `Scripts/seal-forgejo-nsc-secrets.sh` to encrypt those runtime inputs into the agenix secrets used by `burrow-forge`. -8. Ensure `/var/lib/agenix/agenix.key` exists on the host, encrypt `secrets/infra/authentik.env.age`, `secrets/infra/authentik-google-client-id.age`, `secrets/infra/authentik-google-client-secret.age`, `secrets/infra/forgejo-oidc-client-secret.age`, `secrets/infra/headscale-oidc-client-secret.age`, `secrets/infra/forgejo-nsc-token.age`, `secrets/infra/forgejo-nsc-dispatcher-config.age`, and `secrets/infra/forgejo-nsc-autoscaler-config.age`, and let agenix materialize them under `/run/agenix/`. -9. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, `auth.burrow.net`, `ts.burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME. -10. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace. -11. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`. +6. Run `Scripts/provision-forgejo-nsc.sh` locally, then `Scripts/sync-forgejo-nsc-config.sh` to place the raw Namespace dispatcher/autoscaler runtime inputs under `/var/lib/burrow/intake/` for the upstream `services.forgejo-nsc` module. +7. Ensure `/var/lib/agenix/agenix.key` exists on the host, encrypt `secrets/infra/authentik.env.age`, `secrets/infra/authentik-google-client-id.age`, `secrets/infra/authentik-google-client-secret.age`, `secrets/infra/forgejo-oidc-client-secret.age`, and `secrets/infra/headscale-oidc-client-secret.age`, and let agenix materialize them under `/run/agenix/`. +8. Use `Scripts/cloudflare-upsert-a-record.sh` to point `git.burrow.net`, `burrow.net`, `auth.burrow.net`, `ts.burrow.net`, and `nsc-autoscaler.burrow.net` at the host with Cloudflare proxying disabled for ACME. +9. Use `Scripts/forge-deploy.sh --allow-dirty` for subsequent remote `nixos-rebuild` runs from the live workspace. +10. Configure Forward Email custom S3 backups for `burrow.net` and `burrow.rs` out-of-band with `Tools/forwardemail-custom-s3.sh`. ## Current Constraints -- `burrow-forge` is live on NixOS in `hel1` at `89.167.47.21`. -- `services.forgejo-nsc` now expects agenix-backed runtime inputs at `/run/agenix/burrowForgejoNscToken`, `/run/agenix/burrowForgejoNscDispatcherConfig`, and `/run/agenix/burrowForgejoNscAutoscalerConfig`. +- `burrow-forge` is live on NixOS in `hel1` at `89.167.47.21`, and `Scripts/check-forge-host.sh --expect-nsc` passes locally against that host. - Authentik and Headscale secrets now live in tracked agenix blobs under `secrets/infra/` and decrypt to `/run/agenix/` on the forge host. - Public Burrow forge cutover completed on March 15, 2026: - `burrow.net`, `git.burrow.net`, and `nsc-autoscaler.burrow.net` now publish public `A` records to `89.167.47.21` diff --git a/nixos/hosts/burrow-forge/default.nix b/nixos/hosts/burrow-forge/default.nix index 7f6af22..75b76d4 100644 --- a/nixos/hosts/burrow-forge/default.nix +++ b/nixos/hosts/burrow-forge/default.nix @@ -87,24 +87,6 @@ in group = "root"; mode = "0400"; }; - age.secrets.burrowForgejoNscToken = { - file = ../../../secrets/infra/forgejo-nsc-token.age; - owner = "forgejo-nsc"; - group = "forgejo-nsc"; - mode = "0400"; - }; - age.secrets.burrowForgejoNscDispatcherConfig = { - file = ../../../secrets/infra/forgejo-nsc-dispatcher-config.age; - owner = "forgejo-nsc"; - group = "forgejo-nsc"; - mode = "0400"; - }; - age.secrets.burrowForgejoNscAutoscalerConfig = { - file = ../../../secrets/infra/forgejo-nsc-autoscaler-config.age; - owner = "forgejo-nsc"; - group = "forgejo-nsc"; - mode = "0400"; - }; networking.extraHosts = '' 127.0.0.1 burrow.net git.burrow.net auth.burrow.net ts.burrow.net nsc-autoscaler.burrow.net @@ -130,13 +112,13 @@ in services.forgejo-nsc = { enable = true; - nscTokenFile = config.age.secrets.burrowForgejoNscToken.path; + nscTokenFile = "/var/lib/burrow/intake/forgejo_nsc_token.txt"; dispatcher = { - configFile = config.age.secrets.burrowForgejoNscDispatcherConfig.path; + configFile = "/var/lib/burrow/intake/forgejo_nsc_dispatcher.yaml"; }; autoscaler = { enable = true; - configFile = config.age.secrets.burrowForgejoNscAutoscalerConfig.path; + configFile = "/var/lib/burrow/intake/forgejo_nsc_autoscaler.yaml"; }; }; diff --git a/proto/burrow.proto b/proto/burrow.proto index ed1f89e..a590cb1 100644 --- a/proto/burrow.proto +++ b/proto/burrow.proto @@ -5,7 +5,6 @@ import "google/protobuf/timestamp.proto"; service Tunnel { rpc TunnelConfiguration (Empty) returns (stream TunnelConfigurationResponse); - rpc TunnelPackets (stream TunnelPacket) returns (stream TunnelPacket); rpc TunnelStart (Empty) returns (Empty); rpc TunnelStop (Empty) returns (Empty); rpc TunnelStatus (Empty) returns (stream TunnelStatusResponse); @@ -129,12 +128,4 @@ message TunnelStatusResponse { message TunnelConfigurationResponse { repeated string addresses = 1; int32 mtu = 2; - repeated string routes = 3; - repeated string dns_servers = 4; - repeated string search_domains = 5; - bool include_default_route = 6; -} - -message TunnelPacket { - bytes payload = 1; } diff --git a/secrets.nix b/secrets.nix index a8fb923..c0b9b53 100644 --- a/secrets.nix +++ b/secrets.nix @@ -16,9 +16,6 @@ in "secrets/infra/authentik-google-client-secret.age".publicKeys = burrowForgeRecipients; "secrets/infra/authentik-ui-test-password.age".publicKeys = uiTestRecipients; "secrets/infra/forgejo-oidc-client-secret.age".publicKeys = burrowForgeRecipients; - "secrets/infra/forgejo-nsc-autoscaler-config.age".publicKeys = burrowForgeRecipients; - "secrets/infra/forgejo-nsc-dispatcher-config.age".publicKeys = burrowForgeRecipients; - "secrets/infra/forgejo-nsc-token.age".publicKeys = burrowForgeRecipients; "secrets/infra/headscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients; "secrets/infra/tailscale-oidc-client-secret.age".publicKeys = burrowForgeRecipients; } diff --git a/secrets/infra/forgejo-nsc-autoscaler-config.age b/secrets/infra/forgejo-nsc-autoscaler-config.age deleted file mode 100644 index 28e3d4a..0000000 Binary files a/secrets/infra/forgejo-nsc-autoscaler-config.age and /dev/null differ diff --git a/secrets/infra/forgejo-nsc-dispatcher-config.age b/secrets/infra/forgejo-nsc-dispatcher-config.age deleted file mode 100644 index 5ef71b5..0000000 Binary files a/secrets/infra/forgejo-nsc-dispatcher-config.age and /dev/null differ diff --git a/secrets/infra/forgejo-nsc-token.age b/secrets/infra/forgejo-nsc-token.age deleted file mode 100644 index ff8c278..0000000 --- a/secrets/infra/forgejo-nsc-token.age +++ /dev/null @@ -1,15 +0,0 @@ -age-encryption.org/v1 --> ssh-ed25519 ux4N8Q yCjzc3QW91l62Y+U2YZqLpTkiZyTJAxQQCiZ+DxHiWI -mG/+2fppo3RITeohTM/Dm1M6fsErtxhOgIeI2FqvoUs --> ssh-ed25519 IrZmAg +Y59O8SVATZfe8Vu2gis1KNWcL34Ct7M3G34XNURczw -GGkVYcmoUtJRx4zftjLFID2wLtNtCgGVnYuMN8XF74s --> X25519 xqDMDV9XRhSPlFy2IJPBfpUGuNA9gpX73kg8Pnj48VI -TPZZNrRUK+FzruetDFuJcTzed03d7gkxOv8QAZshBn8 ---- PRD84efdrqDmPeRA8zi0D2V8RmT0tFVbDIVD6U/4KVo -2Wk*cS++j9{4j;`wd3,"gligЇ e`''# "'(=LS3hFjgYIF|0$Fp^` -QknUx78b!>n?9^!=ͮ [a ` ϫ_#?T@]Eβ[,g퟇cjx}.̞f45֕DLH4_HdwXwXkRx7DM,0 7*TU{~ä8yC "/oXCe8-ulYt ;ҖDZdm wFyiIώɅ8F}l"Isu{L!+UBei_Z~D>B)L>