Skip to content

Commit 4922fcf

Browse files
committed
chore: add network extension manager
1 parent 4d0b3da commit 4922fcf

File tree

11 files changed

+404
-39
lines changed

11 files changed

+404
-39
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

+190-3
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,203 @@ 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+
guard let tunFd = ptp.tunnelFileDescriptor else {
109+
throw .noTunnelFileDescriptor
110+
}
111+
let resp: Vpn_TunnelMessage
112+
do {
113+
resp = try await speaker.unaryRPC(.with { msg in
114+
msg.start = .with { req in
115+
req.tunnelFileDescriptor = tunFd
116+
req.apiToken = apiToken
117+
req.coderURL = server.absoluteString
118+
}
119+
})
120+
} catch {
121+
throw .failedRPC(error)
122+
}
123+
guard case let .start(startResp) = resp.msg else {
124+
throw .incorrectResponse(resp)
125+
}
126+
if !startResp.success {
127+
throw .errorResponse(msg: startResp.errorMessage)
128+
}
129+
// TODO: notify app over XPC
130+
}
131+
132+
// TODO: Call via XPC
133+
func stopVPN() async throws(ManagerError) {
134+
logger.info("sending stop rpc")
135+
let resp: Vpn_TunnelMessage
136+
do {
137+
resp = try await speaker.unaryRPC(.with { msg in
138+
msg.stop = .init()
139+
})
140+
} catch {
141+
throw .failedRPC(error)
142+
}
143+
guard case let .stop(stopResp) = resp.msg else {
144+
throw .incorrectResponse(resp)
145+
}
146+
if !stopResp.success {
147+
throw .errorResponse(msg: stopResp.errorMessage)
148+
}
149+
// TODO: notify app over XPC
150+
}
151+
152+
// TODO: Call via XPC
153+
// Retrieves the current state of all peers,
154+
// as required when starting the app whilst the network extension is already running
155+
func getPeerInfo() async throws(ManagerError) {
156+
logger.info("sending peer state request")
157+
let resp: Vpn_TunnelMessage
158+
do {
159+
resp = try await speaker.unaryRPC(.with { msg in
160+
msg.getPeerUpdate = .init()
161+
})
162+
} catch {
163+
throw .failedRPC(error)
164+
}
165+
guard case .peerUpdate = resp.msg else {
166+
throw .incorrectResponse(resp)
167+
}
168+
// TODO: pass to app over XPC
169+
}
170+
}
171+
172+
public struct ManagerConfig {
173+
let apiToken: String
174+
let serverUrl: URL
175+
}
176+
177+
enum ManagerError: Error {
178+
case download(DownloadError)
179+
case tunnelSetup(TunnelHandleError)
180+
case handshake(HandshakeError)
181+
case validation(ValidationError)
182+
case incorrectResponse(Vpn_TunnelMessage)
183+
case failedRPC(any Error)
184+
case errorResponse(msg: String)
185+
case noTunnelFileDescriptor
186+
}
187+
188+
func writeVpnLog(_ log: Vpn_Log) {
189+
let level: OSLogType = switch log.level {
190+
case .info: .info
191+
case .debug: .debug
192+
// warn == error
193+
case .warn: .error
194+
case .error: .error
195+
// critical == fatal == fault
196+
case .critical: .fault
197+
case .fatal: .fault
198+
case .UNRECOGNIZED: .info
199+
}
200+
let logger = Logger(
201+
subsystem: "\(Bundle.main.bundleIdentifier!).dylib",
202+
category: log.loggerNames.joined(separator: ".")
203+
)
204+
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
205+
logger.log(level: level, "\(log.message): \(fields)")
19206
}

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/VPNLib/Convert.swift

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import NetworkExtension
2+
import os
3+
4+
// swiftlint:disable function_body_length
5+
public func convertNetworkSettingsRequest(_ req: Vpn_NetworkSettingsRequest) -> NEPacketTunnelNetworkSettings {
6+
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: req.tunnelRemoteAddress)
7+
networkSettings.tunnelOverheadBytes = NSNumber(value: req.tunnelOverheadBytes)
8+
networkSettings.mtu = NSNumber(value: req.mtu)
9+
10+
if req.hasDnsSettings {
11+
let dnsSettings = NEDNSSettings(servers: req.dnsSettings.servers)
12+
dnsSettings.searchDomains = req.dnsSettings.searchDomains
13+
dnsSettings.domainName = req.dnsSettings.domainName
14+
dnsSettings.matchDomains = req.dnsSettings.matchDomains
15+
dnsSettings.matchDomainsNoSearch = req.dnsSettings.matchDomainsNoSearch
16+
networkSettings.dnsSettings = dnsSettings
17+
}
18+
19+
if req.hasIpv4Settings {
20+
let ipv4Settings = NEIPv4Settings(addresses: req.ipv4Settings.addrs, subnetMasks: req.ipv4Settings.subnetMasks)
21+
ipv4Settings.router = req.ipv4Settings.router
22+
ipv4Settings.includedRoutes = req.ipv4Settings.includedRoutes.map {
23+
let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
24+
route.gatewayAddress = $0.router
25+
return route
26+
}
27+
ipv4Settings.excludedRoutes = req.ipv4Settings.excludedRoutes.map {
28+
let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
29+
route.gatewayAddress = $0.router
30+
return route
31+
}
32+
networkSettings.ipv4Settings = ipv4Settings
33+
}
34+
35+
if req.hasIpv6Settings {
36+
let ipv6Settings = NEIPv6Settings(
37+
addresses: req.ipv6Settings.addrs,
38+
networkPrefixLengths: req.ipv6Settings.prefixLengths.map { NSNumber(value: $0)
39+
}
40+
)
41+
ipv6Settings.includedRoutes = req.ipv6Settings.includedRoutes.map {
42+
let route = NEIPv6Route(
43+
destinationAddress: $0.destination,
44+
networkPrefixLength: NSNumber(value: $0.prefixLength)
45+
)
46+
route.gatewayAddress = $0.router
47+
return route
48+
}
49+
ipv6Settings.excludedRoutes = req.ipv6Settings.excludedRoutes.map {
50+
let route = NEIPv6Route(
51+
destinationAddress: $0.destination,
52+
networkPrefixLength: NSNumber(value: $0.prefixLength)
53+
)
54+
route.gatewayAddress = $0.router
55+
return route
56+
}
57+
networkSettings.ipv6Settings = ipv6Settings
58+
}
59+
return networkSettings
60+
}

Coder Desktop/VPNLib/Receiver.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ actor Receiver<RecvMsg: Message> {
5959
/// On read or decoding error, it logs and closes the stream.
6060
func messages() throws(ReceiveError) -> AsyncStream<RecvMsg> {
6161
if running {
62-
throw ReceiveError.alreadyRunning
62+
throw .alreadyRunning
6363
}
6464
running = true
6565
return AsyncStream(

0 commit comments

Comments
 (0)