Skip to content

Commit 06b4c16

Browse files
fix: concurrently open tunnel & handshake (#30)
1 parent fdb8545 commit 06b4c16

File tree

9 files changed

+154
-29
lines changed

9 files changed

+154
-29
lines changed

Diff for: Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj

+34-2
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@
1717
AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
1818
AA3B3E8E2D2E0FF40099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3E8D2D2E0FF40099996A /* Mocker */; };
1919
AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
20-
AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
2120
AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40B52D2FD9DD0099996A /* Mocker */; };
2221
AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
2322
AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40BC2D2FDFBA0099996A /* Mocker */; };
2423
AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
2524
AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
2625
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
2726
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
27+
AAC382352D427B7600F6DFB4 /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
28+
AAC382362D427B7600F6DFB4 /* CoderSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
29+
AAC382392D427B8300F6DFB4 /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
30+
AAC3823A2D427B8300F6DFB4 /* CoderSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
2831
/* End PBXBuildFile section */
2932

3033
/* Begin PBXContainerItemProxy section */
@@ -105,6 +108,13 @@
105108
remoteGlobalIDString = AA3B40902D2FC8560099996A;
106109
remoteInfo = CoderSDK;
107110
};
111+
AAC382372D427B7600F6DFB4 /* PBXContainerItemProxy */ = {
112+
isa = PBXContainerItemProxy;
113+
containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
114+
proxyType = 1;
115+
remoteGlobalIDString = AA3B40902D2FC8560099996A;
116+
remoteInfo = CoderSDK;
117+
};
108118
/* End PBXContainerItemProxy section */
109119

110120
/* Begin PBXCopyFilesBuildPhase section */
@@ -126,6 +136,18 @@
126136
dstSubfolderSpec = 10;
127137
files = (
128138
AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */,
139+
AAC382362D427B7600F6DFB4 /* CoderSDK.framework in Embed Frameworks */,
140+
);
141+
name = "Embed Frameworks";
142+
runOnlyForDeploymentPostprocessing = 0;
143+
};
144+
AAC3823B2D427B8300F6DFB4 /* Embed Frameworks */ = {
145+
isa = PBXCopyFilesBuildPhase;
146+
buildActionMask = 2147483647;
147+
dstPath = "";
148+
dstSubfolderSpec = 10;
149+
files = (
150+
AAC3823A2D427B8300F6DFB4 /* CoderSDK.framework in Embed Frameworks */,
129151
);
130152
name = "Embed Frameworks";
131153
runOnlyForDeploymentPostprocessing = 0;
@@ -228,8 +250,8 @@
228250
isa = PBXFrameworksBuildPhase;
229251
buildActionMask = 2147483647;
230252
files = (
231-
AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */,
232253
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
254+
AAC382392D427B8300F6DFB4 /* CoderSDK.framework in Frameworks */,
233255
AA2C690F2D34F6920059AFAF /* LaunchAtLogin in Frameworks */,
234256
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
235257
);
@@ -257,6 +279,7 @@
257279
buildActionMask = 2147483647;
258280
files = (
259281
961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */,
282+
AAC382352D427B7600F6DFB4 /* CoderSDK.framework in Frameworks */,
260283
AA3B3DCD2D2D249F0099996A /* VPNLib.framework in Frameworks */,
261284
);
262285
runOnlyForDeploymentPostprocessing = 0;
@@ -366,6 +389,7 @@
366389
961678F92CFF100D00B2B6DF /* Frameworks */,
367390
961678FA2CFF100D00B2B6DF /* Resources */,
368391
961679422CFF117300B2B6DF /* Embed System Extensions */,
392+
AAC3823B2D427B8300F6DFB4 /* Embed Frameworks */,
369393
);
370394
buildRules = (
371395
);
@@ -452,6 +476,7 @@
452476
dependencies = (
453477
AA2C69922D354A8B0059AFAF /* PBXTargetDependency */,
454478
AA3B3DD02D2D249F0099996A /* PBXTargetDependency */,
479+
AAC382382D427B7600F6DFB4 /* PBXTargetDependency */,
455480
);
456481
fileSystemSynchronizedGroups = (
457482
AA3C69AD2D2D143400A45481 /* VPN */,
@@ -847,6 +872,11 @@
847872
target = AA3B40902D2FC8560099996A /* CoderSDK */;
848873
targetProxy = AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */;
849874
};
875+
AAC382382D427B7600F6DFB4 /* PBXTargetDependency */ = {
876+
isa = PBXTargetDependency;
877+
target = AA3B40902D2FC8560099996A /* CoderSDK */;
878+
targetProxy = AAC382372D427B7600F6DFB4 /* PBXContainerItemProxy */;
879+
};
850880
/* End PBXTargetDependency section */
851881

852882
/* Begin XCBuildConfiguration section */
@@ -1216,6 +1246,7 @@
12161246
buildSettings = {
12171247
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
12181248
CODE_SIGN_IDENTITY = "";
1249+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
12191250
CODE_SIGN_STYLE = Automatic;
12201251
COMBINE_HIDPI_IMAGES = YES;
12211252
CURRENT_PROJECT_VERSION = 1;
@@ -1324,6 +1355,7 @@
13241355
buildSettings = {
13251356
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
13261357
CODE_SIGN_IDENTITY = "";
1358+
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
13271359
CODE_SIGN_STYLE = Automatic;
13281360
COMBINE_HIDPI_IMAGES = YES;
13291361
CURRENT_PROJECT_VERSION = 1;

Diff for: Coder Desktop/Coder Desktop/Coder_DesktopApp.swift

+9-7
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ struct DesktopApp: App {
1111
EmptyView()
1212
}
1313
Window("Sign In", id: Windows.login.rawValue) {
14-
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
14+
LoginForm<SecureSession>()
15+
.environmentObject(appDelegate.session)
16+
.environmentObject(appDelegate.settings)
1517
}
1618
.windowResizability(.contentSize)
17-
SwiftUI.Settings { SettingsView<PreviewVPN>()
19+
SwiftUI.Settings { SettingsView<CoderVPNService>()
1820
.environmentObject(appDelegate.vpn)
1921
.environmentObject(appDelegate.settings)
2022
}
@@ -25,20 +27,20 @@ struct DesktopApp: App {
2527
@MainActor
2628
class AppDelegate: NSObject, NSApplicationDelegate {
2729
private var menuBarExtra: FluidMenuBarExtra?
28-
let vpn: PreviewVPN
29-
let session: PreviewSession
30+
let vpn: CoderVPNService
31+
let session: SecureSession
3032
let settings: Settings
3133

3234
override init() {
3335
// TODO: Replace with real implementation
34-
vpn = PreviewVPN()
36+
vpn = CoderVPNService()
3537
settings = Settings()
36-
session = PreviewSession()
38+
session = SecureSession(onChange: vpn.configureTunnelProviderProtocol)
3739
}
3840

3941
func applicationDidFinishLaunching(_: Notification) {
4042
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
41-
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
43+
VPNMenu<CoderVPNService, SecureSession>().frame(width: 256)
4244
.environmentObject(self.vpn)
4345
.environmentObject(self.session)
4446
.environmentObject(self.settings)

Diff for: Coder Desktop/Coder Desktop/NetworkExtension.swift

+16-4
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import os
33

44
enum NetworkExtensionState: Equatable {
55
case unconfigured
6-
case disbled
6+
case disabled
77
case enabled
88
case failed(String)
99

1010
var description: String {
1111
switch self {
1212
case .unconfigured:
13-
return "Not logged in to Coder"
13+
return "NetworkExtension not configured, try logging in again"
1414
case .enabled:
1515
return "NetworkExtension tunnel enabled"
16-
case .disbled:
16+
case .disabled:
1717
return "NetworkExtension tunnel disabled"
1818
case let .failed(error):
1919
return "NetworkExtension config failed: \(error)"
@@ -24,6 +24,16 @@ enum NetworkExtensionState: Equatable {
2424
/// An actor that handles configuring, enabling, and disabling the VPN tunnel via the
2525
/// NetworkExtension APIs.
2626
extension CoderVPNService {
27+
// Updates the UI if a previous configuration exists
28+
func loadNetworkExtension() async {
29+
do {
30+
try await getTunnelManager()
31+
neState = .disabled
32+
} catch {
33+
neState = .unconfigured
34+
}
35+
}
36+
2737
func configureNetworkExtension(proto: NETunnelProviderProtocol) async {
2838
// removing the old tunnels, rather than reconfiguring ensures that configuration changes
2939
// are picked up.
@@ -47,6 +57,7 @@ extension CoderVPNService {
4757
logger.error("save tunnel failed: \(error)")
4858
neState = .failed(error.localizedDescription)
4959
}
60+
neState = .disabled
5061
}
5162

5263
func removeNetworkExtension() async throws(VPNServiceError) {
@@ -91,9 +102,10 @@ extension CoderVPNService {
91102
return
92103
}
93104
logger.debug("saved tunnel with enabled=false")
94-
neState = .disbled
105+
neState = .disabled
95106
}
96107

108+
@discardableResult
97109
private func getTunnelManager() async throws(VPNServiceError) -> NETunnelProviderManager {
98110
var tunnels: [NETunnelProviderManager] = []
99111
do {

Diff for: Coder Desktop/Coder Desktop/VPNService.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ final class CoderVPNService: NSObject, VPNService {
5050
guard sysExtnState == .installed else {
5151
return .failed(.systemExtensionError(sysExtnState))
5252
}
53-
guard neState == .enabled || neState == .disbled else {
53+
guard neState == .enabled || neState == .disabled else {
5454
return .failed(.networkExtensionError(neState))
5555
}
5656
return tunnelState
@@ -66,6 +66,9 @@ final class CoderVPNService: NSObject, VPNService {
6666
override init() {
6767
super.init()
6868
installSystemExtension()
69+
Task {
70+
await loadNetworkExtension()
71+
}
6972
}
7073

7174
var startTask: Task<Void, Never>?

Diff for: Coder Desktop/VPN/Manager.swift

+35-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ actor Manager {
1616
.first!.appending(path: "coder-vpn.dylib")
1717
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "manager")
1818

19+
// swiftlint:disable:next function_body_length
1920
init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) {
2021
ptp = with
2122
self.cfg = cfg
2223
#if arch(arm64)
23-
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-arm64.dylib")
24+
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-arm64.dylib")
2425
#elseif arch(x86_64)
25-
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-amd64.dylib")
26+
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-amd64.dylib")
2627
#else
2728
fatalError("unknown architecture")
2829
#endif
@@ -60,6 +61,14 @@ actor Manager {
6061
} catch {
6162
throw .handshake(error)
6263
}
64+
do {
65+
try await tunnelHandle.openTunnelTask?.value
66+
} catch let error as TunnelHandleError {
67+
logger.error("failed to wait for dylib to open tunnel: \(error, privacy: .public) ")
68+
throw .tunnelSetup(error)
69+
} catch {
70+
fatalError("openTunnelTask must only throw TunnelHandleError")
71+
}
6372
readLoop = Task { try await run() }
6473
}
6574

@@ -180,7 +189,7 @@ actor Manager {
180189
}
181190
}
182191

183-
public struct ManagerConfig {
192+
struct ManagerConfig {
184193
let apiToken: String
185194
let serverUrl: URL
186195
}
@@ -195,6 +204,29 @@ enum ManagerError: Error {
195204
case serverInfo(String)
196205
case errorResponse(msg: String)
197206
case noTunnelFileDescriptor
207+
208+
var description: String {
209+
switch self {
210+
case let .download(err):
211+
return "Download error: \(err)"
212+
case let .tunnelSetup(err):
213+
return "Tunnel setup error: \(err)"
214+
case let .handshake(err):
215+
return "Handshake error: \(err)"
216+
case let .validation(err):
217+
return "Validation error: \(err)"
218+
case .incorrectResponse:
219+
return "Received unexpected response over tunnel"
220+
case let .failedRPC(err):
221+
return "Failed rpc: \(err)"
222+
case let .serverInfo(msg):
223+
return msg
224+
case let .errorResponse(msg):
225+
return msg
226+
case .noTunnelFileDescriptor:
227+
return "Could not find a tunnel file descriptor"
228+
}
229+
}
198230
}
199231

200232
func writeVpnLog(_ log: Vpn_Log) {

Diff for: Coder Desktop/VPN/PacketTunnelProvider.swift

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import NetworkExtension
22
import os
3+
import VPNLib
34

45
/* From <sys/kern_control.h> */
56
let CTLIOCGINFO: UInt = 0xC064_4E03
@@ -8,7 +9,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
89
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider")
910
private var manager: Manager?
1011

11-
public var tunnelFileDescriptor: Int32? {
12+
var tunnelFileDescriptor: Int32? {
1213
var ctlInfo = ctl_info()
1314
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
1415
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
@@ -47,19 +48,25 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4748
completionHandler(nil)
4849
return
4950
}
51+
let completionHandler = CallbackWrapper(completionHandler)
5052
Task {
5153
// TODO: Retrieve access URL & Token via Keychain
52-
manager = try await Manager(
53-
with: self,
54-
cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
55-
)
54+
do throws(ManagerError) {
55+
manager = try await Manager(
56+
with: self,
57+
cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
58+
)
59+
completionHandler(nil)
60+
} catch {
61+
completionHandler(error)
62+
logger.error("error starting manager: \(error.description, privacy: .public)")
63+
}
5664
}
57-
completionHandler(nil)
5865
}
5966

6067
override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
6168
logger.debug("stopTunnel called")
62-
guard manager == nil else {
69+
guard manager != nil else {
6370
logger.error("stopTunnel called with nil Manager")
6471
completionHandler()
6572
return

0 commit comments

Comments
 (0)