Skip to content

Commit 300889e

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

File tree

12 files changed

+426
-53
lines changed

12 files changed

+426
-53
lines changed

Coder Desktop/.swiftlint.yml

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

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

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

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

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

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

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

+25-14
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)