Skip to content

Commit f3123f1

Browse files
chore: pass session token to network extension (#34)
1 parent 511bafd commit f3123f1

12 files changed

+256
-159
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,8 @@ xcuserdata
290290
/*.gcno
291291
**/xcshareddata/WorkspaceSettings.xcsettings
292292

293+
### VSCode & Sweetpad ###
294+
.vscode/**
295+
buildServer.json
296+
293297
# End of https://www.toptal.com/developers/gitignore/api/xcode,jetbrains,macos,direnv,swift,swiftpm,objective-c

Coder Desktop/.swiftformat

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--selfrequired log,info,error,debug,critical,fault
2+
--exclude **.pb.swift
3+
--condassignment always

Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ struct DesktopApp: App {
1616
.environmentObject(appDelegate.settings)
1717
}
1818
.windowResizability(.contentSize)
19-
SwiftUI.Settings { SettingsView<CoderVPNService>()
20-
.environmentObject(appDelegate.vpn)
21-
.environmentObject(appDelegate.settings)
19+
SwiftUI.Settings {
20+
SettingsView<CoderVPNService>()
21+
.environmentObject(appDelegate.vpn)
22+
.environmentObject(appDelegate.settings)
2223
}
2324
.windowResizability(.contentSize)
2425
}
@@ -32,7 +33,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3233
let settings: Settings
3334

3435
override init() {
35-
// TODO: Replace with real implementation
3636
vpn = CoderVPNService()
3737
settings = Settings()
3838
session = SecureSession(onChange: vpn.configureTunnelProviderProtocol)

Coder Desktop/Coder Desktop/State.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ class SecureSession: ObservableObject, Session {
4141
if !hasSession { return nil }
4242
let proto = NETunnelProviderProtocol()
4343
proto.providerBundleIdentifier = "\(appId).VPN"
44-
proto.passwordReference = keychain[attributes: Keys.sessionToken]?.persistentRef
44+
// HACK: We can't write to the system keychain, and the user keychain
45+
// isn't accessible, so we'll use providerConfiguration, which is over XPC.
46+
proto.providerConfiguration = ["token": sessionToken!]
4547
proto.serverAddress = baseAccessURL!.absoluteString
4648
return proto
4749
}

Coder Desktop/VPN/Manager.swift

+19-8
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ actor Manager {
7979
case let .message(msg):
8080
handleMessage(msg)
8181
case let .RPC(rpc):
82-
handleRPC(rpc)
82+
await handleRPC(rpc)
8383
}
8484
}
8585
} catch {
86-
logger.error("tunnel read loop failed: \(error)")
86+
logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)")
8787
try await tunnelHandle.close()
8888
// TODO: Notify app over XPC
8989
return
@@ -108,21 +108,33 @@ actor Manager {
108108
}
109109
}
110110

111-
func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) {
111+
func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) async {
112112
guard let msgType = rpc.msg.msg else {
113113
logger.critical("received rpc with no type")
114114
return
115115
}
116116
switch msgType {
117117
case let .networkSettings(ns):
118-
let neSettings = convertNetworkSettingsRequest(ns)
119-
ptp.setTunnelNetworkSettings(neSettings)
118+
do {
119+
try await ptp.applyTunnelNetworkSettings(ns)
120+
try? await rpc.sendReply(.with { resp in
121+
resp.networkSettings = .with { settings in
122+
settings.success = true
123+
}
124+
})
125+
} catch {
126+
try? await rpc.sendReply(.with { resp in
127+
resp.networkSettings = .with { settings in
128+
settings.success = false
129+
settings.errorMessage = error.localizedDescription
130+
}
131+
})
132+
}
120133
case .log, .peerUpdate, .start, .stop:
121134
logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
122135
}
123136
}
124137

125-
// TODO: Call via XPC
126138
func startVPN() async throws(ManagerError) {
127139
logger.info("sending start rpc")
128140
guard let tunFd = ptp.tunnelFileDescriptor else {
@@ -149,7 +161,6 @@ actor Manager {
149161
// TODO: notify app over XPC
150162
}
151163

152-
// TODO: Call via XPC
153164
func stopVPN() async throws(ManagerError) {
154165
logger.info("sending stop rpc")
155166
let resp: Vpn_TunnelMessage
@@ -246,5 +257,5 @@ func writeVpnLog(_ log: Vpn_Log) {
246257
category: log.loggerNames.joined(separator: ".")
247258
)
248259
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
249-
logger.log(level: level, "\(log.message): \(fields)")
260+
logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
250261
}

Coder Desktop/VPN/PacketTunnelProvider.swift

+71-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ let CTLIOCGINFO: UInt = 0xC064_4E03
88
class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
99
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider")
1010
private var manager: Manager?
11+
// a `tunnelRemoteAddress` is required, but not currently used.
12+
private var currentSettings: NEPacketTunnelNetworkSettings = .init(tunnelRemoteAddress: "127.0.0.1")
1113

1214
var tunnelFileDescriptor: Int32? {
1315
var ctlInfo = ctl_info()
@@ -41,21 +43,42 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4143
return nil
4244
}
4345

44-
override func startTunnel(options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) {
46+
override func startTunnel(
47+
options _: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void
48+
) {
4549
logger.debug("startTunnel called")
4650
guard manager == nil else {
4751
logger.error("startTunnel called with non-nil Manager")
48-
completionHandler(nil)
52+
completionHandler(PTPError.alreadyRunning)
4953
return
5054
}
55+
guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
56+
let baseAccessURL = proto.serverAddress
57+
else {
58+
logger.error("startTunnel called with nil protocolConfiguration")
59+
completionHandler(PTPError.missingConfiguration)
60+
return
61+
}
62+
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
63+
guard let token = proto.providerConfiguration?["token"] as? String else {
64+
logger.error("startTunnel called with nil token")
65+
completionHandler(PTPError.missingToken)
66+
return
67+
}
68+
logger.debug("retrieved token & access URL")
5169
let completionHandler = CallbackWrapper(completionHandler)
5270
Task {
53-
// TODO: Retrieve access URL & Token via Keychain
5471
do throws(ManagerError) {
72+
logger.debug("creating manager")
5573
manager = try await Manager(
5674
with: self,
57-
cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
75+
cfg: .init(
76+
apiToken: token, serverUrl: .init(string: baseAccessURL)!
77+
)
5878
)
79+
logger.debug("starting vpn")
80+
try await manager!.startVPN()
81+
logger.info("vpn started")
5982
completionHandler(nil)
6083
} catch {
6184
completionHandler(error)
@@ -64,15 +87,26 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
6487
}
6588
}
6689

67-
override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
90+
override func stopTunnel(
91+
with _: NEProviderStopReason, completionHandler: @escaping () -> Void
92+
) {
6893
logger.debug("stopTunnel called")
69-
guard manager != nil else {
94+
guard let manager else {
7095
logger.error("stopTunnel called with nil Manager")
7196
completionHandler()
7297
return
7398
}
74-
manager = nil
75-
completionHandler()
99+
100+
let completionHandler = CompletionWrapper(completionHandler)
101+
Task { [manager] in
102+
do throws(ManagerError) {
103+
try await manager.stopVPN()
104+
} catch {
105+
logger.error("error stopping manager: \(error.description, privacy: .public)")
106+
}
107+
completionHandler()
108+
}
109+
self.manager = nil
76110
}
77111

78112
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
@@ -92,4 +126,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
92126
// Add code here to wake up.
93127
logger.debug("wake called")
94128
}
129+
130+
// Wrapper around `setTunnelNetworkSettings` that supports merging updates
131+
func applyTunnelNetworkSettings(_ diff: Vpn_NetworkSettingsRequest) async throws {
132+
logger.debug("applying settings diff: \(diff.debugDescription, privacy: .public)")
133+
134+
if diff.hasDnsSettings {
135+
currentSettings.dnsSettings = convertDnsSettings(diff.dnsSettings)
136+
}
137+
138+
if diff.mtu != 0 {
139+
currentSettings.mtu = NSNumber(value: diff.mtu)
140+
}
141+
142+
if diff.hasIpv4Settings {
143+
currentSettings.ipv4Settings = convertIPv4Settings(diff.ipv4Settings)
144+
}
145+
if diff.hasIpv6Settings {
146+
currentSettings.ipv6Settings = convertIPv6Settings(diff.ipv6Settings)
147+
}
148+
149+
logger.info("applying settings: \(self.currentSettings.debugDescription, privacy: .public)")
150+
try await setTunnelNetworkSettings(currentSettings)
151+
}
152+
}
153+
154+
enum PTPError: Error {
155+
case alreadyRunning
156+
case missingConfiguration
157+
case missingToken
95158
}

Coder Desktop/VPNLib/Convert.swift

+43-42
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,61 @@
11
import NetworkExtension
22
import os
33

4-
// swiftlint:disable:next 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)
4+
public func convertDnsSettings(_ req: Vpn_NetworkSettingsRequest.DNSSettings) -> NEDNSSettings {
5+
let dnsSettings = NEDNSSettings(servers: req.servers)
6+
dnsSettings.searchDomains = req.searchDomains
7+
dnsSettings.domainName = req.domainName
8+
dnsSettings.matchDomains = req.matchDomains
9+
dnsSettings.matchDomainsNoSearch = req.matchDomainsNoSearch
10+
return dnsSettings
11+
}
912

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
13+
public func convertIPv4Settings(_ req: Vpn_NetworkSettingsRequest.IPv4Settings) -> NEIPv4Settings {
14+
let ipv4Settings = NEIPv4Settings(addresses: req.addrs, subnetMasks: req.subnetMasks)
15+
if !req.router.isEmpty {
16+
ipv4Settings.router = req.router
1717
}
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)
18+
ipv4Settings.includedRoutes = req.includedRoutes.map {
19+
let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
20+
if !$0.router.isEmpty {
2421
route.gatewayAddress = $0.router
25-
return route
2622
}
27-
ipv4Settings.excludedRoutes = req.ipv4Settings.excludedRoutes.map {
28-
let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
23+
return route
24+
}
25+
ipv4Settings.excludedRoutes = req.excludedRoutes.map {
26+
let route = NEIPv4Route(destinationAddress: $0.destination, subnetMask: $0.mask)
27+
if !$0.router.isEmpty {
2928
route.gatewayAddress = $0.router
30-
return route
3129
}
32-
networkSettings.ipv4Settings = ipv4Settings
30+
return route
3331
}
32+
return ipv4Settings
33+
}
3434

35-
if req.hasIpv6Settings {
36-
let ipv6Settings = NEIPv6Settings(
37-
addresses: req.ipv6Settings.addrs,
38-
networkPrefixLengths: req.ipv6Settings.prefixLengths.map { NSNumber(value: $0)
39-
}
35+
public func convertIPv6Settings(_ req: Vpn_NetworkSettingsRequest.IPv6Settings) -> NEIPv6Settings {
36+
let ipv6Settings = NEIPv6Settings(
37+
addresses: req.addrs,
38+
networkPrefixLengths: req.prefixLengths.map { NSNumber(value: $0) }
39+
)
40+
ipv6Settings.includedRoutes = req.includedRoutes.map {
41+
let route = NEIPv6Route(
42+
destinationAddress: $0.destination,
43+
networkPrefixLength: NSNumber(value: $0.prefixLength)
4044
)
41-
ipv6Settings.includedRoutes = req.ipv6Settings.includedRoutes.map {
42-
let route = NEIPv6Route(
43-
destinationAddress: $0.destination,
44-
networkPrefixLength: NSNumber(value: $0.prefixLength)
45-
)
45+
if !$0.router.isEmpty {
4646
route.gatewayAddress = $0.router
47-
return route
4847
}
49-
ipv6Settings.excludedRoutes = req.ipv6Settings.excludedRoutes.map {
50-
let route = NEIPv6Route(
51-
destinationAddress: $0.destination,
52-
networkPrefixLength: NSNumber(value: $0.prefixLength)
53-
)
48+
return route
49+
}
50+
ipv6Settings.excludedRoutes = req.excludedRoutes.map {
51+
let route = NEIPv6Route(
52+
destinationAddress: $0.destination,
53+
networkPrefixLength: NSNumber(value: $0.prefixLength)
54+
)
55+
if !$0.router.isEmpty {
5456
route.gatewayAddress = $0.router
55-
return route
5657
}
57-
networkSettings.ipv6Settings = ipv6Settings
58+
return route
5859
}
59-
return networkSettings
60+
return ipv6Settings
6061
}

Coder Desktop/VPNLib/Receiver.swift

+1-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import SwiftProtobuf
66
actor Receiver<RecvMsg: Message> {
77
private let dispatch: DispatchIO
88
private let queue: DispatchQueue
9-
private var running = false
109
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "proto")
1110

1211
/// Creates an instance using the given `DispatchIO` channel and queue.
@@ -58,11 +57,7 @@ actor Receiver<RecvMsg: Message> {
5857
/// Starts reading protocol messages from the `DispatchIO` channel and returns them as an `AsyncStream` of messages.
5958
/// On read or decoding error, it logs and closes the stream.
6059
func messages() throws(ReceiveError) -> AsyncStream<RecvMsg> {
61-
if running {
62-
throw .alreadyRunning
63-
}
64-
running = true
65-
return AsyncStream(
60+
AsyncStream(
6661
unfolding: {
6762
do {
6863
let length = try await self.readLen()
@@ -83,7 +78,6 @@ actor Receiver<RecvMsg: Message> {
8378
enum ReceiveError: Error {
8479
case readError(String)
8580
case invalidLength
86-
case alreadyRunning
8781
}
8882

8983
func deserializeLen(_ data: Data) throws -> UInt32 {

Coder Desktop/VPNLib/Speaker.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ public actor Speaker<SendMsg: RPCMessage & Message, RecvMsg: RPCMessage & Messag
7979
}
8080
)
8181
receiver = Receiver(dispatch: dispatch, queue: queue)
82-
if SendMsg.self == Vpn_TunnelMessage.self {
83-
role = .tunnel
82+
role = if SendMsg.self == Vpn_TunnelMessage.self {
83+
.tunnel
8484
} else {
85-
role = .manager
85+
.manager
8686
}
8787
}
8888

0 commit comments

Comments
 (0)