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 7e24349

Browse files
committedJan 10, 2025·
chore: add network extension manager
1 parent 4d0b3da commit 7e24349

File tree

10 files changed

+406
-39
lines changed

10 files changed

+406
-39
lines changed
 

Diff for: ‎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
}

Diff for: ‎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

Diff for: ‎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
}

Diff for: ‎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

Diff for: ‎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
}

Diff for: ‎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

Diff for: ‎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+
}

Diff for: ‎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(

Diff for: ‎Coder Desktop/VPNLib/Speaker.swift

+40-25
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(HandshakeError) {
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 {
@@ -188,7 +188,7 @@ actor Handshaker {
188188
}
189189

190190
/// Performs the initial VPN protocol handshake, returning the negotiated `ProtoVersion` that we should use.
191-
func handshake() async throws -> ProtoVersion {
191+
func handshake() async throws(HandshakeError) -> ProtoVersion {
192192
// kick off the read async before we try to write, synchronously, so we don't deadlock, both
193193
// waiting to write with nobody reading.
194194
let readTask = Task {
@@ -201,9 +201,22 @@ actor Handshaker {
201201

202202
let vStr = versions.map { $0.description }.joined(separator: ",")
203203
let ours = String(format: "\(headerPreamble) \(role) \(vStr)\n")
204-
try writeFD.write(contentsOf: ours.data(using: .utf8)!)
204+
do {
205+
try writeFD.write(contentsOf: ours.data(using: .utf8)!)
206+
} catch {
207+
throw HandshakeError.writeError(error)
208+
}
209+
210+
do {
211+
theirData = try await readTask.value
212+
} catch let error as HandshakeError {
213+
throw error
214+
} catch {
215+
// This can't be checked at compile-time, as both Tasks & Continuations can only ever throw
216+
// a type-erased `Error`
217+
fatalError("handleRead must always throw HandshakeError")
218+
}
205219

206-
let theirData = try await readTask.value
207220
guard let theirsString = String(bytes: theirData, encoding: .utf8) else {
208221
throw HandshakeError.invalidHeader("<unparsable: \(theirData)")
209222
}
@@ -216,6 +229,7 @@ actor Handshaker {
216229
}
217230
}
218231

232+
// resumes must only ever throw HandshakeError
219233
private func handleRead(_: Bool, _ data: DispatchData?, _ error: Int32) {
220234
guard error == 0 else {
221235
let errStrPtr = strerror(error)
@@ -235,7 +249,7 @@ actor Handshaker {
235249
dispatch.read(offset: 0, length: 1, queue: queue, ioHandler: handleRead)
236250
}
237251

238-
private func validateHeader(_ header: String) throws -> ProtoVersion {
252+
private func validateHeader(_ header: String) throws(HandshakeError) -> ProtoVersion {
239253
let parts = header.split(separator: " ")
240254
guard parts.count == 3 else {
241255
throw HandshakeError.invalidHeader("expected 3 parts: \(header)")
@@ -252,12 +266,12 @@ actor Handshaker {
252266
}
253267
let theirVersions = try parts[2]
254268
.split(separator: ",")
255-
.map { try ProtoVersion(parse: String($0)) }
269+
.map { v throws(HandshakeError) in try ProtoVersion(parse: String(v)) }
256270
return try pickVersion(ours: versions, theirs: theirVersions)
257271
}
258272
}
259273

260-
func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws -> ProtoVersion {
274+
func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws(HandshakeError) -> ProtoVersion {
261275
for our in ours.reversed() {
262276
for their in theirs.reversed() where our.major == their.major {
263277
if our.minor < their.minor {
@@ -266,27 +280,28 @@ func pickVersion(ours: [ProtoVersion], theirs: [ProtoVersion]) throws -> ProtoVe
266280
return their
267281
}
268282
}
269-
throw HandshakeError.unsupportedVersion(theirs)
283+
throw .unsupportedVersion(theirs)
270284
}
271285

272-
enum HandshakeError: Error {
286+
public enum HandshakeError: Error {
273287
case readError(String)
288+
case writeError(any Error)
274289
case invalidHeader(String)
275290
case wrongRole(String)
276291
case invalidVersion(String)
277292
case unsupportedVersion([ProtoVersion])
278293
}
279294

280295
public struct RPCRequest<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Sendable>: Sendable {
281-
let msg: RecvMsg
296+
public let msg: RecvMsg
282297
private let sender: Sender<SendMsg>
283298

284299
public init(req: RecvMsg, sender: Sender<SendMsg>) {
285300
msg = req
286301
self.sender = sender
287302
}
288303

289-
func sendReply(_ reply: SendMsg) async throws {
304+
public func sendReply(_ reply: SendMsg) async throws {
290305
var reply = reply
291306
reply.rpc.responseTo = msg.rpc.msgID
292307
try await sender.send(reply)
@@ -303,10 +318,10 @@ enum RPCError: Error {
303318

304319
/// An actor to record outgoing RPCs and route their replies to the original sender
305320
actor RPCSecretary<RecvMsg: RPCMessage & Sendable> {
306-
private var continuations: [UInt64: CheckedContinuation<RecvMsg, Error>] = [:]
321+
private var continuations: [UInt64: CheckedContinuation<RecvMsg, any Error>] = [:]
307322
private var nextMsgID: UInt64 = 1
308323

309-
func record(continuation: CheckedContinuation<RecvMsg, Error>) -> UInt64 {
324+
func record(continuation: CheckedContinuation<RecvMsg, any Error>) -> UInt64 {
310325
let id = nextMsgID
311326
nextMsgID += 1
312327
continuations[id] = continuation
@@ -326,13 +341,13 @@ actor RPCSecretary<RecvMsg: RPCMessage & Sendable> {
326341

327342
func route(reply: RecvMsg) throws(RPCError) {
328343
guard reply.hasRpc else {
329-
throw RPCError.missingRPC
344+
throw .missingRPC
330345
}
331346
guard reply.rpc.responseTo != 0 else {
332-
throw RPCError.notAResponse
347+
throw .notAResponse
333348
}
334349
guard let cont = continuations[reply.rpc.responseTo] else {
335-
throw RPCError.unknownResponseID(reply.rpc.responseTo)
350+
throw .unknownResponseID(reply.rpc.responseTo)
336351
}
337352
continuations[reply.rpc.responseTo] = nil
338353
cont.resume(returning: reply)

Diff for: ‎Coder Desktop/VPNLibTests/ConvertTests.swift

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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 == req.tunnelRemoteAddress)
64+
#expect(result.dnsSettings!.servers == req.dnsSettings.servers)
65+
#expect(result.dnsSettings!.domainName == req.dnsSettings.domainName)
66+
#expect(result.ipv4Settings!.addresses == req.ipv4Settings.addrs)
67+
#expect(result.ipv4Settings!.subnetMasks == req.ipv4Settings.subnetMasks)
68+
#expect(result.ipv6Settings!.addresses == req.ipv6Settings.addrs)
69+
#expect(result.ipv6Settings!.networkPrefixLengths == [64])
70+
71+
try #require(result.ipv4Settings!.includedRoutes?.count == req.ipv4Settings.includedRoutes.count)
72+
let ipv4IncludedRoute = result.ipv4Settings!.includedRoutes![0]
73+
let expectedIpv4IncludedRoute = req.ipv4Settings.includedRoutes[0]
74+
#expect(ipv4IncludedRoute.destinationAddress == expectedIpv4IncludedRoute.destination)
75+
#expect(ipv4IncludedRoute.destinationSubnetMask == expectedIpv4IncludedRoute.mask)
76+
#expect(ipv4IncludedRoute.gatewayAddress == expectedIpv4IncludedRoute.router)
77+
78+
try #require(result.ipv4Settings!.excludedRoutes?.count == req.ipv4Settings.excludedRoutes.count)
79+
let ipv4ExcludedRoute = result.ipv4Settings!.excludedRoutes![0]
80+
let expectedIpv4ExcludedRoute = req.ipv4Settings.excludedRoutes[0]
81+
#expect(ipv4ExcludedRoute.destinationAddress == expectedIpv4ExcludedRoute.destination)
82+
#expect(ipv4ExcludedRoute.destinationSubnetMask == expectedIpv4ExcludedRoute.mask)
83+
#expect(ipv4ExcludedRoute.gatewayAddress == expectedIpv4ExcludedRoute.router)
84+
85+
try #require(result.ipv6Settings!.includedRoutes?.count == req.ipv6Settings.includedRoutes.count)
86+
let ipv6IncludedRoute = result.ipv6Settings!.includedRoutes![0]
87+
let expectedIpv6IncludedRoute = req.ipv6Settings.includedRoutes[0]
88+
#expect(ipv6IncludedRoute.destinationAddress == expectedIpv6IncludedRoute.destination)
89+
#expect(ipv6IncludedRoute.destinationNetworkPrefixLength == 64)
90+
#expect(ipv6IncludedRoute.gatewayAddress == expectedIpv6IncludedRoute.router)
91+
92+
try #require(result.ipv6Settings!.excludedRoutes?.count == req.ipv6Settings.excludedRoutes.count)
93+
let ipv6ExcludedRoute = result.ipv6Settings!.excludedRoutes![0]
94+
let expectedIpv6ExcludedRoute = req.ipv6Settings.excludedRoutes[0]
95+
#expect(ipv6ExcludedRoute.destinationAddress == expectedIpv6ExcludedRoute.destination)
96+
#expect(ipv6ExcludedRoute.destinationNetworkPrefixLength == 128)
97+
#expect(ipv6ExcludedRoute.gatewayAddress == expectedIpv6ExcludedRoute.router)
98+
}
99+
}

0 commit comments

Comments
 (0)
Please sign in to comment.