8000 chore: add dylib downloader and validator (#16) · coder/coder-desktop-macos@161e5c2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 161e5c2

Browse files
chore: add dylib downloader and validator (#16)
First PR for #2. This PR adds an abstraction for downloading & validating the dylib from a Coder server, and the network extension scaffolding. It also adds a `TunnelHandle` type for owning the pair of pipes passed to the dylib, and the handle to the dylib itself. You cannot create a unit test target that targets a System Extension. So, this PR extracts the portion of the network extension that we'd like to test into it's own Framework, `VPNLib`. Of note is that `SwiftProtobuf` doesn't have a stable ABI (as it doesn't use [library evolution](https://www.swift.org/blog/library-evolution/)), so the Framework has the `Build libraries for distribution` setting disabled. This shouldn't effect anything. Exporting the `SwiftProtobuf` types should be fine provided we don't import `SwiftProtobuf` in to the `VPN` target as well.
1 parent e9f5c6f commit 161e5c2

File tree

21 files changed

+1059
-387
lines changed
  • 21 files changed

    +1059
    -387
    lines changed

    Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

    Lines changed: 304 additions & 75 deletions
    Large diffs are not rendered by default.

    Coder Desktop/Coder Desktop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

    Lines changed: 10 additions & 1 deletion
    Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

    Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/Coder Desktop.xcscheme

    Lines changed: 14 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -62,9 +62,20 @@
    6262
    parallelizable = "YES">
    6363
    <BuildableReference
    6464
    BuildableIdentifier = "primary"
    65-
    BlueprintIdentifier = "961679D82D030E1D00B2B6DF"
    66-
    BuildableName = "ProtoTests.xctest"
    67-
    BlueprintName = "ProtoTests"
    65+
    BlueprintIdentifier = "AA3B3DA02D2D23860099996A"
    66+
    BuildableName = "VPNLib.framework"
    67+
    BlueprintName = "VPNLib"
    68+
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    69+
    </BuildableReference>
    70+
    </TestableReference>
    71+
    <TestableReference
    72+
    skipped = "NO"
    73+
    parallelizable = "YES">
    74+
    <BuildableReference
    75+
    BuildableIdentifier = "primary"
    76+
    BlueprintIdentifier = "AA3B3DA72D2D23860099996A"
    77+
    BuildableName = "VPNLibTests.xctest"
    78+
    BlueprintName = "VPNLibTests"
    6879
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    6980
    </BuildableReference>
    7081
    </TestableReference>

    Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/ProtoTests.xcscheme renamed to Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/VPN.xcscheme

    Lines changed: 19 additions & 26 deletions
    Original file line numberDiff line numberDiff line change
    @@ -6,26 +6,29 @@
    66
    parallelizeBuildables = "YES"
    77
    buildImplicitDependencies = "YES"
    88
    buildArchitectures = "Automatic">
    9+
    <BuildActionEntries>
    10+
    <BuildActionEntry
    11+
    buildForTesting = "YES"
    12+
    buildForRunning = "YES"
    13+
    buildForProfiling = "YES"
    14+
    buildForArchiving = "YES"
    15+
    buildForAnalyzing = "YES">
    16+
    <BuildableReference
    17+
    BuildableIdentifier = "primary"
    18+
    BlueprintIdentifier = "9616792F2CFF117300B2B6DF"
    19+
    BuildableName = "com.coder.Coder-Desktop.VPN.systemextension"
    20+
    BlueprintName = "VPN"
    21+
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    22+
    </BuildableReference>
    23+
    </BuildActionEntry>
    24+
    </BuildActionEntries>
    925
    </BuildAction>
    1026
    <TestAction
    1127
    buildConfiguration = "Debug"
    1228
    selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
    1329
    selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
    1430
    shouldUseLaunchSchemeArgsEnv = "YES"
    1531
    shouldAutocreateTestPlan = "YES">
    16-
    <Testables>
    17-
    <TestableReference
    18-
    skipped = "NO"
    19-
    parallelizable = "YES">
    20-
    <BuildableReference
    21-
    BuildableIdentifier = "primary"
    22-
    BlueprintIdentifier = "961679D82D030E1D00B2B6DF"
    23-
    BuildableName = "ProtoTests.xctest"
    24-
    BlueprintName = "ProtoTests"
    25-
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    26-
    </BuildableReference>
    27-
    </TestableReference>
    28-
    </Testables>
    2932
    </TestAction>
    3033
    <LaunchAction
    3134
    buildConfiguration = "Debug"
    @@ -37,16 +40,6 @@
    3740
    debugDocumentVersioning = "YES"
    3841
    debugServiceExtension = "internal"
    3942
    allowLocationSimulation = "YES">
    40-
    <BuildableProductRunnable
    41-
    runnableDebuggingMode = "0">
    42-
    <BuildableReference
    43-
    BuildableIdentifier = "primary"
    44-
    BlueprintIdentifier = "961678FB2CFF100D00B2B6DF"
    45-
    BuildableName = "Coder Desktop.app"
    46-
    BlueprintName = "Coder Desktop"
    47-
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    48-
    </BuildableReference>
    49-
    </BuildableProductRunnable>
    5043
    </LaunchAction>
    5144
    <ProfileAction
    5245
    buildConfiguration = "Release"
    @@ -57,9 +50,9 @@
    5750
    <MacroExpansion>
    5851
    <BuildableReference
    5952
    BuildableIdentifier = "primary"
    60-
    BlueprintIdentifier = "961678FB2CFF100D00B2B6DF"
    61-
    BuildableName = "Coder Desktop.app"
    62-
    BlueprintName = "Coder Desktop"
    53+
    BlueprintIdentifier = "9616792F2CFF117300B2B6DF"
    54+
    BuildableName = "com.coder.Coder-Desktop.VPN.systemextension"
    55+
    BlueprintName = "VPN"
    6356
    ReferencedContainer = "container:Coder Desktop.xcodeproj">
    6457
    </BuildableReference>
    6558
    </MacroExpansion>

    Coder Desktop/Coder Desktop.xctestplan

    Lines changed: 4 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -19,15 +19,15 @@
    1919
    {
    2020
    "target" : {
    2121
    "containerPath" : "container:Coder Desktop.xcodeproj",
    22-
    "identifier" : "961679D82D030E1D00B2B6DF",
    23-
    "name" : "ProtoTests"
    22+
    "identifier" : "9616790E2CFF100E00B2B6DF",
    23+
    "name" : "Coder DesktopTests"
    2424
    }
    2525
    },
    2626
    {
    2727
    "target" : {
    2828
    "containerPath" : "container:Coder Desktop.xcodeproj",
    29-
    "identifier" : "9616790E2CFF100E00B2B6DF",
    30-
    "name" : "Coder DesktopTests"
    29+
    "identifier" : "AA3B3DA72D2D23860099996A",
    30+
    "name" : "VPNLibTests"
    3131
    }
    3232
    },
    3333
    {

    Coder Desktop/Coder Desktop/SDK/Client.swift

    Lines changed: 3 additions & 3 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1,7 +1,7 @@
    11
    import Alamofire
    22
    import Foundation
    33

    4-
    protocol Client {
    4+
    protocol Client: Sendable {
    55
    init(url: URL, token: String?)
    66
    func user(_ ident: String) async throws(ClientError) -> User
    77
    }
    @@ -114,10 +114,10 @@ struct APIError: Decodable {
    114114
    struct Response: Decodable {
    115115
    let message: String
    116116
    let detail: String?
    117-
    let validations: [ValidationError]?
    117+
    let validations: [FieldValidation]?
    118118
    }
    119119

    120-
    struct ValidationError: Decodable {
    120+
    struct FieldValidation: Decodable {
    121121
    let field: String
    122122
    let detail: String
    123123
    }

    Coder Desktop/VPN/Manager.swift

    Lines changed: 19 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,19 @@
    1+
    import NetworkExtension
    2+
    import os
    3+
    import VPNLib
    4+
    5+
    actor Manager {
    6+
    let ptp: PacketTunnelProvider
    7+
    8+
    var tunnelHandle: TunnelHandle?
    9+
    var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>?
    10+
    // TODO: XPC Speaker
    11+
    12+
    private let dest = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    13+
    .first!.appending(path: "coder-vpn.dylib")
    14+
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "manager")
    15+
    16+
    init(with: PacketTunnelProvider) {
    17+
    ptp = with
    18+
    }
    19+
    }

    Coder Desktop/VPN/PacketTunnelProvider.swift

    Lines changed: 54 additions & 4 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1,12 +1,62 @@
    11
    import NetworkExtension
    2+
    import os
    23

    3-
    class PacketTunnelProvider: NEPacketTunnelProvider {
    4-
    override func startTunnel(options _: [String: NSObject]?, completionHandler _: @escaping (Error?) -> Void) {
    5-
    // Add code here to start the process of connecting the tunnel.
    4+
    /* From <sys/kern_control.h> */
    5+
    let CTLIOCGINFO: UInt = 0xC064_4E03
    6+
    7+
    class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
    8+
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network-extension")
    9+
    private var manager: Manager?
    10+
    11+
    private var tunnelFileDescriptor: Int32? {
    12+
    var ctlInfo = ctl_info()
    13+
    withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
    14+
    $0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
    15+
    _ = strcpy($0, "com.apple.net.utun_control")
    16+
    }
    17+
    }
    18+
    for fd: Int32 in 0 ... 1024 {
    19+
    var addr = sockaddr_ctl()
    20+
    var ret: Int32 = -1
    21+
    var len = socklen_t(MemoryLayout.size(ofValue: addr))
    22+
    withUnsafeMutablePointer(to: &addr) {
    23+
    $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
    24+
    ret = getpeername(fd, $0, &len)
    25+
    }
    26+
    }
    27+
    if ret != 0 || addr.sc_family != AF_SYSTEM {
    28+
    continue
    29+
    }
    30+
    if ctlInfo.ctl_id == 0 {
    31+
    ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
    32+
    if ret != 0 {
    33+
    continue
    34+
    }
    35+
    }
    36+
    if addr.sc_id == ctlInfo.ctl_id {
    37+
    return fd
    38+
    }
    39+
    }
    40+
    return nil
    41+
    }
    42+
    43+
    override func startTunnel(options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
    44+
    guard manager == nil else {
    45+
    logger.error("startTunnel called with non-nil Manager")
    46+
    completionHandler(nil)
    47+
    return
    48+
    }
    49+
    manager = Manager(with: self)
    50+
    completionHandler(nil)
    651
    }
    752

    853
    override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
    9-
    // Add code here to start the process of stopping the tunnel.
    54+
    guard manager == nil else {
    55+
    logger.error("stopTunnel called with nil Manager")
    56+
    completionHandler()
    57+
    return
    58+
    }
    59+
    manager = nil
    1060
    completionHandler()
    1161
    }
    1262

    Coder Desktop/VPN/TunnelHandle.swift

    Lines changed: 91 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,91 @@
    1+
    import Foundation
    2+
    import os
    3+
    4+
    let startSymbol = "OpenTunnel"
    5+
    6+
    actor TunnelHandle {
    7+
    private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "tunnel-handle")
    8+
    9+
    private let tunnelWritePipe: Pipe
    10+
    private let tunnelReadPipe: Pipe
    11+
    private let dylibHandle: UnsafeMutableRawPointer
    12+
    13+
    var writeHandle: FileHandle { tunnelReadPipe.fileHandleForWriting }
    14+
    var readHandle: FileHandle { tunnelWritePipe.fileHandleForReading }
    15+
    16+
    init(dylibPath: URL) throws(TunnelHandleError) {
    17+
    guard let dylibHandle = dlopen(dylibPath.path, RTLD_NOW | RTLD_LOCAL) else {
    18+
    throw .dylib(dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
    19+
    }
    20+
    self.dylibHandle = dylibHandle
    21+
    22+
    guard let startSym = dlsym(dylibHandle, startSymbol) else {
    23+
    throw .symbol(startSymbol, dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
    24+
    }
    25+
    let openTunnelFn = unsafeBitCast(startSym, to: OpenTunnel.self)
    26+
    tunnelReadPipe = Pipe()
    27+
    tunnelWritePipe = Pipe()
    28+
    let res = openTunnelFn(tunnelReadPipe.fileHandleForReading.fileDescriptor,
    29+
    tunnelWritePipe.fileHandleForWriting.fileDescriptor)
    30+
    guard res == 0 else {
    31+
    throw .openTunnel(OpenTunnelError(rawValue: res) ?? .unknown)
    32+
    }
    33+
    }
    34+
    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+
    }
    54+
    }
    55+
    }
    56+
    57+
    enum TunnelHandleError: Error {
    58+
    case dylib(String)
    59+
    case symbol(String, String)
    60+
    case openTunnel(OpenTunnelError)
    61+
    case pipe(any Error)
    62+
    case close([any Error])
    63+
    64+
    var description: String {
    65+
    switch self {
    66+
    case let .pipe(err): return "pipe error: \(err)"
    67+
    case let .dylib(d): return d
    68+
    case let .symbol(symbol, message): return "\(symbol): \(message)"
    69+
    case let .openTunnel(error): return "OpenTunnel: \(error.message)"
    70+
    case let .close(errs): return "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))"
    71+
    }
    72+
    }
    73+
    }
    74+
    75+
    enum OpenTunnelError: Int32 {
    76+
    case errDupReadFD = -2
    77+
    case errDupWriteFD = -3
    78+
    case errOpenPipe = -4
    79+
    case errNewTunnel = -5
    80+
    case unknown = -99
    81+
    82+
    var message: String {
    83+
    switch self {
    84+
    case .errDupReadFD: return "Failed to duplicate read file descriptor"
    85+
    case .errDupWriteFD: return "Failed to duplicate write file descriptor"
    86+
    case .errOpenPipe: return "Failed to open the pipe"
    87+
    case .errNewTunnel: return "Failed to create a new tunnel"
    88+
    case .unknown: return "Unknown error code"
    89+
    }
    90+
    }
    91+
    }
    Lines changed: 7 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,7 @@
    1+
    #ifndef CoderPacketTunnelProvider_Bridging_Header_h
    2+
    #define CoderPacketTunnelProvider_Bridging_Header_h
    3+
    4+
    // GoInt32 OpenTunnel(GoInt32 cReadFD, GoInt32 cWriteFD);
    5+
    typedef int(*OpenTunnel)(int, int);
    6+
    7+
    #endif /* CoderPacketTunnelProvider_Bridging_Header_h */

    0 commit comments

    Comments
     (0)
    0