Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4b43bbb

Browse files
committedJan 8, 2025·
chore: add network extension manager
1 parent 8453170 commit 4b43bbb

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

‎Coder Desktop/VPNLib/Downloader.swift

+46-23
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import CryptoKit
22
import Foundation
33

44
public protocol Validator: Sendable {
5-
func validate(path: URL) async throws
5+
func validate(path: URL) async throws(ValidationError)
66
}
77

8-
public enum ValidationError: LocalizedError {
8+
public enum ValidationError: Error {
99
case fileNotFound
1010
case unableToCreateStaticCode
1111
case invalidSignature
@@ -51,58 +51,58 @@ public struct SignatureValidator: Validator {
5151

5252
public init() {}
5353

54-
public func validate(path: URL) throws {
54+
public func validate(path: URL) throws(ValidationError) {
5555
guard FileManager.default.fileExists(atPath: path.path) else {
56-
throw ValidationError.fileNotFound
56+
throw .fileNotFound
5757
}
5858

5959
var staticCode: SecStaticCode?
6060
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode)
6161
guard status == errSecSuccess, let code = staticCode else {
62-
throw ValidationError.unableToCreateStaticCode
62+
throw .unableToCreateStaticCode
6363
}
6464

6565
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
6666
guard validateStatus == errSecSuccess else {
67-
throw ValidationError.invalidSignature
67+
throw .invalidSignature
6868
}
6969

7070
var information: CFDictionary?
7171
let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information)
7272
guard infoStatus == errSecSuccess, let info = information as? [String: Any] else {
73-
throw ValidationError.unableToRetrieveInfo
73+
throw .unableToRetrieveInfo
7474
}
7575

7676
guard let identifier = info[kSecCodeInfoIdentifier as String] as? String,
7777
identifier == expectedIdentifier
7878
else {
79-
throw ValidationError.invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
79+
throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
8080
}
8181

8282
guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String,
8383
teamIdentifier == expectedTeamIdentifier
8484
else {
85-
throw ValidationError.invalidTeamIdentifier(
85+
throw .invalidTeamIdentifier(
8686
identifier: info[kSecCodeInfoTeamIdentifier as String] as? String
8787
)
8888
}
8989

9090
guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else {
91-
throw ValidationError.missingInfoPList
91+
throw .missingInfoPList
9292
}
9393

9494
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else {
95-
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
95+
throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
9696
}
9797

9898
guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else {
99-
throw ValidationError.invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
99+
throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
100100
}
101101

102102
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
103103
minDylibVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
104104
else {
105-
throw ValidationError.invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
105+
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
106106
}
107107
}
108108
}
@@ -113,36 +113,50 @@ public actor Downloader {
113113
self.validator = validator
114114
}
115115

116-
public func download(src: URL, dest: URL) async throws {
116+
public func download(src: URL, dest: URL) async throws(DownloadError) {
117117
var req = URLRequest(url: src)
118118
if FileManager.default.fileExists(atPath: dest.path) {
119119
if let existingFileData = try? Data(contentsOf: dest) {
120120
req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match")
121121
}
122122
}
123123
// TODO: Add Content-Length headers to coderd, add download progress delegate
124-
let (tempURL, response) = try await URLSession.shared.download(for: req)
124+
let tempURL: URL
125+
let response: URLResponse
126+
do {
127+
(tempURL, response) = try await URLSession.shared.download(for: req)
128+
} catch {
129+
throw .networkError(error)
130+
}
125131
defer {
126132
if FileManager.default.fileExists(atPath: dest.path) {
127133
do { try FileManager.default.removeItem(at: tempURL) } catch {}
128134
}
129135
}
130136

131137
guard let httpResponse = response as? HTTPURLResponse else {
132-
throw DownloadError.invalidResponse
138+
throw .invalidResponse
133139
}
134140
guard httpResponse.statusCode != 304 else {
135141
return
136142
}
137143
guard (200 ..< 300).contains(httpResponse.statusCode) else {
138-
throw DownloadError.unexpectedStatusCode(httpResponse.statusCode)
144+
throw .unexpectedStatusCode(httpResponse.statusCode)
139145
}
140146

141-
if FileManager.default.fileExists(atPath: dest.path) {
142-
try FileManager.default.removeItem(at: dest)
147+
do {
148+
if FileManager.default.fileExists(atPath: dest.path) {
149+
try FileManager.default.removeItem(at: dest)
150+
}
151+
try FileManager.default.moveItem(at: tempURL, to: dest)
152+
} catch {
153+
throw .fileOpError(error)
154+
}
155+
do throws(ValidationError) {
156+
try await validator.validate(path: dest)
157+
} catch {
158+
throw .validationError(error)
143159
}
144-
try FileManager.default.moveItem(at: tempURL, to: dest)
145-
try await validator.validate(path: dest)
146160
}
147161
}
148162

@@ -152,14 +166,23 @@ func etag(data: Data) -> String {
152166
return "\"\(etag)\""
153167
}
154168

155-
enum DownloadError: LocalizedError {
169+
public enum DownloadError: Error {
156170
case unexpectedStatusCode(Int)
157171
case invalidResponse
172+
case networkError(any Error)
173+
case fileOpError(any Error)
174+
case validationError(ValidationError)
158175

159176
var localizedDescription: String {
160177
switch self {
161178
case let .unexpectedStatusCode(code):
162-
return "Unexpected status code: \(code)"
179+
return "Unexpected HTTP status code: \(code)"
180+
case let .networkError(error):
181+
return "Network error: \(error.localizedDescription)"
182+
case let .fileOpError(error):
183+
return "File operation error: \(error.localizedDescription)"
184+
case let .validationError(error):
185+
return "Validation error: \(error.localizedDescription)"
163186
case .invalidResponse:
164187
return "Received non-HTTP response"
165188
}

‎Coder Desktop/VPNLib/Receiver.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ actor Receiver<RecvMsg: Message> {
77
private let dispatch: DispatchIO
88
private let queue: DispatchQueue
99
private var running = false
10-
private let logger = Logger(subsystem: "com.coder.Coder-Desktop", category: "proto")
10+
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "proto")
1111

1212
/// Creates an instance using the given `DispatchIO` channel and queue.
1313
init(dispatch: DispatchIO, queue: DispatchQueue) {
@@ -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(

‎Coder Desktop/VPNLib/Speaker.swift

+18-18
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,27 @@ enum ProtoRole: String {
2222
}
2323

2424
/// A version of the VPN protocol that can be negotiated.
25-
struct ProtoVersion: CustomStringConvertible, Equatable, Codable {
25+
public struct ProtoVersion: CustomStringConvertible, Equatable, Codable, Sendable {
2626
let major: Int
2727
let minor: Int
2828

29-
var description: String { "\(major).\(minor)" }
29+
public var description: String { "\(major).\(minor)" }
3030

3131
init(_ major: Int, _ minor: Int) {
3232
self.major = major
3333
self.minor = minor
3434
}
3535

36-
init(parse str: String) throws {
36+
init(parse str: String) throws(HandshakeError) {
3737
let parts = str.split(separator: ".").map { Int($0) }
3838
if parts.count != 2 {
39-
throw HandshakeError.invalidVersion(str)
39+
throw .invalidVersion(str)
4040
}
4141
guard let major = parts[0] else {
42-
throw HandshakeError.invalidVersion(str)
42+
throw .invalidVersion(str)
4343
}
4444
guard let minor = parts[1] else {
45-
throw HandshakeError.invalidVersion(str)
45+
throw .invalidVersion(str)
4646
}
4747
self.major = major
4848
self.minor = minor
@@ -87,14 +87,14 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
8787
}
8888

8989
/// Does the VPN Protocol handshake and validates the result
90-
func handshake() async throws {
90+
public func handshake() async throws {
9191
let hndsh = Handshaker(writeFD: writeFD, dispatch: dispatch, queue: queue, role: role)
9292
// ignore the version for now because we know it can only be 1.0
9393
try _ = await hndsh.handshake()
9494
}
9595

9696
/// Send a unary RPC message and handle the response
97-
func unaryRPC(_ req: SendMsg) async throws -> RecvMsg {
97+
public func unaryRPC(_ req: SendMsg) async throws -> RecvMsg {
9898
return try await withCheckedThrowingContinuation { continuation in
9999
Task { [sender, secretary, logger] in
100100
let msgID = await secretary.record(continuation: continuation)
@@ -114,15 +114,15 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
114114
}
115115
}
116116

117-
func closeWrite() {
117+
public func closeWrite() {
118118
do {
119119
try writeFD.close()
120120
} catch {
121121
logger.error("failed to close write file handle: \(error)")
122122
}
123123
}
124124

125-
func closeRead() {
125+
public func closeRead() {
126126
do {
127127
try readFD.close()
128128
} catch {
@@ -257,7 +257,7 @@ actor Handshaker {
257257
}
258258
}
259259

260-
func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws -> ProtoVersion {
260+
func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws(HandshakeError) -> ProtoVersion {
261261
for our in ours.reversed() {
262262
for their in theirs.reversed() where our.major == their.major {
263263
if our.minor < their.minor {
@@ -266,10 +266,10 @@ func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws -> ProtoVe
266266
return their
267267
}
268268
}
269-
throw HandshakeError.unsupportedVersion(theirs)
269+
throw .unsupportedVersion(theirs)
270270
}
271271

272-
enum HandshakeError: Error {
272+
public enum HandshakeError: Error {
273273
case readError(String)
274274
case invalidHeader(String)
275275
case wrongRole(String)
@@ -278,15 +278,15 @@ enum HandshakeError: Error {
278278
}
279279

280280
public struct RPCRequest<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Sendable>: Sendable {
281-
let msg: RecvMsg
281+
public let msg: RecvMsg
282282
private let sender: Sender<SendMsg>
283283

284284
public init(req: RecvMsg, sender: Sender<SendMsg>) {
285285
msg = req
286286
self.sender = sender
287287
}
288288

289-
func sendReply(_ reply: SendMsg) async throws {
289+
public func sendReply(_ reply: SendMsg) async throws {
290290
var reply = reply
291291
reply.rpc.responseTo = msg.rpc.msgID
292292
try await sender.send(reply)
@@ -326,13 +326,13 @@ actor RPCSecretary<RecvMsg: RPCMessage & Sendable> {
326326

327327
func route(reply: RecvMsg) throws(RPCError) {
328328
guard reply.hasRpc else {
329-
throw RPCError.missingRPC
329+
throw .missingRPC
330330
}
331331
guard reply.rpc.responseTo != 0 else {
332-
throw RPCError.notAResponse
332+
throw .notAResponse
333333
}
334334
guard let cont = continuations[reply.rpc.responseTo] else {
335-
throw RPCError.unknownResponseID(reply.rpc.responseTo)
335+
throw .unknownResponseID(reply.rpc.responseTo)
336336
}
337337
continuations[reply.rpc.responseTo] = nil
338338
cont.resume(returning: reply)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Testing
2+
@testable import VPNLib
3+
4+
@Suite(.timeLimit(.minutes(1)))
5+
struct ConvertTests {
6+
@Test
7+
// swiftlint:disable function_body_length
8+
func convertProtoNetworkSettingsRequest() async throws {
9+
let req: Vpn_NetworkSettingsRequest = .with { req in
10+
req.tunnelRemoteAddress = "10.0.0.1"
11+
req.tunnelOverheadBytes = 20
12+
req.mtu = 1400
13+
14+
req.dnsSettings = .with { dns in
15+
dns.servers = ["8.8.8.8"]
16+
dns.searchDomains = ["example.com"]
17+
dns.domainName = "example.com"
18+
dns.matchDomains = ["example.com"]
19+
dns.matchDomainsNoSearch = false
20+
}
21+
22+
req.ipv4Settings = .with { ipv4 in
23+
ipv4.addrs = ["192.168.1.1"]
24+
ipv4.subnetMasks = ["255.255.255.0"]
25+
ipv4.router = "192.168.1.254"
26+
ipv4.includedRoutes = [
27+
.with { route in
28+
route.destination = "10.0.0.0"
29+
route.mask = "255.0.0.0"
30+
route.router = "192.168.1.254"
31+
},
32+
]
33+
ipv4.excludedRoutes = [
34+
.with { route in
35+
route.destination = "172.16.0.0"
36+
route.mask = "255.240.0.0"
37+
route.router = "192.168.1.254"
38+
},
39+
]
40+
}
41+
42+
req.ipv6Settings = .with { ipv6 in
43+
ipv6.addrs = ["2001:db8::1"]
44+
ipv6.prefixLengths = [64]
45+
ipv6.includedRoutes = [
46+
.with { route in
47+
route.destination = "2001:db8::"
48+
route.router = "2001:db8::1"
49+
route.prefixLength = 64
50+
},
51+
]
52+
ipv6.excludedRoutes = [
53+
.with { route in
54+
route.destination = "2001:0db8:85a3::"
55+
route.router = "2001:db8::1"
56+
route.prefixLength = 128
57+
},
58+
]
59+
}
60+
}
61+
62+
let result = convertNetworkSettingsRequest(req)
63+
#expect(result.tunnelRemoteAddress == "10.0.0.1")
64+
#expect(result.dnsSettings!.servers == ["8.8.8.8"])
65+
#expect(result.dnsSettings!.domainName == "example.com")
66+
#expect(result.ipv4Settings!.addresses == ["192.168.1.1"])
67+
#expect(result.ipv4Settings!.subnetMasks == ["255.255.255.0"])
68+
#expect(result.ipv6Settings!.addresses == ["2001:db8::1"])
69+
#expect(result.ipv6Settings!.networkPrefixLengths == [64])
70+
71+
try #require(result.ipv4Settings!.includedRoutes?.count == 1)
72+
let ipv4IncludedRoute = result.ipv4Settings!.includedRoutes![0]
73+
#expect(ipv4IncludedRoute.destinationAddress == "10.0.0.0")
74+
#expect(ipv4IncludedRoute.destinationSubnetMask == "255.0.0.0")
75+
#expect(ipv4IncludedRoute.gatewayAddress == "192.168.1.254")
76+
77+
try #require(result.ipv4Settings!.excludedRoutes?.count == 1)
78+
let ipv4ExcludedRoute = result.ipv4Settings!.excludedRoutes![0]
79+
#expect(ipv4ExcludedRoute.destinationAddress == "172.16.0.0")
80+
#expect(ipv4ExcludedRoute.destinationSubnetMask == "255.240.0.0")
81+
#expect(ipv4ExcludedRoute.gatewayAddress == "192.168.1.254")
82+
83+
try #require(result.ipv6Settings!.includedRoutes?.count == 1)
84+
let ipv6IncludedRoute = result.ipv6Settings!.includedRoutes![0]
85+
#expect(ipv6IncludedRoute.destinationAddress == "2001:db8::")
86+
#expect(ipv6IncludedRoute.destinationNetworkPrefixLength == 64)
87+
#expect(ipv6IncludedRoute.gatewayAddress == "2001:db8::1")
88+
89+
try #require(result.ipv6Settings!.excludedRoutes?.count == 1)
90+
let ipv6ExcludedRoute = result.ipv6Settings!.excludedRoutes![0]
91+
#expect(ipv6ExcludedRoute.destinationAddress == "2001:0db8:85a3::")
92+
#expect(ipv6ExcludedRoute.destinationNetworkPrefixLength == 128)
93+
#expect(ipv6ExcludedRoute.gatewayAddress == "2001:db8::1")
94+
}
95+
}

‎Coder Desktop/VPNLibTests/DownloaderTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import Testing
44
@testable import VPNLib
55

66
struct NoopValidator: Validator {
7-
func validate(path _: URL) async throws {}
7+
func validate(path _: URL) async throws(ValidationError) {}
88
}
99

10-
@Suite
10+
@Suite(.timeLimit(.minutes(1)))
1111
struct DownloaderTests {
1212
let downloader = Downloader(validator: NoopValidator())
1313

0 commit comments

Comments
 (0)
Please sign in to comment.