8000 chore: add network extension manager · coder/coder-desktop-macos@300889e · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 300889e

Browse files
committed
chore: add network extension manager
1 parent 844df27 commit 300889e

File tree

12 files changed

+426
-53
lines changed
  • Coder Desktop
    • < 8000 div class="PRIVATE_TreeView-item-level-line prc-TreeView-TreeViewItemLevelLine-KPSSL">
  • Coder Desktop
  • Coder DesktopTests
  • VPN
  • VPNLib
  • VPNLibTests
  • 12 files changed

    +426
    -53
    lines changed

    Coder Desktop/.swiftlint.yml

    Lines changed: 2 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -8,3 +8,5 @@ type_name:
    88
    identifier_name:
    99
    allowed_symbols: "_"
    1010
    min_length: 1
    11+
    cyclomatic_complexity:
    12+
    warning: 15

    Coder Desktop/Coder Desktop/Preview Content/PreviewClient.swift

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -23,7 +23,7 @@ struct PreviewClient: Client {
    2323
    roles: []
    2424
    )
    2525
    } catch {
    26-
    throw ClientError.reqError(AFError.explicitlyCancelled)
    26+
    throw .reqError(.explicitlyCancelled)
    2727
    }
    2828
    }
    2929
    }

    Coder Desktop/Coder Desktop/SDK/Client.swift

    Lines changed: 4 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -39,7 +39,7 @@ struct CoderClient: Client {
    3939
    case let .success(data):
    4040
    return HTTPResponse(resp: out.response!, data: data, req: out.request)
    4141
    case let .failure(error):
    42-
    throw ClientError.reqError(error)
    42+
    throw .reqError(error)
    4343
    }
    4444
    }
    4545

    @@ -58,7 +58,7 @@ struct CoderClient: Client {
    5858
    case let .success(data):
    5959
    return HTTPResponse(resp: out.response!, data: data, req: out.request)
    6060
    case let .failure(error):
    61-
    throw ClientError.reqError(error)
    61+
    throw .reqError(error)
    6262
    }
    6363
    }
    6464

    @@ -71,9 +71,9 @@ struct CoderClient: Client {
    7171
    method: resp.req?.httpMethod,
    7272
    url: resp.req?.url
    7373
    )
    74-
    return ClientError.apiError(out)
    74+
    return .apiError(out)
    7575
    } catch {
    76-
    return ClientError.unexpectedResponse(resp.data[...1024])
    76+
    return .unexpectedResponse(resp.data[...1024])
    7777
    }
    7878
    }
    7979

    Coder Desktop/Coder Desktop/SDK/User.swift

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -9,7 +9,7 @@ extension CoderClient {
    99
    do {
    1010
    return try CoderClient.decoder.decode(User.self, from: res.data)
    1111
    } catch {
    12-
    throw ClientError.unexpectedResponse(res.data[...1024])
    12+
    throw .unexpectedResponse(res.data[...1024])
    1313
    }
    1414
    }
    1515
    }

    Coder Desktop/Coder DesktopTests/Util.swift

    Lines changed: 1 addition & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -68,7 +68,7 @@ struct MockClient: Client {
    6868
    struct MockErrorClient: Client {
    6969
    init(url _: URL, token _: String?) {}
    7070
    func user(_: String) async throws(ClientError) -> Coder_Desktop.User {
    71-
    throw ClientError.reqError(.explicitlyCancelled)
    71+
    throw .reqError(.explicitlyCancelled)
    7272
    }
    7373
    }
    7474

    Coder Desktop/VPN/Manager.swift

    Lines changed: 187 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -4,16 +4,200 @@ import VPNLib
    44

    55
    actor Manager {
    66
    let ptp: PacketTunnelProvider
    7+
    let cfg: ManagerConfig
    78

    8-
    var tunnelHandle: TunnelHandle?
    9-
    var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>?
    9+
    let tunnelHandle: TunnelHandle
    10+
    let speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>
    11+
    var readLoop: Task<Void, any Error>!
    1012
    // TODO: XPC Speaker
    1113

    1214
    private let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    1315
    .first!.appending(path: "coder-vpn.dylib")
    1416
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "manager")
    1517

    16-
    init(with: PacketTunnelProvider) {
    18+
    init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) {
    1719
    ptp = with
    20+
    self.cfg = cfg
    21+
    #if arch(arm64)
    22+
    let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-arm64.dylib")
    23+
    #elseif arch(x86_64)
    24+
    let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-amd64.dylib")
    25+
    #else
    26+
    fatalError("unknown architecture")
    27+
    #endif
    28+
    do {
    29+
    try await download(src: dylibPath, dest: dest)
    30+
    } catch {
    31+
    throw .download(error)
    32+
    }
    33+
    do throws(ValidationError) {
    34+
    try SignatureValidator.validate(path: dest)
    35+
    } catch {
    36+
    throw .validation(error)
    37+
    }
    38+
    do {
    39+
    try tunnelHandle = TunnelHandle(dylibPath: dest)
    40+
    } catch {
    41+
    throw .tunnelSetup(error)
    42+
    }
    43+
    speaker = await Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>(
    44+
    writeFD: tunnelHandle.writeHandle,
    45+
    readFD: tunnelHandle.readHandle
    46+
    )
    47+
    do throws(HandshakeError) {
    48+
    try await speaker.handshake()
    49+
    } catch {
    50+
    throw .handshake(error)
    51+
    }
    52+
    readLoop = Task { try await run() }
    1853
    }
    54+
    55+
    func run() async throws {
    56+
    do {
    57+
    for try await m in speaker {
    58+
    switch m {
    59+
    case let .message(msg):
    60+
    handleMessage(msg)
    61+
    case let .RPC(rpc):
    62+
    handleRPC(rpc)
    63+
    }
    64+
    }
    65+
    } catch {
    66+
    logger.error("tunnel read loop failed: \(error)")
    67+
    try await tunnelHandle.close()
    68+
    // TODO: Notify app over XPC
    69+
    return
    70+
    }
    71+
    logger.info("tunnel read loop exited")
    72+
    try await tunnelHandle.close()
    73+
    // TODO: Notify app over XPC
    74+
    }
    75+
    76+
    func handleMessage(_ msg: Vpn_TunnelMessage) {
    77+
    guard let msgType = msg.msg else {
    78+
    logger.critical("received message with no type")
    79+
    return
    80+
    }
    81+
    switch msgType {
    82+
    case .peerUpdate:
    83+
    {}() // TODO: Send over XPC
    84+
    case let .log(logMsg):
    85+
    writeVpnLog(logMsg)
    86+
    case .networkSettings, .start, .stop:
    87+
    logger.critical("received unexpected message: `\(String(describing: msgType))`")
    88+
    }
    89+
    }
    90+
    91+
    func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) {
    92+
    guard let msgType = rpc.msg.msg else {
    93+
    logger.critical("received rpc with no type")
    94+
    return
    95+
    }
    96+
    switch msgType {
    97+
    case let .networkSettings(ns):
    98+
    let neSettings = convertNetworkSettingsRequest(ns)
    99+
    ptp.setTunnelNetworkSettings(neSettings)
    100+
    case .log, .peerUpdate, .start, .stop:
    101+
    logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
    102+
    }
    103+
    }
    104+
    105+
    // TODO: Call via XPC
    106+
    func startVPN(apiToken: String, server: URL) async throws(ManagerError) {
    107+
    logger.info("sending start rpc")
    108+
    let resp: Vpn_TunnelMessage
    109+
    do {
    110+
    resp = try await speaker.unaryRPC(.with { msg in
    111+
    msg.start = .with { req in
    112+
    // TODO: handle nil FD
    113+
    req.tunnelFileDescriptor = ptp.tunnelFileDescriptor!
    114+
    req.apiToken = apiToken
    115+
    req.coderURL = server.absoluteString
    116+
    }
    117+
    })
    118+
    } catch {
    119+
    throw .failedRPC(error)
    120+
    }
    121+
    guard case let .start(startResp) = resp.msg else {
    122+
    throw .incorrectResponse(resp)
    123+
    }
    124+
    if !startResp.success {
    125+
    throw .errorResponse(msg: startResp.errorMessage)
    126+
    }
    127+
    // TODO: notify app over XPC
    128+
    }
    129+
    130+
    // TODO: Call via XPC
    131+
    func stopVPN() async throws(ManagerError) {
    132+
    logger.info("sending stop rpc")
    133+
    let resp: Vpn_TunnelMessage
    134+
    do {
    135+
    resp = try await speaker.unaryRPC(.with { msg in
    136+
    msg.stop = .init()
    137+
    })
    138+
    } catch {
    139+
    throw .failedRPC(error)
    140+
    }
    141+
    guard case let .stop(stopResp) = resp.msg else {
    142+
    throw .incorrectResponse(resp)
    143+
    }
    144+
    if !stopResp.success {
    145+
    throw .errorResponse(msg: stopResp.errorMessage)
    146+
    }
    147+
    // TODO: notify app over XPC
    148+
    }
    149+
    150+
    // TODO: Call via XPC
    151+
    // Retrieves the current state of all peers,
    152+
    // as required when starting the app whilst the network extension is already running
    153+
    func getPeerInfo() async throws(ManagerError) {
    154+
    logger.info("sending peer state request")
    155+
    let resp: Vpn_TunnelMessage
    156+
    do {
    157+
    resp = try await speaker.unaryRPC(.with { msg in
    158+
    msg.getPeerUpdate = .init()
    159+
    })
    160+
    } catch {
    161+
    throw .failedRPC(error)
    162+
    }
    163+
    guard case .peerUpdate = resp.msg else {
    164+
    throw .incorrectResponse(resp)
    165+
    }
    166+
    // TODO: pass to app over XPC
    167+
    }
    168+
    }
    169+
    170+
    public struct ManagerConfig {
    171+
    let apiToken: String
    172+
    let serverUrl: URL
    173+
    }
    174+
    175+
    enum ManagerError: Error {
    176+
    case download(DownloadError)
    177+
    case tunnelSetup(TunnelHandleError)
    178+
    case handshake(HandshakeError)
    179+
    case validation(ValidationError)
    180+
    case incorrectResponse(Vpn_TunnelMessage)
    181+
    case failedRPC(any Error)
    182+
    case errorResponse(msg: String)
    183+
    }
    184+
    185+
    func writeVpnLog(_ log: Vpn_Log) {
    186+
    le 10000 t level: OSLogType = switch log.level {
    187+
    case .info: .info
    188+
    case .debug: .debug
    189+
    // warn == error
    190+
    case .warn: .error
    191+
    case .error: .error
    192+
    // critical == fatal == fault
    193+
    case .critical: .fault
    194+
    case .fatal: .fault
    195+
    case .UNRECOGNIZED: .info
    196+
    }
    197+
    let logger = Logger(
    198+
    subsystem: "\(Bundle.main.bundleIdentifier!).dylib",
    199+
    category: log.loggerNames.joined(separator: ".")
    200+
    )
    201+
    let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
    202+
    logger.log(level: level, "\(log.message): \(fields)")
    19203
    }

    Coder Desktop/VPN/PacketTunnelProvider.swift

    Lines changed: 9 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -5,10 +5,10 @@ import os
    55
    let CTLIOCGINFO: UInt = 0xC064_4E03
    66

    77
    class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
    8-
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network-extension")
    8+
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "packet-tunnel-provider")
    99
    private var manager: Manager?
    1010

    11-
    private var tunnelFileDescriptor: Int32? {
    11+
    public var tunnelFileDescriptor: Int32? {
    1212
    var ctlInfo = ctl_info()
    1313
    withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
    1414
    $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
    @@ -46,7 +46,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
    4646
    completionHandler(nil)
    4747
    return
    4848
    }
    49-
    manager = Manager(with: self)
    49+
    Task {
    50+
    // TODO: Retrieve access URL & Token via Keychain
    51+
    manager = try awai 10000 t Manager(
    52+
    with: self,
    53+
    cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
    54+
    )
    55+
    }
    5056
    completionHandler(nil)
    5157
    }
    5258

    Coder Desktop/VPN/TunnelHandle.swift

    Lines changed: 25 additions & 14 deletions
    Original file line numberDiff line numberDiff line change
    @@ -15,22 +15,12 @@ actor TunnelHandle {
    1515

    1616
    init(dylibPath: URL) throws(TunnelHandleError) {
    1717
    guard let dylibHandle = dlopen(dylibPath.path, RTLD_NOW | RTLD_LOCAL) else {
    18-
    var errStr = "UNKNOWN"
    19-
    let e = dlerror()
    20-
    if e != nil {
    21-
    errStr = String(cString: e!)
    22-
    }
    23-
    throw .dylib(errStr)
    18+
    throw .dylib(dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
    2419
    }
    2520
    self.dylibHandle = dylibHandle
    2621

    2722
    guard let startSym = dlsym(dylibHandle, startSymbol) else {
    28-
    var errStr = "UNKNOWN"
    29-
    let e = dlerror()
    30-
    if e != nil {
    31-
    errStr = String(cString: e!)
    32-
    }
    33-
    throw .symbol(startSymbol, errStr)
    23+
    throw .symbol(startSymbol, dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
    3424
    }
    3525
    let openTunnelFn = unsafeBitCast(startSym, to: OpenTunnel.self)
    3626
    tunnelReadPipe = Pipe()
    @@ -42,21 +32,42 @@ actor TunnelHandle {
    4232
    }
    4333
    }
    4434

    45-
    func close() throws {
    46-
    dlclose(dylibHandle)
    35+
    // This could be an isolated deinit in Swift 6.1
    36+
    func close() throws(TunnelHandleError) {
    37+
    var errs: [Error] = []
    38+
    if dlclose(dylibHandle) == 0 {
    39+
    errs.append(TunnelHandleError.dylib(dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN"))
    40+
    }
    41+
    do {
    42+
    try writeHandle.close()
    43+
    } catch {
    44+
    errs.append(error)
    45+
    }
    46+
    do {
    47+
    try readHandle.close()
    48+
    } catch {
    49+
    errs.append(error)
    50+
    }
    51+
    if !errs.isEmpty {
    52+
    throw .close(errs)
    53+
    }
    4754
    }
    4855
    }
    4956

    5057
    enum TunnelHandleError: Error {
    5158
    case dylib(String)
    5259
    case symbol(String, String)
    5360
    case openTunnel(OpenTunnelError)
    61+
    case pipe(any Error)
    62+
    case close([any Error])
    5463

    5564
    var description: String {
    5665
    switch self {
    66+
    case let .pipe(err): return "pipe error: \(err)"
    5767
    case let .dylib(d): return d
    5868
    case let .symbol(symbol, message): return "\(symbol): \(message)"
    5969
    case let .openTunnel(error): return "OpenTunnel: \(error.message)"
    70+
    case let .close(errs): return "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))"
    6071
    }
    6172
    }
    6273
    }

    0 commit comments

    Comments
     (0)
    0