diff --git a/Apple/Burrow.xcodeproj/project.pbxproj b/Apple/Burrow.xcodeproj/project.pbxproj index 995af28..9897f79 100644 --- a/Apple/Burrow.xcodeproj/project.pbxproj +++ b/Apple/Burrow.xcodeproj/project.pbxproj @@ -42,8 +42,8 @@ D0D4E5A62C8D9E65007F820A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; D0F4FAD32C8DC79C0068730A /* BurrowCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D4E5312C8D996F007F820A /* BurrowCore.framework */; }; D0F7594E2C8DAB6B00126CF3 /* GRPC in Frameworks */ = {isa = PBXBuildFile; productRef = D078F7E02C8DA375008A8CEC /* GRPC */; }; - D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4962C8D921A007F820A /* grpc-swift-config.json */; }; - D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */; }; + D0FA10012D10200100112233 /* burrow.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10032D10200100112233 /* burrow.pb.swift */; }; + D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FA10042D10200100112233 /* burrow.grpc.swift */; }; D0F7597E2C8DB30500126CF3 /* CGRPCZlib in Frameworks */ = {isa = PBXBuildFile; productRef = D0F7597D2C8DB30500126CF3 /* CGRPCZlib */; }; D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D4E4992C8D921A007F820A /* Client.swift */; }; /* End PBXBuildFile section */ @@ -154,8 +154,6 @@ D0BCC6032A09535900AD070D /* libburrow.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libburrow.a; sourceTree = BUILT_PRODUCTS_DIR; }; D0BF09582C8E6789000D8DEC /* UI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UI.xcconfig; sourceTree = ""; }; D0D4E4952C8D921A007F820A /* burrow.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = burrow.proto; sourceTree = ""; }; - D0D4E4962C8D921A007F820A /* grpc-swift-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "grpc-swift-config.json"; sourceTree = ""; }; - D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "swift-protobuf-config.json"; sourceTree = ""; }; D0D4E4992C8D921A007F820A /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; D0D4E49A2C8D921A007F820A /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; D0D4E49E2C8D921A007F820A /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; @@ -179,6 +177,8 @@ D0D4E58E2C8D9D0A007F820A /* Constants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Constants.h; sourceTree = ""; }; D0D4E58F2C8D9D0A007F820A /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; D0D4E5902C8D9D0A007F820A /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D0FA10032D10200100112233 /* burrow.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.pb.swift; sourceTree = ""; }; + D0FA10042D10200100112233 /* burrow.grpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/burrow.grpc.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -317,8 +317,8 @@ isa = PBXGroup; children = ( D0D4E4952C8D921A007F820A /* burrow.proto */, - D0D4E4962C8D921A007F820A /* grpc-swift-config.json */, - D0D4E4972C8D921A007F820A /* swift-protobuf-config.json */, + D0FA10032D10200100112233 /* burrow.pb.swift */, + D0FA10042D10200100112233 /* burrow.grpc.swift */, ); path = Client; sourceTree = ""; @@ -428,8 +428,6 @@ ); dependencies = ( D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */, - D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */, - D0F759602C8DB24400126CF3 /* PBXTargetDependency */, ); name = Core; packageProductDependencies = ( @@ -617,8 +615,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0F759612C8DB24B00126CF3 /* grpc-swift-config.json in Sources */, - D0F759622C8DB24B00126CF3 /* swift-protobuf-config.json in Sources */, + D0FA10012D10200100112233 /* burrow.pb.swift in Sources */, + D0FA10022D10200100112233 /* burrow.grpc.swift in Sources */, D0F7598D2C8DB3DA00126CF3 /* Client.swift in Sources */, D0D4E56B2C8D9C2F007F820A /* Logging.swift in Sources */, ); @@ -689,14 +687,6 @@ target = D0D4E5302C8D996F007F820A /* Core */; targetProxy = D0F4FAD12C8DC7960068730A /* PBXContainerItemProxy */; }; - D0F7595E2C8DB24400126CF3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */; - }; - D0F759602C8DB24400126CF3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - productRef = D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */; - }; D0F7598A2C8DB34200126CF3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = D0F759892C8DB34200126CF3 /* GRPC */; @@ -921,16 +911,6 @@ package = D0B1D10E2C436152004B7823 /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; productName = AsyncAlgorithms; }; - D0F7595D2C8DB24400126CF3 /* GRPCSwiftPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; - productName = "plugin:GRPCSwiftPlugin"; - }; - D0F7595F2C8DB24400126CF3 /* SwiftProtobufPlugin */ = { - isa = XCSwiftPackageProductDependency; - package = D0D4E4852C8D8F29007F820A /* XCRemoteSwiftPackageReference "swift-protobuf" */; - productName = "plugin:SwiftProtobufPlugin"; - }; D0F7597D2C8DB30500126CF3 /* CGRPCZlib */ = { isa = XCSwiftPackageProductDependency; package = D0D4E4822C8D8EF6007F820A /* XCRemoteSwiftPackageReference "grpc-swift" */; diff --git a/Apple/Core/Client/Generated/burrow.grpc.swift b/Apple/Core/Client/Generated/burrow.grpc.swift new file mode 100644 index 0000000..d1f848c --- /dev/null +++ b/Apple/Core/Client/Generated/burrow.grpc.swift @@ -0,0 +1,761 @@ +// +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the protocol buffer compiler. +// Source: burrow.proto +// +import GRPC +import NIO +import NIOConcurrencyHelpers +import SwiftProtobuf + + +/// Usage: instantiate `Burrow_TunnelClient`, then call methods of this protocol to make API calls. +public protocol Burrow_TunnelClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? { get } + + func tunnelConfiguration( + _ request: Burrow_Empty, + callOptions: CallOptions?, + handler: @escaping (Burrow_TunnelConfigurationResponse) -> Void + ) -> ServerStreamingCall + + func tunnelStart( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> UnaryCall + + func tunnelStop( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> UnaryCall + + func tunnelStatus( + _ request: Burrow_Empty, + callOptions: CallOptions?, + handler: @escaping (Burrow_TunnelStatusResponse) -> Void + ) -> ServerStreamingCall +} + +extension Burrow_TunnelClientProtocol { + public var serviceName: String { + return "burrow.Tunnel" + } + + /// Server streaming call to TunnelConfiguration + /// + /// - Parameters: + /// - request: Request to send to TunnelConfiguration. + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. + public func tunnelConfiguration( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil, + handler: @escaping (Burrow_TunnelConfigurationResponse) -> Void + ) -> ServerStreamingCall { + return self.makeServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelConfiguration.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelConfigurationInterceptors() ?? [], + handler: handler + ) + } + + /// Unary call to TunnelStart + /// + /// - Parameters: + /// - request: Request to send to TunnelStart. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func tunnelStart( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStart.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStartInterceptors() ?? [] + ) + } + + /// Unary call to TunnelStop + /// + /// - Parameters: + /// - request: Request to send to TunnelStop. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func tunnelStop( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStop.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStopInterceptors() ?? [] + ) + } + + /// Server streaming call to TunnelStatus + /// + /// - Parameters: + /// - request: Request to send to TunnelStatus. + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. + public func tunnelStatus( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil, + handler: @escaping (Burrow_TunnelStatusResponse) -> Void + ) -> ServerStreamingCall { + return self.makeServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStatus.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStatusInterceptors() ?? [], + handler: handler + ) + } +} + +@available(*, deprecated) +extension Burrow_TunnelClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Burrow_TunnelNIOClient") +public final class Burrow_TunnelClient: Burrow_TunnelClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? + public let channel: GRPCChannel + public var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + public var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the burrow.Tunnel service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +public struct Burrow_TunnelNIOClient: Burrow_TunnelClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? + + /// Creates a client for the burrow.Tunnel service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol Burrow_TunnelAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? { get } + + func makeTunnelConfigurationCall( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> GRPCAsyncServerStreamingCall + + func makeTunnelStartCall( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeTunnelStopCall( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeTunnelStatusCall( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> GRPCAsyncServerStreamingCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Burrow_TunnelAsyncClientProtocol { + public static var serviceDescriptor: GRPCServiceDescriptor { + return Burrow_TunnelClientMetadata.serviceDescriptor + } + + public var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? { + return nil + } + + public func makeTunnelConfigurationCall( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncServerStreamingCall { + return self.makeAsyncServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelConfiguration.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelConfigurationInterceptors() ?? [] + ) + } + + public func makeTunnelStartCall( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStart.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStartInterceptors() ?? [] + ) + } + + public func makeTunnelStopCall( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStop.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStopInterceptors() ?? [] + ) + } + + public func makeTunnelStatusCall( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncServerStreamingCall { + return self.makeAsyncServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStatus.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStatusInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Burrow_TunnelAsyncClientProtocol { + public func tunnelConfiguration( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream { + return self.performAsyncServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelConfiguration.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelConfigurationInterceptors() ?? [] + ) + } + + public func tunnelStart( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) async throws -> Burrow_Empty { + return try await self.performAsyncUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStart.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStartInterceptors() ?? [] + ) + } + + public func tunnelStop( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) async throws -> Burrow_Empty { + return try await self.performAsyncUnaryCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStop.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStopInterceptors() ?? [] + ) + } + + public func tunnelStatus( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream { + return self.performAsyncServerStreamingCall( + path: Burrow_TunnelClientMetadata.Methods.tunnelStatus.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeTunnelStatusInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct Burrow_TunnelAsyncClient: Burrow_TunnelAsyncClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? + + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_TunnelClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +public protocol Burrow_TunnelClientInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when invoking 'tunnelConfiguration'. + func makeTunnelConfigurationInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'tunnelStart'. + func makeTunnelStartInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'tunnelStop'. + func makeTunnelStopInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'tunnelStatus'. + func makeTunnelStatusInterceptors() -> [ClientInterceptor] +} + +public enum Burrow_TunnelClientMetadata { + public static let serviceDescriptor = GRPCServiceDescriptor( + name: "Tunnel", + fullName: "burrow.Tunnel", + methods: [ + Burrow_TunnelClientMetadata.Methods.tunnelConfiguration, + Burrow_TunnelClientMetadata.Methods.tunnelStart, + Burrow_TunnelClientMetadata.Methods.tunnelStop, + Burrow_TunnelClientMetadata.Methods.tunnelStatus, + ] + ) + + public enum Methods { + public static let tunnelConfiguration = GRPCMethodDescriptor( + name: "TunnelConfiguration", + path: "/burrow.Tunnel/TunnelConfiguration", + type: GRPCCallType.serverStreaming + ) + + public static let tunnelStart = GRPCMethodDescriptor( + name: "TunnelStart", + path: "/burrow.Tunnel/TunnelStart", + type: GRPCCallType.unary + ) + + public static let tunnelStop = GRPCMethodDescriptor( + name: "TunnelStop", + path: "/burrow.Tunnel/TunnelStop", + type: GRPCCallType.unary + ) + + public static let tunnelStatus = GRPCMethodDescriptor( + name: "TunnelStatus", + path: "/burrow.Tunnel/TunnelStatus", + type: GRPCCallType.serverStreaming + ) + } +} + +/// Usage: instantiate `Burrow_NetworksClient`, then call methods of this protocol to make API calls. +public protocol Burrow_NetworksClientProtocol: GRPCClient { + var serviceName: String { get } + var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? { get } + + func networkAdd( + _ request: Burrow_Network, + callOptions: CallOptions? + ) -> UnaryCall + + func networkList( + _ request: Burrow_Empty, + callOptions: CallOptions?, + handler: @escaping (Burrow_NetworkListResponse) -> Void + ) -> ServerStreamingCall + + func networkReorder( + _ request: Burrow_NetworkReorderRequest, + callOptions: CallOptions? + ) -> UnaryCall + + func networkDelete( + _ request: Burrow_NetworkDeleteRequest, + callOptions: CallOptions? + ) -> UnaryCall +} + +extension Burrow_NetworksClientProtocol { + public var serviceName: String { + return "burrow.Networks" + } + + /// Unary call to NetworkAdd + /// + /// - Parameters: + /// - request: Request to send to NetworkAdd. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func networkAdd( + _ request: Burrow_Network, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkAdd.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkAddInterceptors() ?? [] + ) + } + + /// Server streaming call to NetworkList + /// + /// - Parameters: + /// - request: Request to send to NetworkList. + /// - callOptions: Call options. + /// - handler: A closure called when each response is received from the server. + /// - Returns: A `ServerStreamingCall` with futures for the metadata and status. + public func networkList( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil, + handler: @escaping (Burrow_NetworkListResponse) -> Void + ) -> ServerStreamingCall { + return self.makeServerStreamingCall( + path: Burrow_NetworksClientMetadata.Methods.networkList.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkListInterceptors() ?? [], + handler: handler + ) + } + + /// Unary call to NetworkReorder + /// + /// - Parameters: + /// - request: Request to send to NetworkReorder. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func networkReorder( + _ request: Burrow_NetworkReorderRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkReorder.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkReorderInterceptors() ?? [] + ) + } + + /// Unary call to NetworkDelete + /// + /// - Parameters: + /// - request: Request to send to NetworkDelete. + /// - callOptions: Call options. + /// - Returns: A `UnaryCall` with futures for the metadata, status and response. + public func networkDelete( + _ request: Burrow_NetworkDeleteRequest, + callOptions: CallOptions? = nil + ) -> UnaryCall { + return self.makeUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkDelete.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkDeleteInterceptors() ?? [] + ) + } +} + +@available(*, deprecated) +extension Burrow_NetworksClient: @unchecked Sendable {} + +@available(*, deprecated, renamed: "Burrow_NetworksNIOClient") +public final class Burrow_NetworksClient: Burrow_NetworksClientProtocol { + private let lock = Lock() + private var _defaultCallOptions: CallOptions + private var _interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? + public let channel: GRPCChannel + public var defaultCallOptions: CallOptions { + get { self.lock.withLock { return self._defaultCallOptions } } + set { self.lock.withLockVoid { self._defaultCallOptions = newValue } } + } + public var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? { + get { self.lock.withLock { return self._interceptors } } + set { self.lock.withLockVoid { self._interceptors = newValue } } + } + + /// Creates a client for the burrow.Networks service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self._defaultCallOptions = defaultCallOptions + self._interceptors = interceptors + } +} + +public struct Burrow_NetworksNIOClient: Burrow_NetworksClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? + + /// Creates a client for the burrow.Networks service. + /// + /// - Parameters: + /// - channel: `GRPCChannel` to the service host. + /// - defaultCallOptions: Options to use for each service call if the user doesn't provide them. + /// - interceptors: A factory providing interceptors for each RPC. + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public protocol Burrow_NetworksAsyncClientProtocol: GRPCClient { + static var serviceDescriptor: GRPCServiceDescriptor { get } + var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? { get } + + func makeNetworkAddCall( + _ request: Burrow_Network, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeNetworkListCall( + _ request: Burrow_Empty, + callOptions: CallOptions? + ) -> GRPCAsyncServerStreamingCall + + func makeNetworkReorderCall( + _ request: Burrow_NetworkReorderRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall + + func makeNetworkDeleteCall( + _ request: Burrow_NetworkDeleteRequest, + callOptions: CallOptions? + ) -> GRPCAsyncUnaryCall +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Burrow_NetworksAsyncClientProtocol { + public static var serviceDescriptor: GRPCServiceDescriptor { + return Burrow_NetworksClientMetadata.serviceDescriptor + } + + public var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? { + return nil + } + + public func makeNetworkAddCall( + _ request: Burrow_Network, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkAdd.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkAddInterceptors() ?? [] + ) + } + + public func makeNetworkListCall( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncServerStreamingCall { + return self.makeAsyncServerStreamingCall( + path: Burrow_NetworksClientMetadata.Methods.networkList.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkListInterceptors() ?? [] + ) + } + + public func makeNetworkReorderCall( + _ request: Burrow_NetworkReorderRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkReorder.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkReorderInterceptors() ?? [] + ) + } + + public func makeNetworkDeleteCall( + _ request: Burrow_NetworkDeleteRequest, + callOptions: CallOptions? = nil + ) -> GRPCAsyncUnaryCall { + return self.makeAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkDelete.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkDeleteInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Burrow_NetworksAsyncClientProtocol { + public func networkAdd( + _ request: Burrow_Network, + callOptions: CallOptions? = nil + ) async throws -> Burrow_Empty { + return try await self.performAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkAdd.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkAddInterceptors() ?? [] + ) + } + + public func networkList( + _ request: Burrow_Empty, + callOptions: CallOptions? = nil + ) -> GRPCAsyncResponseStream { + return self.performAsyncServerStreamingCall( + path: Burrow_NetworksClientMetadata.Methods.networkList.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkListInterceptors() ?? [] + ) + } + + public func networkReorder( + _ request: Burrow_NetworkReorderRequest, + callOptions: CallOptions? = nil + ) async throws -> Burrow_Empty { + return try await self.performAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkReorder.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkReorderInterceptors() ?? [] + ) + } + + public func networkDelete( + _ request: Burrow_NetworkDeleteRequest, + callOptions: CallOptions? = nil + ) async throws -> Burrow_Empty { + return try await self.performAsyncUnaryCall( + path: Burrow_NetworksClientMetadata.Methods.networkDelete.path, + request: request, + callOptions: callOptions ?? self.defaultCallOptions, + interceptors: self.interceptors?.makeNetworkDeleteInterceptors() ?? [] + ) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct Burrow_NetworksAsyncClient: Burrow_NetworksAsyncClientProtocol { + public var channel: GRPCChannel + public var defaultCallOptions: CallOptions + public var interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? + + public init( + channel: GRPCChannel, + defaultCallOptions: CallOptions = CallOptions(), + interceptors: Burrow_NetworksClientInterceptorFactoryProtocol? = nil + ) { + self.channel = channel + self.defaultCallOptions = defaultCallOptions + self.interceptors = interceptors + } +} + +public protocol Burrow_NetworksClientInterceptorFactoryProtocol: Sendable { + + /// - Returns: Interceptors to use when invoking 'networkAdd'. + func makeNetworkAddInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'networkList'. + func makeNetworkListInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'networkReorder'. + func makeNetworkReorderInterceptors() -> [ClientInterceptor] + + /// - Returns: Interceptors to use when invoking 'networkDelete'. + func makeNetworkDeleteInterceptors() -> [ClientInterceptor] +} + +public enum Burrow_NetworksClientMetadata { + public static let serviceDescriptor = GRPCServiceDescriptor( + name: "Networks", + fullName: "burrow.Networks", + methods: [ + Burrow_NetworksClientMetadata.Methods.networkAdd, + Burrow_NetworksClientMetadata.Methods.networkList, + Burrow_NetworksClientMetadata.Methods.networkReorder, + Burrow_NetworksClientMetadata.Methods.networkDelete, + ] + ) + + public enum Methods { + public static let networkAdd = GRPCMethodDescriptor( + name: "NetworkAdd", + path: "/burrow.Networks/NetworkAdd", + type: GRPCCallType.unary + ) + + public static let networkList = GRPCMethodDescriptor( + name: "NetworkList", + path: "/burrow.Networks/NetworkList", + type: GRPCCallType.serverStreaming + ) + + public static let networkReorder = GRPCMethodDescriptor( + name: "NetworkReorder", + path: "/burrow.Networks/NetworkReorder", + type: GRPCCallType.unary + ) + + public static let networkDelete = GRPCMethodDescriptor( + name: "NetworkDelete", + path: "/burrow.Networks/NetworkDelete", + type: GRPCCallType.unary + ) + } +} + diff --git a/Apple/Core/Client/Generated/burrow.pb.swift b/Apple/Core/Client/Generated/burrow.pb.swift new file mode 100644 index 0000000..bba0f16 --- /dev/null +++ b/Apple/Core/Client/Generated/burrow.pb.swift @@ -0,0 +1,566 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// swiftlint:disable all +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: burrow.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +public enum Burrow_NetworkType: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case wireGuard // = 0 + case tailnet // = 1 + case UNRECOGNIZED(Int) + + public init() { + self = .wireGuard + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .wireGuard + case 1: self = .tailnet + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .wireGuard: return 0 + case .tailnet: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Burrow_NetworkType] = [ + .wireGuard, + .tailnet, + ] + +} + +public enum Burrow_State: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case stopped // = 0 + case running // = 1 + case UNRECOGNIZED(Int) + + public init() { + self = .stopped + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .stopped + case 1: self = .running + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .stopped: return 0 + case .running: return 1 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Burrow_State] = [ + .stopped, + .running, + ] + +} + +public struct Burrow_NetworkReorderRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var id: Int32 = 0 + + public var index: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_WireGuardPeer: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var endpoint: String = String() + + public var subnet: [String] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_WireGuardNetwork: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var address: String = String() + + public var dns: String = String() + + public var peer: [Burrow_WireGuardPeer] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_NetworkDeleteRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var id: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_Network: @unchecked Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var id: Int32 = 0 + + public var type: Burrow_NetworkType = .wireGuard + + public var payload: Data = Data() + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_NetworkListResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var network: [Burrow_Network] = [] + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_Empty: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +public struct Burrow_TunnelStatusResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var state: Burrow_State = .stopped + + public var start: SwiftProtobuf.Google_Protobuf_Timestamp { + get {return _start ?? SwiftProtobuf.Google_Protobuf_Timestamp()} + set {_start = newValue} + } + /// Returns true if `start` has been explicitly set. + public var hasStart: Bool {return self._start != nil} + /// Clears the value of `start`. Subsequent reads from it will return its default value. + public mutating func clearStart() {self._start = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _start: SwiftProtobuf.Google_Protobuf_Timestamp? = nil +} + +public struct Burrow_TunnelConfigurationResponse: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var addresses: [String] = [] + + public var mtu: Int32 = 0 + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "burrow" + +extension Burrow_NetworkType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "WireGuard"), + 1: .same(proto: "Tailnet"), + ] +} + +extension Burrow_State: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "Stopped"), + 1: .same(proto: "Running"), + ] +} + +extension Burrow_NetworkReorderRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".NetworkReorderRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "index"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.id) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.index) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.id != 0 { + try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) + } + if self.index != 0 { + try visitor.visitSingularInt32Field(value: self.index, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_NetworkReorderRequest, rhs: Burrow_NetworkReorderRequest) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.index != rhs.index {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_WireGuardPeer: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".WireGuardPeer" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "endpoint"), + 2: .same(proto: "subnet"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.endpoint) }() + case 2: try { try decoder.decodeRepeatedStringField(value: &self.subnet) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.endpoint.isEmpty { + try visitor.visitSingularStringField(value: self.endpoint, fieldNumber: 1) + } + if !self.subnet.isEmpty { + try visitor.visitRepeatedStringField(value: self.subnet, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_WireGuardPeer, rhs: Burrow_WireGuardPeer) -> Bool { + if lhs.endpoint != rhs.endpoint {return false} + if lhs.subnet != rhs.subnet {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_WireGuardNetwork: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".WireGuardNetwork" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "address"), + 2: .same(proto: "dns"), + 3: .same(proto: "peer"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularStringField(value: &self.address) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.dns) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.peer) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.address.isEmpty { + try visitor.visitSingularStringField(value: self.address, fieldNumber: 1) + } + if !self.dns.isEmpty { + try visitor.visitSingularStringField(value: self.dns, fieldNumber: 2) + } + if !self.peer.isEmpty { + try visitor.visitRepeatedMessageField(value: self.peer, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_WireGuardNetwork, rhs: Burrow_WireGuardNetwork) -> Bool { + if lhs.address != rhs.address {return false} + if lhs.dns != rhs.dns {return false} + if lhs.peer != rhs.peer {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_NetworkDeleteRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".NetworkDeleteRequest" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.id) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.id != 0 { + try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_NetworkDeleteRequest, rhs: Burrow_NetworkDeleteRequest) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_Network: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Network" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "id"), + 2: .same(proto: "type"), + 3: .same(proto: "payload"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularInt32Field(value: &self.id) }() + case 2: try { try decoder.decodeSingularEnumField(value: &self.type) }() + case 3: try { try decoder.decodeSingularBytesField(value: &self.payload) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if self.id != 0 { + try visitor.visitSingularInt32Field(value: self.id, fieldNumber: 1) + } + if self.type != .wireGuard { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 2) + } + if !self.payload.isEmpty { + try visitor.visitSingularBytesField(value: self.payload, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_Network, rhs: Burrow_Network) -> Bool { + if lhs.id != rhs.id {return false} + if lhs.type != rhs.type {return false} + if lhs.payload != rhs.payload {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_NetworkListResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".NetworkListResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "network"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.network) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.network.isEmpty { + try visitor.visitRepeatedMessageField(value: self.network, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_NetworkListResponse, rhs: Burrow_NetworkListResponse) -> Bool { + if lhs.network != rhs.network {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_Empty: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Empty" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_Empty, rhs: Burrow_Empty) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_TunnelStatusResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".TunnelStatusResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "state"), + 2: .same(proto: "start"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.state) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._start) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.state != .stopped { + try visitor.visitSingularEnumField(value: self.state, fieldNumber: 1) + } + try { if let v = self._start { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Burrow_TunnelStatusResponse, rhs: Burrow_TunnelStatusResponse) -> Bool { + if lhs.state != rhs.state {return false} + if lhs._start != rhs._start {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Burrow_TunnelConfigurationResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".TunnelConfigurationResponse" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "addresses"), + 2: .same(proto: "mtu"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedStringField(value: &self.addresses) }() + case 2: try { try decoder.decodeSingularInt32Field(value: &self.mtu) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + if !self.addresses.isEmpty { + try visitor.visitRepeatedStringField(value: self.addresses, fieldNumber: 1) + } + if self.mtu != 0 { + try visitor.visitSingularInt32Field(value: self.mtu, fieldNumber: 2) + } + 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.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Apple/Core/Client/grpc-swift-config.json b/Apple/Core/Client/grpc-swift-config.json deleted file mode 100644 index 2d89698..0000000 --- a/Apple/Core/Client/grpc-swift-config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "invocations": [ - { - "protoFiles": [ - "burrow.proto", - ], - "server": false, - "visibility": "public" - } - ] -} diff --git a/Apple/Core/Client/swift-protobuf-config.json b/Apple/Core/Client/swift-protobuf-config.json deleted file mode 100644 index 87aaec3..0000000 --- a/Apple/Core/Client/swift-protobuf-config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "invocations": [ - { - "protoFiles": [ - "burrow.proto", - ], - "visibility": "public" - } - ] -} diff --git a/Apple/UI/BurrowView.swift b/Apple/UI/BurrowView.swift index 835510d..b4fa7d8 100644 --- a/Apple/UI/BurrowView.swift +++ b/Apple/UI/BurrowView.swift @@ -284,6 +284,7 @@ private struct AccountDraft { var identityName = "" var wireGuardConfig = "" + var discoveryEmail = "" var tailnetProvider: TailnetProvider = .tailscale var authority = "" var tailnet = "" @@ -327,6 +328,9 @@ private struct ConfigurationSheetView: View { @State private var errorMessage: String? @State private var loginSessionID: String? @State private var loginStatus: TailnetLoginStatus? + @State private var discoveryStatus: TailnetDiscoveryResponse? + @State private var discoveryError: String? + @State private var isDiscoveringTailnet = false @State private var authorityProbeStatus: TailnetAuthorityProbeStatus? @State private var authorityProbeError: String? @State private var isProbingAuthority = false @@ -449,6 +453,9 @@ private struct ConfigurationSheetView: View { .onChange(of: draft.authority) { _, _ in resetAuthorityProbe() } + .onChange(of: draft.discoveryEmail) { _, _ in + resetTailnetDiscoveryFeedback() + } .onDisappear { pollingTask?.cancel() webAuthenticationTask?.cancel() @@ -459,7 +466,37 @@ private struct ConfigurationSheetView: View { @ViewBuilder private var tailnetSections: some View { Section("Connection") { - Picker("Provider", selection: $draft.tailnetProvider) { + TextField("Email address", text: $draft.discoveryEmail) + .textInputAutocapitalization(.never) + .keyboardType(.emailAddress) + .burrowLoginField() + .autocorrectionDisabled() + + 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) + + if let discoveryStatus { + tailnetDiscoveryCard(status: discoveryStatus, failure: nil) + } else if let discoveryError { + tailnetDiscoveryCard(status: nil, failure: discoveryError) + } + + Picker( + "Provider", + selection: Binding( + get: { draft.tailnetProvider }, + set: { applyTailnetProvider($0) } + ) + ) { ForEach(TailnetProvider.allCases) { provider in Text(provider.title).tag(provider) } @@ -503,14 +540,14 @@ private struct ConfigurationSheetView: View { } Section("Authentication") { - if draft.tailnetProvider.usesWebLogin { + if tailnetUsesWebLogin { tailnetWebLoginCard } else { TextField("Username", text: $draft.username) .burrowLoginField() .autocorrectionDisabled() Picker("Authentication", selection: $draft.authMode) { - ForEach([AccountAuthMode.none, .password, .preauthKey]) { mode in + ForEach(availableTailnetAuthModes) { mode in Text(mode.title).tag(mode) } } @@ -583,7 +620,7 @@ private struct ConfigurationSheetView: View { HStack(spacing: 8) { summaryBadge(draft.tailnetProvider.title) summaryBadge( - draft.tailnetProvider.usesWebLogin ? "Web Sign-In" : draft.authMode.title + tailnetUsesWebLogin ? "Web Sign-In" : draft.authMode.title ) } } @@ -656,7 +693,7 @@ private struct ConfigurationSheetView: View { .foregroundStyle(.secondary) } } else { - Text("Burrow launches the local bridge, then opens the real Tailscale sign-in page in-app.") + Text("Burrow launches the local bridge, then opens the real provider sign-in page in-app.") .font(.footnote) .foregroundStyle(.secondary) } @@ -696,6 +733,41 @@ private struct ConfigurationSheetView: View { ) } + private func tailnetDiscoveryCard( + status: TailnetDiscoveryResponse?, + failure: String? + ) -> some View { + VStack(alignment: .leading, spacing: 6) { + if let status { + Text("Discovered \(status.provider.title)") + .font(.subheadline.weight(.medium)) + Text(status.authority) + .font(.footnote.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + if let oidcIssuer = status.oidcIssuer { + Text("OIDC: \(oidcIssuer)") + .font(.footnote) + .foregroundStyle(.secondary) + .lineLimit(3) + .textSelection(.enabled) + } + } else if let failure { + Text("Discovery failed") + .font(.subheadline.weight(.medium)) + .foregroundStyle(.red) + Text(failure) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(.thinMaterial) + ) + } + private func summaryBadge(_ label: String) -> some View { Text(label) .font(.caption.weight(.medium)) @@ -762,12 +834,12 @@ private struct ConfigurationSheetView: View { } } - if !draft.tailnetProvider.usesWebLogin { + if availableTailnetAuthModes.count > 1 { Menu("Authentication") { - ForEach([AccountAuthMode.none, .password, .preauthKey]) { mode in + ForEach(availableTailnetAuthModes) { mode in Button(mode.title) { draft.authMode = mode - if mode == .none { + if mode == .none || mode == .web { draft.secret = "" } } @@ -848,7 +920,7 @@ private struct ConfigurationSheetView: View { case .tor: return "Save Account" case .tailnet: - if draft.tailnetProvider.usesWebLogin { + if tailnetUsesWebLogin { return loginStatus?.running == true ? "Save Account" : "Start Sign-In" } return "Save Account" @@ -865,12 +937,12 @@ private struct ConfigurationSheetView: View { if normalizedOptional(draft.accountName) == nil || normalizedOptional(draft.identityName) == nil { return true } - if draft.tailnetProvider.usesWebLogin { - return false - } if draft.tailnetProvider.requiresControlURL && normalizedOptional(draft.authority) == nil { return true } + if tailnetUsesWebLogin { + return false + } if draft.authMode != .none && normalizedOptional(draft.secret) == nil { return true } @@ -955,14 +1027,14 @@ private struct ConfigurationSheetView: View { } private func submitTailnet() async throws { - if draft.tailnetProvider.usesWebLogin { + if tailnetUsesWebLogin { if loginStatus?.running == true { webAuthenticationTask?.cancel() webAuthenticationTask = nil try await saveTailnetAccount(secret: nil, username: nil) dismiss() } else { - try await startTailscaleLogin() + try await startTailnetLogin() } return } @@ -973,13 +1045,13 @@ private struct ConfigurationSheetView: View { dismiss() } - private func startTailscaleLogin() async throws { + private func startTailnetLogin() async throws { let response = try await TailnetBridgeClient.startLogin( TailnetLoginStartRequest( accountName: normalized(draft.accountName, fallback: "default"), identityName: normalized(draft.identityName, fallback: "apple"), hostname: normalizedOptional(draft.hostname), - controlURL: draft.tailnetProvider.defaultAuthority + controlURL: normalizedOptional(draft.authority) ?? draft.tailnetProvider.defaultAuthority ) ) loginSessionID = response.sessionID @@ -1010,7 +1082,7 @@ private struct ConfigurationSheetView: View { case .tailnetLogin: draft.tailnetProvider = .tailscale do { - try await startTailscaleLogin() + try await startTailnetLogin() } catch { errorMessage = error.localizedDescription } @@ -1078,14 +1150,14 @@ private struct ConfigurationSheetView: View { let provider = draft.tailnetProvider let title = titleOrFallback( hostnameFallback( - from: provider.usesWebLogin ? (loginStatus?.tailnetName ?? "") : draft.authority, + from: tailnetUsesWebLogin ? (loginStatus?.tailnetName ?? "") : draft.authority, fallback: provider.title ) ) let payload = TailnetNetworkPayload( provider: provider, - authority: normalizedOptional(provider.defaultAuthority ?? draft.authority), + authority: normalizedOptional(draft.authority) ?? normalizedOptional(provider.defaultAuthority ?? ""), account: normalized(draft.accountName, fallback: "default"), identity: normalized(draft.identityName, fallback: "apple"), tailnet: normalizedOptional(loginStatus?.tailnetName ?? draft.tailnet), @@ -1094,7 +1166,7 @@ private struct ConfigurationSheetView: View { var noteParts: [String] = [ provider.title, - provider.usesWebLogin + tailnetUsesWebLogin ? "State: \(loginStatus?.backendState ?? "NeedsLogin")" : "Auth: \(draft.authMode.title)", ] @@ -1123,7 +1195,7 @@ private struct ConfigurationSheetView: View { hostname: payload.hostname, username: username, tailnet: payload.tailnet, - authMode: provider.usesWebLogin ? .web : draft.authMode, + authMode: tailnetUsesWebLogin ? .web : draft.authMode, note: noteParts.joined(separator: " • "), createdAt: .now, updatedAt: .now @@ -1155,18 +1227,25 @@ private struct ConfigurationSheetView: View { } private func applyTailnetProvider(_ provider: TailnetProvider) { + resetTailnetDiscoveryFeedback() draft.tailnetProvider = provider applyTailnetDefaults(for: provider) } private func applyTailnetDefaults(for provider: TailnetProvider) { draft.authority = provider.defaultAuthority ?? "" - if provider.usesWebLogin { + loginStatus = nil + loginSessionID = nil + pollingTask?.cancel() + if provider == .tailscale { draft.authMode = .web draft.username = "" draft.secret = "" } else { - if draft.authMode == .web { + if !availableTailnetAuthModes.contains(draft.authMode) { + draft.authMode = provider.supportsWebLogin ? .web : .none + } + if draft.authMode == .web && !provider.supportsWebLogin { draft.authMode = .none } } @@ -1202,6 +1281,41 @@ private struct ConfigurationSheetView: View { authorityProbeError = nil } + private func resetTailnetDiscoveryFeedback() { + discoveryStatus = nil + discoveryError = nil + } + + private func discoverTailnetAuthority() { + guard let email = normalizedOptional(draft.discoveryEmail) else { + discoveryStatus = nil + discoveryError = "Enter an email address first." + return + } + + isDiscoveringTailnet = true + discoveryStatus = nil + discoveryError = nil + + Task { @MainActor in + defer { isDiscoveringTailnet = false } + do { + let discovery = try await TailnetDiscoveryClient.discover(email: email) + discoveryStatus = discovery + draft.tailnetProvider = discovery.provider + draft.authority = discovery.authority + if discovery.provider.supportsWebLogin, discovery.oidcIssuer != nil { + draft.authMode = .web + draft.username = "" + draft.secret = "" + } + probeTailnetAuthority() + } catch { + discoveryError = error.localizedDescription + } + } + } + private func pasteWireGuardConfiguration() { guard let clipboardString else { return } draft.wireGuardConfig = clipboardString @@ -1247,6 +1361,21 @@ private struct ConfigurationSheetView: View { return host } + private var tailnetUsesWebLogin: Bool { + draft.authMode == .web && draft.tailnetProvider.supportsWebLogin + } + + private var availableTailnetAuthModes: [AccountAuthMode] { + switch draft.tailnetProvider { + case .tailscale: + [.web] + case .headscale: + [.web, .none, .password, .preauthKey] + case .burrow: + [.none, .password, .preauthKey] + } + } + @ViewBuilder private func labeledValue(_ label: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: 2) { diff --git a/Apple/UI/Networks/Network.swift b/Apple/UI/Networks/Network.swift index 71e5bca..9a534ce 100644 --- a/Apple/UI/Networks/Network.swift +++ b/Apple/UI/Networks/Network.swift @@ -33,6 +33,13 @@ struct TailnetLoginStartRequest: Codable, Sendable { var controlURL: String? } +struct TailnetDiscoveryResponse: Codable, Sendable { + var domain: String + var provider: TailnetProvider + var authority: String + var oidcIssuer: String? +} + struct TailnetLoginStatus: Codable, Sendable { var backendState: String var authURL: String? @@ -91,7 +98,7 @@ enum TailnetBridgeClient { return try decoder.decode(TailnetLoginStatus.self, from: data) } - private static func validate(response: URLResponse, data: Data) throws { + fileprivate static func validate(response: URLResponse, data: Data) throws { guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } @@ -104,6 +111,32 @@ enum TailnetBridgeClient { } } +enum TailnetDiscoveryClient { + private static let baseURL = URL(string: "http://127.0.0.1:8080")! + + static func discover(email: String) async throws -> TailnetDiscoveryResponse { + guard var components = URLComponents( + url: baseURL.appendingPathComponent("v1/tailnet/discover"), + resolvingAgainstBaseURL: false + ) else { + throw URLError(.badURL) + } + components.queryItems = [ + URLQueryItem(name: "email", value: email) + ] + guard let url = components.url else { + throw URLError(.badURL) + } + + let (data, response) = try await URLSession.shared.data(from: url) + try TailnetBridgeClient.validate(response: response, data: data) + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(TailnetDiscoveryResponse.self, from: data) + } +} + enum TailnetAuthorityProbeClient { static func probe(provider: TailnetProvider, authority: String) async throws -> TailnetAuthorityProbeStatus { let normalizedAuthority = normalizeAuthority(authority) @@ -308,8 +341,13 @@ enum TailnetProvider: String, CaseIterable, Codable, Identifiable, Sendable { } } - var usesWebLogin: Bool { - self == .tailscale + var supportsWebLogin: Bool { + switch self { + case .tailscale, .headscale: + true + case .burrow: + false + } } var requiresControlURL: Bool { @@ -332,7 +370,7 @@ enum TailnetProvider: String, CaseIterable, Codable, Identifiable, Sendable { case .tailscale: "Use Tailscale's real browser login flow." case .headscale: - "Store a Headscale control-plane endpoint and credentials." + "Use your Headscale control plane with browser or key-based sign-in." case .burrow: "Store Burrow control-plane credentials." } diff --git a/burrow/src/auth/server/mod.rs b/burrow/src/auth/server/mod.rs index b0c0522..fdffce3 100644 --- a/burrow/src/auth/server/mod.rs +++ b/burrow/src/auth/server/mod.rs @@ -5,17 +5,18 @@ use std::{env, path::Path}; use anyhow::{Context, Result}; use axum::{ - extract::{Json, Path as AxumPath, State}, + extract::{Json, Path as AxumPath, Query, State}, http::{header::AUTHORIZATION, HeaderMap, StatusCode}, response::IntoResponse, routing::{get, post}, Router, }; +use serde::Deserialize; use tokio::signal; use crate::control::{ - LocalAuthRequest, LocalAuthResponse, MapRequest, MapResponse, RegisterRequest, - RegisterResponse, BURROW_TAILNET_DOMAIN, + discovery, LocalAuthRequest, LocalAuthResponse, MapRequest, MapResponse, RegisterRequest, + RegisterResponse, TailnetDiscovery, BURROW_TAILNET_DOMAIN, }; #[derive(Clone, Debug)] @@ -105,6 +106,11 @@ struct AppState { tailscale: tailscale::TailscaleBridgeManager, } +#[derive(Debug, Deserialize)] +struct TailnetDiscoveryQuery { + email: String, +} + type AppResult = Result; pub async fn serve() -> Result<()> { @@ -139,6 +145,7 @@ pub fn build_router(config: AuthServerConfig) -> Router { .route("/v1/auth/login", post(login_local)) .route("/v1/control/register", post(control_register)) .route("/v1/control/map", post(control_map)) + .route("/v1/tailnet/discover", get(tailnet_discover)) .route("/v1/tailscale/login/start", post(tailscale_login_start)) .route("/v1/tailscale/login/:session_id", get(tailscale_login_status)) .with_state(AppState { @@ -205,6 +212,19 @@ async fn control_map( Ok(Json(response)) } +async fn tailnet_discover( + Query(query): Query, +) -> AppResult> { + if query.email.trim().is_empty() { + return Err((StatusCode::BAD_REQUEST, "email is required".to_owned())); + } + + let discovery = discovery::discover_tailnet(&query.email) + .await + .map_err(|err| (StatusCode::BAD_GATEWAY, err.to_string()))?; + Ok(Json(discovery)) +} + async fn tailscale_login_start( State(state): State, Json(request): Json, @@ -394,4 +414,17 @@ mod tests { assert!(map.dns.expect("dns").magic_dns); Ok(()) } + + #[tokio::test] + async fn tailnet_discover_requires_email() -> Result<()> { + let app = build_router(AuthServerConfig::default()); + let response = app + .oneshot( + Request::get("/v1/tailnet/discover?email=") + .body(Body::empty())?, + ) + .await?; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + Ok(()) + } } diff --git a/burrow/src/control/discovery.rs b/burrow/src/control/discovery.rs new file mode 100644 index 0000000..28b48bb --- /dev/null +++ b/burrow/src/control/discovery.rs @@ -0,0 +1,212 @@ +use anyhow::{anyhow, Context, Result}; +use reqwest::{Client, StatusCode, Url}; +use serde::{Deserialize, Serialize}; + +use super::TailnetProvider; + +pub const TAILNET_DISCOVERY_REL: &str = "https://burrow.net/rel/tailnet-control-server"; +const TAILNET_DISCOVERY_PATH: &str = "/.well-known/burrow-tailnet"; +const WEBFINGER_PATH: &str = "/.well-known/webfinger"; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TailnetDiscovery { + pub domain: String, + pub provider: TailnetProvider, + pub authority: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub oidc_issuer: Option, +} + +#[derive(Clone, Debug, Default, Deserialize)] +struct WebFingerDocument { + #[serde(default)] + links: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize)] +struct WebFingerLink { + #[serde(default)] + rel: String, + #[serde(default)] + href: Option, +} + +pub async fn discover_tailnet(email: &str) -> Result { + let domain = email_domain(email)?; + let base_url = Url::parse(&format!("https://{domain}")) + .with_context(|| format!("invalid discovery domain {domain}"))?; + let client = Client::builder() + .user_agent("burrow-tailnet-discovery") + .timeout(std::time::Duration::from_secs(10)) + .build() + .context("failed to build tailnet discovery client")?; + discover_tailnet_at(&client, email, &base_url).await +} + +pub async fn discover_tailnet_at( + client: &Client, + email: &str, + base_url: &Url, +) -> Result { + let domain = email_domain(email)?; + + if let Some(discovery) = discover_well_known(client, base_url).await? { + return Ok(TailnetDiscovery { domain, ..discovery }); + } + + if let Some(authority) = discover_webfinger(client, email, base_url).await? { + return Ok(TailnetDiscovery { + domain, + provider: TailnetProvider::Headscale, + authority, + oidc_issuer: None, + }); + } + + Err(anyhow!("no tailnet discovery metadata found for {domain}")) +} + +pub fn email_domain(email: &str) -> Result { + let trimmed = email.trim(); + let (_, domain) = trimmed + .rsplit_once('@') + .ok_or_else(|| anyhow!("email address must include a domain"))?; + let domain = domain.trim().trim_matches('.').to_ascii_lowercase(); + if domain.is_empty() { + return Err(anyhow!("email address must include a domain")); + } + Ok(domain) +} + +async fn discover_well_known(client: &Client, base_url: &Url) -> Result> { + let url = base_url + .join(TAILNET_DISCOVERY_PATH) + .context("failed to build tailnet discovery URL")?; + let response = client + .get(url) + .header("accept", "application/json") + .send() + .await + .context("tailnet well-known request failed")?; + + match response.status() { + StatusCode::OK => response + .json::() + .await + .context("invalid tailnet discovery document") + .map(Some), + StatusCode::NOT_FOUND => Ok(None), + status => Err(anyhow!("tailnet well-known lookup failed with HTTP {status}")), + } +} + +async fn discover_webfinger(client: &Client, email: &str, base_url: &Url) -> Result> { + let mut url = base_url + .join(WEBFINGER_PATH) + .context("failed to build webfinger URL")?; + url.query_pairs_mut() + .append_pair("resource", &format!("acct:{email}")) + .append_pair("rel", TAILNET_DISCOVERY_REL); + + let response = client + .get(url) + .header("accept", "application/jrd+json, application/json") + .send() + .await + .context("tailnet webfinger request failed")?; + + match response.status() { + StatusCode::OK => { + let document = response + .json::() + .await + .context("invalid webfinger document")?; + Ok(document + .links + .into_iter() + .find(|link| link.rel == TAILNET_DISCOVERY_REL) + .and_then(|link| link.href) + .filter(|href| !href.trim().is_empty())) + } + StatusCode::NOT_FOUND => Ok(None), + status => Err(anyhow!("tailnet webfinger lookup failed with HTTP {status}")), + } +} + +#[cfg(test)] +mod tests { + use axum::{routing::get, Router}; + use serde_json::json; + use tokio::net::TcpListener; + + use super::*; + + #[test] + fn extracts_domain_from_email() { + assert_eq!(email_domain("Contact@Burrow.net").unwrap(), "burrow.net"); + assert!(email_domain("contact").is_err()); + } + + #[tokio::test] + async fn discovers_from_well_known_document() -> Result<()> { + let router = Router::new().route( + TAILNET_DISCOVERY_PATH, + get(|| async { + axum::Json(json!({ + "domain": "burrow.net", + "provider": "headscale", + "authority": "https://ts.burrow.net", + "oidc_issuer": "https://auth.burrow.net/application/o/ts/" + })) + }), + ); + + let listener = TcpListener::bind("127.0.0.1:0").await?; + let base_url = Url::parse(&format!("http://{}", listener.local_addr()?))?; + let server = tokio::spawn(async move { axum::serve(listener, router).await }); + + let client = Client::builder().build()?; + let discovery = discover_tailnet_at(&client, "contact@burrow.net", &base_url).await?; + assert_eq!(discovery.provider, TailnetProvider::Headscale); + assert_eq!(discovery.authority, "https://ts.burrow.net"); + assert_eq!(discovery.domain, "burrow.net"); + + server.abort(); + Ok(()) + } + + #[tokio::test] + async fn falls_back_to_webfinger_authority() -> Result<()> { + let router = Router::new() + .route( + TAILNET_DISCOVERY_PATH, + get(|| async { (StatusCode::NOT_FOUND, "") }), + ) + .route( + WEBFINGER_PATH, + get(|| async { + axum::Json(json!({ + "subject": "acct:contact@burrow.net", + "links": [ + { + "rel": TAILNET_DISCOVERY_REL, + "href": "https://ts.burrow.net" + } + ] + })) + }), + ); + + let listener = TcpListener::bind("127.0.0.1:0").await?; + let base_url = Url::parse(&format!("http://{}", listener.local_addr()?))?; + let server = tokio::spawn(async move { axum::serve(listener, router).await }); + + let client = Client::builder().build()?; + let discovery = discover_tailnet_at(&client, "contact@burrow.net", &base_url).await?; + assert_eq!(discovery.provider, TailnetProvider::Headscale); + assert_eq!(discovery.authority, "https://ts.burrow.net"); + + server.abort(); + Ok(()) + } +} diff --git a/burrow/src/control/mod.rs b/burrow/src/control/mod.rs index 331a7d2..472f673 100644 --- a/burrow/src/control/mod.rs +++ b/burrow/src/control/mod.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod discovery; use std::collections::BTreeMap; @@ -6,6 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; pub use config::{TailnetConfig, TailnetProvider}; +pub use discovery::{TailnetDiscovery, TAILNET_DISCOVERY_REL}; pub const BURROW_CAPABILITY_VERSION: i32 = 1; pub const BURROW_TAILNET_DOMAIN: &str = "burrow.net"; diff --git a/nixos/modules/burrow-forge.nix b/nixos/modules/burrow-forge.nix index 51af7eb..0d0f5c8 100644 --- a/nixos/modules/burrow-forge.nix +++ b/nixos/modules/burrow-forge.nix @@ -259,9 +259,12 @@ in encode gzip zstd @oidcConfig path /.well-known/openid-configuration redir @oidcConfig https://${config.services.burrow.authentik.domain}/application/o/${config.services.burrow.authentik.forgejoProviderSlug}/.well-known/openid-configuration 308 + @tailnetConfig path /.well-known/burrow-tailnet + header @tailnetConfig Content-Type application/json + respond @tailnetConfig "{\"domain\":\"${cfg.siteDomain}\",\"provider\":\"headscale\",\"authority\":\"https://${config.services.burrow.headscale.domain}\",\"oidc_issuer\":\"https://${config.services.burrow.authentik.domain}/application/o/${config.services.burrow.authentik.headscaleProviderSlug}/\"}" 200 @webfinger path /.well-known/webfinger header @webfinger Content-Type application/jrd+json - respond @webfinger "{\"subject\":\"{query.resource}\",\"links\":[{\"rel\":\"http://openid.net/specs/connect/1.0/issuer\",\"href\":\"https://${config.services.burrow.authentik.domain}/application/o/${config.services.burrow.authentik.forgejoProviderSlug}/\"}]}" 200 + respond @webfinger "{\"subject\":\"{query.resource}\",\"links\":[{\"rel\":\"http://openid.net/specs/connect/1.0/issuer\",\"href\":\"https://${config.services.burrow.authentik.domain}/application/o/${config.services.burrow.authentik.forgejoProviderSlug}/\"},{\"rel\":\"https://burrow.net/rel/tailnet-control-server\",\"href\":\"https://${config.services.burrow.headscale.domain}\"}]}" 200 @root path / redir @root ${homeRepoUrl} 308 respond 404 diff --git a/nixos/modules/burrow-headscale.nix b/nixos/modules/burrow-headscale.nix index 98cf5ba..ad5ec68 100644 --- a/nixos/modules/burrow-headscale.nix +++ b/nixos/modules/burrow-headscale.nix @@ -3,131 +3,6 @@ let cfg = config.services.burrow.headscale; policyFile = ./burrow-headscale-policy.hujson; - landingPage = pkgs.writeTextDir "index.html" '' - - - - - - Burrow Tailnet - - - -
-
Burrow Tailnet
-
-

Sign-in starts from your client, not this page.

-

- ts.burrow.net is the Burrow Headscale control plane. Headscale does not provide a built-in web UI, - so browser authentication starts only after a Tailscale-compatible client initiates login. -

-
-
tailscale up --login-server https://ts.burrow.net
- -
- - - ''; in { options.services.burrow.headscale = { @@ -346,14 +221,7 @@ in services.caddy.virtualHosts."${cfg.domain}".extraConfig = '' encode gzip zstd - @root path / - handle @root { - root * ${landingPage} - file_server - } - handle { - reverse_proxy 127.0.0.1:${toString cfg.port} - } + reverse_proxy 127.0.0.1:${toString cfg.port} ''; }; }