Skip to content

Commit deb773a

Browse files
committed
Merge branch 'main' into colin/xpc
2 parents 31f0b3b + f3123f1 commit deb773a

File tree

11 files changed

+249
-167
lines changed

11 files changed

+249
-167
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/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-6
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,11 @@ actor Manager {
8080
case let .message(msg):
8181
handleMessage(msg)
8282
case let .RPC(rpc):
83-
handleRPC(rpc)
83+
await handleRPC(rpc)
8484
}
8585
}
8686
} catch {
87-
logger.error("tunnel read loop failed: \(error)")
87+
logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)")
8888
try await tunnelHandle.close()
8989
if let conn = globalXPCListenerDelegate.getActiveConnection() {
9090
conn.onError(error as NSError)
@@ -120,15 +120,28 @@ actor Manager {
120120
}
121121
}
122122

123-
func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) {
123+
func handleRPC(_ rpc: RPCRequest<Vpn_ManagerMessage, Vpn_TunnelMessage>) async {
124124
guard let msgType = rpc.msg.msg else {
125125
logger.critical("received rpc with no type")
126126
return
127127
}
128128
switch msgType {
129129
case let .networkSettings(ns):
130-
let neSettings = convertNetworkSettingsRequest(ns)
131-
ptp.setTunnelNetworkSettings(neSettings)
130+
do {
131+
try await ptp.applyTunnelNetworkSettings(ns)
132+
try? await rpc.sendReply(.with { resp in
133+
resp.networkSettings = .with { settings in
134+
settings.success = true
135+
}
136+
})
137+
} catch {
138+
try? await rpc.sendReply(.with { resp in
139+
resp.networkSettings = .with { settings in
140+
settings.success = false
141+
settings.errorMessage = error.localizedDescription
142+
}
143+
})
144+
}
132145
case .log, .peerUpdate, .start, .stop:
133146
logger.critical("received unexpected rpc: `\(String(describing: msgType))`")
134147
}
@@ -262,5 +275,5 @@ func writeVpnLog(_ log: Vpn_Log) {
262275
category: log.loggerNames.joined(separator: ".")
263276
)
264277
let fields = log.fields.map { "\($0.name): \($0.value)" }.joined(separator: ", ")
265-
logger.log(level: level, "\(log.message): \(fields)")
278+
logger.log(level: level, "\(log.message, privacy: .public): \(fields, privacy: .public)")
266279
}

Coder Desktop/VPN/PacketTunnelProvider.swift

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

1315
var tunnelFileDescriptor: Int32? {
1416
var ctlInfo = ctl_info()
@@ -48,25 +50,37 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4850
logger.info("startTunnel called")
4951
guard manager == nil else {
5052
logger.error("startTunnel called with non-nil Manager")
51-
completionHandler(nil)
53+
completionHandler(PTPError.alreadyRunning)
5254
return
5355
}
56+
guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
57+
let baseAccessURL = proto.serverAddress
58+
else {
59+
logger.error("startTunnel called with nil protocolConfiguration")
60+
completionHandler(PTPError.missingConfiguration)
61+
return
62+
}
63+
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
64+
guard let token = proto.providerConfiguration?["token"] as? String else {
65+
logger.error("startTunnel called with nil token")
66+
completionHandler(PTPError.missingToken)
67+
return
68+
}
69+
logger.debug("retrieved token & access URL")
5470
let completionHandler = CallbackWrapper(completionHandler)
5571
Task {
56-
// TODO: Retrieve access URL & Token via Keychain
5772
do throws(ManagerError) {
58-
logger.info("creating manager")
73+
logger.debug("creating manager")
5974
manager = try await Manager(
6075
with: self,
6176
cfg: .init(
62-
apiToken: "qGg1rDGWzL-a814TWDGcTDOs4AX7laDEI",
63-
serverUrl: .init(string: "https://dev.coder.com")!
77+
apiToken: token, serverUrl: .init(string: baseAccessURL)!
6478
)
6579
)
6680
globalXPCListenerDelegate.vpnXPCInterface.setManager(manager)
67-
logger.debug("calling manager.startVPN")
68-
// try await manager!.startVPN()
69-
logger.debug("vpn started")
81+
logger.debug("starting vpn")
82+
try await manager!.startVPN()
83+
logger.info("vpn started")
7084
if let conn = globalXPCListenerDelegate.getActiveConnection() {
7185
conn.onStart()
7286
} else {
@@ -75,6 +89,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
7589
completionHandler(nil)
7690
} catch {
7791
logger.error("error starting manager: \(error.description, privacy: .public)")
92+
if let conn = globalXPCListenerDelegate.getActiveConnection() {
93+
conn.onError(error as NSError)
94+
} else {
95+
logger.info("no active connection")
96+
}
7897
completionHandler(error as NSError)
7998
}
8099
}
@@ -84,30 +103,28 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
84103
with _: NEProviderStopReason, completionHandler: @escaping () -> Void
85104
) {
86105
logger.debug("stopTunnel called")
87-
guard manager != nil else {
106+
guard let manager else {
88107
logger.error("stopTunnel called with nil Manager")
89108
completionHandler()
90109
return
91110
}
92111

93-
if let conn = globalXPCListenerDelegate.getActiveConnection() {
94-
conn.onStop()
95-
} else {
96-
logger.info("no active connection")
97-
}
98-
99-
let managerCopy = manager
100-
Task {
112+
let completionHandler = CompletionWrapper(completionHandler)
113+
Task { [manager] in
101114
do throws(ManagerError) {
102-
try await managerCopy?.stopVPN()
115+
try await manager.stopVPN()
103116
} catch {
104117
logger.error("error stopping manager: \(error.description, privacy: .public)")
105118
}
119+
if let conn = globalXPCListenerDelegate.getActiveConnection() {
120+
conn.onStop()
121+
} else {
122+
logger.info("no active connection")
123+
}
124+
globalXPCListenerDelegate.vpnXPCInterface.setManager(nil)
125+
completionHandler()
106126
}
107-
108-
manager = nil
109-
globalXPCListenerDelegate.vpnXPCInterface.setManager(nil)
110-
completionHandler()
127+
self.manager = nil
111128
}
112129

113130
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
@@ -127,4 +144,33 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
127144
// Add code here to wake up.
128145
logger.debug("wake called")
129146
}
147+
148+
// Wrapper around `setTunnelNetworkSettings` that supports merging updates
149+
func applyTunnelNetworkSettings(_ diff: Vpn_NetworkSettingsRequest) async throws {
150+
logger.debug("applying settings diff: \(diff.debugDescription, privacy: .public)")
151+
152+
if diff.hasDnsSettings {
153+
currentSettings.dnsSettings = convertDnsSettings(diff.dnsSettings)
154+
}
155+
156+
if diff.mtu != 0 {
157+
currentSettings.mtu = NSNumber(value: diff.mtu)
158+
}
159+
160+
if diff.hasIpv4Settings {
161+
currentSettings.ipv4Settings = convertIPv4Settings(diff.ipv4Settings)
162+
}
163+
if diff.hasIpv6Settings {
164+
currentSettings.ipv6Settings = convertIPv6Settings(diff.ipv6Settings)
165+
}
166+
167+
logger.info("applying settings: \(self.currentSettings.debugDescription, privacy: .public)")
168+
try await setTunnelNetworkSettings(currentSettings)
169+
}
170+
}
171+
172+
enum PTPError: Error {
173+
case alreadyRunning
174+
case missingConfiguration
175+
case missingToken
130176
}

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

Coder Desktop/VPNLib/Util.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
public final class CallbackWrapper<T, U>: @unchecked Sendable {
1+
public struct CallbackWrapper<T, U>: @unchecked Sendable {
22
private let block: (T?) -> U
33

44
public init(_ block: @escaping (T?) -> U) {
55
self.block = block
66
}
77

88
public func callAsFunction(_ error: T?) -> U {
9-
// Just forward to the original block
109
block(error)
1110
}
1211
}
12+
13+
public struct CompletionWrapper<T>: @unchecked Sendable {
14+
private let block: () -> T
15+
16+
public init(_ block: @escaping () -> T) {
17+
self.block = block
18+
}
19+
20+
public func callAsFunction() -> T {
21+
block()
22+
}
23+
}

0 commit comments

Comments
 (0)