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

21 files changed

+1059
-387
lines changed

Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

+304-75
Large diffs are not rendered by default.

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"originHash" : "aa8dd97dc6e28dedc4a5c45c435467a247486474bf3c1caf5e67085d52325132",
2+
"originHash" : "1cd4f7368eeddbaa35ef829e13093bc7081a4e6d3da9492d22db0757464ad473",
33
"pins" : [
44
{
55
"identity" : "alamofire",
@@ -27,6 +27,15 @@
2727
"revision" : "e0c7eebc5a4465a3c4680764f26b7a61f567cdaf"
2828
}
2929
},
30+
{
31+
"identity" : "mocker",
32+
"kind" : "remoteSourceControl",
33+
"location" : "https://github.com/WeTransfer/Mocker",
34+
"state" : {
35+
"revision" : "95fa785c751f6bc40c49e112d433c3acf8417a97",
36+
"version" : "3.0.2"
37+
}
38+
},
3039
{
3140
"identity" : "swift-protobuf",
3241
"kind" : "remoteSourceControl",

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

+14-3
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 Coder Desktop/Coder Desktop.xcodeproj/xcshareddata/xcschemes/VPN.xcscheme

+19-26
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

+4-4
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

+3-3
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

+19
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

+54-4
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

+91
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+
}
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)