Skip to content

Commit

Permalink
chore: add dylib downloader and validator (#16)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ethanndickson authored Jan 14, 2025
1 parent e9f5c6f commit 161e5c2
Show file tree
Hide file tree
Showing 21 changed files with 1,059 additions and 387 deletions.
379 changes: 304 additions & 75 deletions Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "aa8dd97dc6e28dedc4a5c45c435467a247486474bf3c1caf5e67085d52325132",
"originHash" : "1cd4f7368eeddbaa35ef829e13093bc7081a4e6d3da9492d22db0757464ad473",
"pins" : [
{
"identity" : "alamofire",
Expand Down Expand Up @@ -27,6 +27,15 @@
"revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf"
}
},
{
"identity" : "mocker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/WeTransfer/Mocker",
"state" : {
"revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97",
"version" : "3.0.2"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,20 @@
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "961679D82D030E1D00B2B6DF"
BuildableName = "ProtoTests.xctest"
BlueprintName = "ProtoTests"
BlueprintIdentifier = "AA3B3DA02D2D23860099996A"
BuildableName = "VPNLib.framework"
BlueprintName = "VPNLib"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA3B3DA72D2D23860099996A"
BuildableName = "VPNLibTests.xctest"
BlueprintName = "VPNLibTests"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</TestableReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "9616792F2CFF117300B2B6DF"
BuildableName = "com.coder.Coder-Desktop.VPN.systemextension"
BlueprintName = "VPN"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "961679D82D030E1D00B2B6DF"
BuildableName = "ProtoTests.xctest"
BlueprintName = "ProtoTests"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand All @@ -37,16 +40,6 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "961678FB2CFF100D00B2B6DF"
BuildableName = "Coder Desktop.app"
BlueprintName = "Coder Desktop"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
Expand All @@ -57,9 +50,9 @@
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "961678FB2CFF100D00B2B6DF"
BuildableName = "Coder Desktop.app"
BlueprintName = "Coder Desktop"
BlueprintIdentifier = "9616792F2CFF117300B2B6DF"
BuildableName = "com.coder.Coder-Desktop.VPN.systemextension"
BlueprintName = "VPN"
ReferencedContainer = "container:Coder Desktop.xcodeproj">
</BuildableReference>
</MacroExpansion>
Expand Down
8 changes: 4 additions & 4 deletions Coder Desktop/Coder Desktop.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
{
"target" : {
"containerPath" : "container:Coder Desktop.xcodeproj",
"identifier" : "961679D82D030E1D00B2B6DF",
"name" : "ProtoTests"
"identifier" : "9616790E2CFF100E00B2B6DF",
"name" : "Coder DesktopTests"
}
},
{
"target" : {
"containerPath" : "container:Coder Desktop.xcodeproj",
"identifier" : "9616790E2CFF100E00B2B6DF",
"name" : "Coder DesktopTests"
"identifier" : "AA3B3DA72D2D23860099996A",
"name" : "VPNLibTests"
}
},
{
Expand Down
6 changes: 3 additions & 3 deletions Coder Desktop/Coder Desktop/SDK/Client.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Alamofire
import Foundation

protocol Client {
protocol Client: Sendable {
init(url: URL, token: String?)
func user(_ ident: String) async throws(ClientError) -> User
}
Expand Down Expand Up @@ -114,10 +114,10 @@ struct APIError: Decodable {
struct Response: Decodable {
let message: String
let detail: String?
let validations: [ValidationError]?
let validations: [FieldValidation]?
}

struct ValidationError: Decodable {
struct FieldValidation: Decodable {
let field: String
let detail: String
}
Expand Down
19 changes: 19 additions & 0 deletions Coder Desktop/VPN/Manager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import NetworkExtension
import os
import VPNLib

actor Manager {
let ptp: PacketTunnelProvider

var tunnelHandle: TunnelHandle?
var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>?
// TODO: XPC Speaker

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

init(with: PacketTunnelProvider) {
ptp = with
}
}
58 changes: 54 additions & 4 deletions Coder Desktop/VPN/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,62 @@
import NetworkExtension
import os

class PacketTunnelProvider: NEPacketTunnelProvider {
override func startTunnel(options _: [String: NSObject]?, completionHandler _: @escaping (Error?) -> Void) {
// Add code here to start the process of connecting the tunnel.
/* From <sys/kern_control.h> */
let CTLIOCGINFO: UInt = 0xC064_4E03

class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "network-extension")
private var manager: Manager?

private var tunnelFileDescriptor: Int32? {
var ctlInfo = ctl_info()
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
_ = strcpy($0, "com.apple.net.utun_control")
}
}
for fd: Int32 in 0 ... 1024 {
var addr = sockaddr_ctl()
var ret: Int32 = -1
var len = socklen_t(MemoryLayout.size(ofValue: addr))
withUnsafeMutablePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
ret = getpeername(fd, $0, &len)
}
}
if ret != 0 || addr.sc_family != AF_SYSTEM {
continue
}
if ctlInfo.ctl_id == 0 {
ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
if ret != 0 {
continue
}
}
if addr.sc_id == ctlInfo.ctl_id {
return fd
}
}
return nil
}

override func startTunnel(options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
guard manager == nil else {
logger.error("startTunnel called with non-nil Manager")
completionHandler(nil)
return
}
manager = Manager(with: self)
completionHandler(nil)
}

override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
// Add code here to start the process of stopping the tunnel.
guard manager == nil else {
logger.error("stopTunnel called with nil Manager")
completionHandler()
return
}
manager = nil
completionHandler()
}

Expand Down
91 changes: 91 additions & 0 deletions Coder Desktop/VPN/TunnelHandle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation
import os

let startSymbol = "OpenTunnel"

actor TunnelHandle {
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "tunnel-handle")

private let tunnelWritePipe: Pipe
private let tunnelReadPipe: Pipe
private let dylibHandle: UnsafeMutableRawPointer

var writeHandle: FileHandle { tunnelReadPipe.fileHandleForWriting }
var readHandle: FileHandle { tunnelWritePipe.fileHandleForReading }

init(dylibPath: URL) throws(TunnelHandleError) {
guard let dylibHandle = dlopen(dylibPath.path, RTLD_NOW | RTLD_LOCAL) else {
throw .dylib(dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
}
self.dylibHandle = dylibHandle

guard let startSym = dlsym(dylibHandle, startSymbol) else {
throw .symbol(startSymbol, dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN")
}
let openTunnelFn = unsafeBitCast(startSym, to: OpenTunnel.self)
tunnelReadPipe = Pipe()
tunnelWritePipe = Pipe()
let res = openTunnelFn(tunnelReadPipe.fileHandleForReading.fileDescriptor,
tunnelWritePipe.fileHandleForWriting.fileDescriptor)
guard res == 0 else {
throw .openTunnel(OpenTunnelError(rawValue: res) ?? .unknown)
}
}

// This could be an isolated deinit in Swift 6.1
func close() throws(TunnelHandleError) {
var errs: [Error] = []
if dlclose(dylibHandle) == 0 {
errs.append(TunnelHandleError.dylib(dlerror().flatMap { String(cString: $0) } ?? "UNKNOWN"))
}
do {
try writeHandle.close()
} catch {
errs.append(error)
}
do {
try readHandle.close()
} catch {
errs.append(error)
}
if !errs.isEmpty {
throw .close(errs)
}
}
}

enum TunnelHandleError: Error {
case dylib(String)
case symbol(String, String)
case openTunnel(OpenTunnelError)
case pipe(any Error)
case close([any Error])

var description: String {
switch self {
case let .pipe(err): return "pipe error: \(err)"
case let .dylib(d): return d
case let .symbol(symbol, message): return "\(symbol): \(message)"
case let .openTunnel(error): return "OpenTunnel: \(error.message)"
case let .close(errs): return "close tunnel: \(errs.map(\.localizedDescription).joined(separator: ", "))"
}
}
}

enum OpenTunnelError: Int32 {
case errDupReadFD = -2
case errDupWriteFD = -3
case errOpenPipe = -4
case errNewTunnel = -5
case unknown = -99

var message: String {
switch self {
case .errDupReadFD: return "Failed to duplicate read file descriptor"
case .errDupWriteFD: return "Failed to duplicate write file descriptor"
case .errOpenPipe: return "Failed to open the pipe"
case .errNewTunnel: return "Failed to create a new tunnel"
case .unknown: return "Unknown error code"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#ifndef CoderPacketTunnelProvider_Bridging_Header_h
#define CoderPacketTunnelProvider_Bridging_Header_h

// GoInt32 OpenTunnel(GoInt32 cReadFD, GoInt32 cWriteFD);
typedef int(*OpenTunnel)(int, int);

#endif /* CoderPacketTunnelProvider_Bridging_Header_h */
Loading

0 comments on commit 161e5c2

Please sign in to comment.