Skip to content

Commit 0f67d90

Browse files
committed
chore: add network extension manager
1 parent bfb98f0 commit 0f67d90

14 files changed

+338
-57
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

+100-3
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,113 @@ actor Manager {
66
let ptp: PacketTunnelProvider
77
let downloader: Downloader
88

9-
var tunnelHandle: TunnelHandle?
10-
var speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>?
9+
let tunnelHandle: TunnelHandle
10+
let speaker: Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>
11+
var readLoop: Task<Void, Error>!
1112
// TODO: XPC Speaker
1213

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

17-
init(with: PacketTunnelProvider) {
18+
init(with: PacketTunnelProvider, server: URL) async throws(ManagerError) {
1819
ptp = with
1920
downloader = Downloader()
21+
#if arch(arm64)
22+
let dylibPath = server.appending(path: "bin/coder-vpn-arm64.dylib")
23+
#elseif arch(x86_64)
24+
let dylibPath = server.appending(path: "bin/coder-vpn-amd64.dylib")
25+
#else
26+
fatalError("unknown architecture")
27+
#endif
28+
do {
29+
try await downloader.download(src: dylibPath, dest: dest)
30+
} catch {
31+
throw .download(error)
32+
}
33+
do {
34+
try tunnelHandle = TunnelHandle(dylibPath: dest)
35+
} catch {
36+
throw .tunnelSetup(error)
37+
}
38+
speaker = await Speaker<Vpn_ManagerMessage, Vpn_TunnelMessage>(
39+
writeFD: tunnelHandle.writeHandle,
40+
readFD: tunnelHandle.readHandle
41+
)
42+
// TODO: Handshake
43+
// do throws(HandshakeError) {
44+
// try await speaker.handshake()
45+
// } catch {
46+
// throw .handshake(<#T##HandshakeError#>)
47+
// }
48+
readLoop = Task {
49+
for try await m in speaker {
50+
switch m {
51+
case let .message(msg):
52+
handleMessage(msg)
53+
case let .RPC(rpc):
54+
handleRPC(rpc)
55+
}
56+
}
57+
}
2058
}
59+
60+
func handleMessage(_ msg: Vpn_TunnelMessage) {
61+
guard let msgType = msg.msg else {
62+
logger.critical("received message with no type")
63+
return
64+
}
65+
switch msgType {
66+
case .peerUpdate:
67+
{}() // TODO: Send over XPC
68+
case let .log(logMsg):
69+
writeVpnLog(logMsg)
70+
case .networkSettings, .start, .stop:
71+
logger.critical("received unexpected message `\(String(describing: msgType))`")
72+
}
73+
}
74+
75+
func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) {
76+
guard let msgType = rpc.msg.msg else {
77+
logger.critical("received rpc with no type")
78+
return
79+
}
80+
switch msgType {
81+
case let .networkSettings(ns):
82+
let neSettings = convertNetworkSettingsRequest(ns)
83+
ptp.setTunnelNetworkSettings(neSettings)
84+
case .log, .peerUpdate, .start, .stop:
85+
logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
86+
}
87+
}
88+
89+
// TODO:
90+
func startVPN() throws {}
91+
func stopVPN() throws {}
92+
}
93+
94+
enum ManagerError: Error {
95+
case download(DownloadError)
96+
case tunnelSetup(TunnelHandleError)
97+
case handshake(HandshakeError)
98+
}
99+
100+
func writeVpnLog(_ log: Vpn_Log) {
101+
let level: OSLogType = switch log.level {
102+
case .info: .info
103+
case .debug: .debug
104+
// warn == error
105+
case .warn: .error
106+
case .error: .error
107+
// critical == fatal == fault
108+
case .critical: .fault
109+
case .fatal: .fault
110+
case .UNRECOGNIZED: .info
111+
}
112+
let logger = Logger(
113+
subsystem: "\(Bundle.main.bundleIdentifier!).dylib",
114+
category: log.loggerNames.joined(separator: ".")
115+
)
116+
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
117+
logger.log(level: level, "\(log.message): \(fields)")
21118
}

Coder Desktop/VPN/PacketTunnelProvider.swift

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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

1111
private var tunnelFileDescriptor: Int32? {
@@ -46,7 +46,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4646
completionHandler(nil)
4747
return
4848
}
49-
manager = Manager(with: self)
49+
Task {
50+
// TODO: Receive access URL w/ Token via Keychain?
51+
manager = try await Manager(with: self, server: URL(string: "https://dev.coder.com")!)
52+
}
5053
completionHandler(nil)
5154
}
5255

Coder Desktop/VPN/TunnelHandle.swift

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ actor TunnelHandle {
4242
}
4343
}
4444

45+
// This could be an isolated deinit in Swift 6.1
4546
func close() throws {
4647
dlclose(dylibHandle)
4748
}

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

0 commit comments

Comments
 (0)