Skip to content

fix: concurrently open tunnel & handshake #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions Coder Desktop/Coder Desktop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B3DA12D2D23860099996A /* VPNLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
AA3B3E8E2D2E0FF40099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B3E8D2D2E0FF40099996A /* Mocker */; };
AA3B40992D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AA3B40B62D2FD9DD0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40B52D2FD9DD0099996A /* Mocker */; };
AA3B40B72D2FDA5C0099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AA3B40BD2D2FDFBA0099996A /* Mocker in Frameworks */ = {isa = PBXBuildFile; productRef = AA3B40BC2D2FDFBA0099996A /* Mocker */; };
AA3B40C02D2FE7760099996A /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AA8BC3392D0060A900E1ABAA /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC3382D0060A900E1ABAA /* ViewInspector */; };
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC33E2D0061F200E1ABAA /* FluidMenuBarExtra */; };
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = AA8BC4CE2D00A4B700E1ABAA /* KeychainAccess */; };
AAC382352D427B7600F6DFB4 /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AAC382362D427B7600F6DFB4 /* CoderSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
AAC382392D427B8300F6DFB4 /* CoderSDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; };
AAC3823A2D427B8300F6DFB4 /* CoderSDK.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = AA3B40912D2FC8560099996A /* CoderSDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -105,6 +108,13 @@
remoteGlobalIDString = AA3B40902D2FC8560099996A;
remoteInfo = CoderSDK;
};
AAC382372D427B7600F6DFB4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 961678F42CFF100D00B2B6DF /* Project object */;
proxyType = 1;
remoteGlobalIDString = AA3B40902D2FC8560099996A;
remoteInfo = CoderSDK;
};
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -126,6 +136,18 @@
dstSubfolderSpec = 10;
files = (
AA3B3DCE2D2D249F0099996A /* VPNLib.framework in Embed Frameworks */,
AAC382362D427B7600F6DFB4 /* CoderSDK.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
AAC3823B2D427B8300F6DFB4 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
AAC3823A2D427B8300F6DFB4 /* CoderSDK.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -228,8 +250,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AA3B40A42D2FC8560099996A /* CoderSDK.framework in Frameworks */,
AA8BC4CF2D00A4B700E1ABAA /* KeychainAccess in Frameworks */,
AAC382392D427B8300F6DFB4 /* CoderSDK.framework in Frameworks */,
AA2C690F2D34F6920059AFAF /* LaunchAtLogin in Frameworks */,
AA8BC33F2D0061F200E1ABAA /* FluidMenuBarExtra in Frameworks */,
);
Expand Down Expand Up @@ -257,6 +279,7 @@
buildActionMask = 2147483647;
files = (
961679332CFF117300B2B6DF /* NetworkExtension.framework in Frameworks */,
AAC382352D427B7600F6DFB4 /* CoderSDK.framework in Frameworks */,
AA3B3DCD2D2D249F0099996A /* VPNLib.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -366,6 +389,7 @@
961678F92CFF100D00B2B6DF /* Frameworks */,
961678FA2CFF100D00B2B6DF /* Resources */,
961679422CFF117300B2B6DF /* Embed System Extensions */,
AAC3823B2D427B8300F6DFB4 /* Embed Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -452,6 +476,7 @@
dependencies = (
AA2C69922D354A8B0059AFAF /* PBXTargetDependency */,
AA3B3DD02D2D249F0099996A /* PBXTargetDependency */,
AAC382382D427B7600F6DFB4 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
AA3C69AD2D2D143400A45481 /* VPN */,
Expand Down Expand Up @@ -847,6 +872,11 @@
target = AA3B40902D2FC8560099996A /* CoderSDK */;
targetProxy = AA3B40C22D2FE7760099996A /* PBXContainerItemProxy */;
};
AAC382382D427B7600F6DFB4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = AA3B40902D2FC8560099996A /* CoderSDK */;
targetProxy = AAC382372D427B7600F6DFB4 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
Expand Down Expand Up @@ -1216,6 +1246,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
Expand Down Expand Up @@ -1324,6 +1355,7 @@
buildSettings = {
BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
Expand Down
16 changes: 9 additions & 7 deletions Coder Desktop/Coder Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ struct DesktopApp: App {
EmptyView()
}
Window("Sign In", id: Windows.login.rawValue) {
LoginForm<PreviewSession>().environmentObject(appDelegate.session)
LoginForm<SecureSession>()
.environmentObject(appDelegate.session)
.environmentObject(appDelegate.settings)
}
.windowResizability(.contentSize)
SwiftUI.Settings { SettingsView<PreviewVPN>()
SwiftUI.Settings { SettingsView<CoderVPNService>()
.environmentObject(appDelegate.vpn)
.environmentObject(appDelegate.settings)
}
Expand All @@ -25,20 +27,20 @@ struct DesktopApp: App {
@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBarExtra: FluidMenuBarExtra?
let vpn: PreviewVPN
let session: PreviewSession
let vpn: CoderVPNService
let session: SecureSession
let settings: Settings

override init() {
// TODO: Replace with real implementation
vpn = PreviewVPN()
vpn = CoderVPNService()
settings = Settings()
session = PreviewSession()
session = SecureSession(onChange: vpn.configureTunnelProviderProtocol)
}

func applicationDidFinishLaunching(_: Notification) {
menuBarExtra = FluidMenuBarExtra(title: "Coder Desktop", image: "MenuBarIcon") {
VPNMenu<PreviewVPN, PreviewSession>().frame(width: 256)
VPNMenu<CoderVPNService, SecureSession>().frame(width: 256)
.environmentObject(self.vpn)
.environmentObject(self.session)
.environmentObject(self.settings)
Expand Down
20 changes: 16 additions & 4 deletions Coder Desktop/Coder Desktop/NetworkExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import os

enum NetworkExtensionState: Equatable {
case unconfigured
case disbled
case disabled
case enabled
case failed(String)

var description: String {
switch self {
case .unconfigured:
return "Not logged in to Coder"
return "NetworkExtension not configured, try logging in again"
case .enabled:
return "NetworkExtension tunnel enabled"
case .disbled:
case .disabled:
return "NetworkExtension tunnel disabled"
case let .failed(error):
return "NetworkExtension config failed: \(error)"
Expand All @@ -24,6 +24,16 @@ enum NetworkExtensionState: Equatable {
/// An actor that handles configuring, enabling, and disabling the VPN tunnel via the
/// NetworkExtension APIs.
extension CoderVPNService {
// Updates the UI if a previous configuration exists
func loadNetworkExtension() async {
do {
try await getTunnelManager()
neState = .disabled
} catch {
neState = .unconfigured
}
}

func configureNetworkExtension(proto: NETunnelProviderProtocol) async {
// removing the old tunnels, rather than reconfiguring ensures that configuration changes
// are picked up.
Expand All @@ -47,6 +57,7 @@ extension CoderVPNService {
logger.error("save tunnel failed: \(error)")
neState = .failed(error.localizedDescription)
}
neState = .disabled
}

func removeNetworkExtension() async throws(VPNServiceError) {
Expand Down Expand Up @@ -91,9 +102,10 @@ extension CoderVPNService {
return
}
logger.debug("saved tunnel with enabled=false")
neState = .disbled
neState = .disabled
}

@discardableResult
private func getTunnelManager() async throws(VPNServiceError) -> NETunnelProviderManager {
var tunnels: [NETunnelProviderManager] = []
do {
Expand Down
5 changes: 4 additions & 1 deletion Coder Desktop/Coder Desktop/VPNService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ final class CoderVPNService: NSObject, VPNService {
guard sysExtnState == .installed else {
return .failed(.systemExtensionError(sysExtnState))
}
guard neState == .enabled || neState == .disbled else {
guard neState == .enabled || neState == .disabled else {
return .failed(.networkExtensionError(neState))
}
return tunnelState
Expand All @@ -66,6 +66,9 @@ final class CoderVPNService: NSObject, VPNService {
override init() {
super.init()
installSystemExtension()
Task {
await loadNetworkExtension()
}
Comment on lines +69 to +71
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This avoids prompting the user to configure the system VPN each time they launch the app.

}

var startTask: Task<Void, Never>?
Expand Down
38 changes: 35 additions & 3 deletions Coder Desktop/VPN/Manager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ actor Manager {
.first!.appending(path: "coder-vpn.dylib")
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "manager")

// swiftlint:disable:next function_body_length
init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) {
ptp = with
self.cfg = cfg
#if arch(arm64)
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-arm64.dylib")
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-arm64.dylib")
#elseif arch(x86_64)
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-amd64.dylib")
let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-amd64.dylib")
#else
fatalError("unknown architecture")
#endif
Expand Down Expand Up @@ -60,6 +61,14 @@ actor Manager {
} catch {
throw .handshake(error)
}
do {
try await tunnelHandle.openTunnelTask?.value
} catch let error as TunnelHandleError {
logger.error("failed to wait for dylib to open tunnel: \(error, privacy: .public) ")
throw .tunnelSetup(error)
} catch {
fatalError("openTunnelTask must only throw TunnelHandleError")
}
readLoop = Task { try await run() }
}

Expand Down Expand Up @@ -180,7 +189,7 @@ actor Manager {
}
}

public struct ManagerConfig {
struct ManagerConfig {
let apiToken: String
let serverUrl: URL
}
Expand All @@ -195,6 +204,29 @@ enum ManagerError: Error {
case serverInfo(String)
case errorResponse(msg: String)
case noTunnelFileDescriptor

var description: String {
switch self {
case let .download(err):
return "Download error: \(err)"
case let .tunnelSetup(err):
return "Tunnel setup error: \(err)"
case let .handshake(err):
return "Handshake error: \(err)"
case let .validation(err):
return "Validation error: \(err)"
case .incorrectResponse:
return "Received unexpected response over tunnel"
case let .failedRPC(err):
return "Failed rpc: \(err)"
case let .serverInfo(msg):
return msg
case let .errorResponse(msg):
return msg
case .noTunnelFileDescriptor:
return "Could not find a tunnel file descriptor"
}
}
}

func writeVpnLog(_ log: Vpn_Log) {
Expand Down
21 changes: 14 additions & 7 deletions Coder Desktop/VPN/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import NetworkExtension
import os
import VPNLib

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

public var tunnelFileDescriptor: Int32? {
var tunnelFileDescriptor: Int32? {
var ctlInfo = ctl_info()
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
Expand Down Expand Up @@ -47,19 +48,25 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
completionHandler(nil)
return
}
let completionHandler = CallbackWrapper(completionHandler)
Task {
// TODO: Retrieve access URL & Token via Keychain
manager = try await Manager(
with: self,
cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
)
do throws(ManagerError) {
manager = try await Manager(
with: self,
cfg: .init(apiToken: "fake-token", serverUrl: .init(string: "https://dev.coder.com")!)
)
completionHandler(nil)
} catch {
completionHandler(error)
logger.error("error starting manager: \(error.description, privacy: .public)")
}
}
completionHandler(nil)
}

override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
logger.debug("stopTunnel called")
guard manager == nil else {
guard manager != nil else {
logger.error("stopTunnel called with nil Manager")
completionHandler()
return
Expand Down
Loading