diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index faf15e0..090b5a8 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -4,6 +4,7 @@ import KeychainAccess import NetworkExtension import os import SwiftUI +import VPNLib @MainActor class AppState: ObservableObject { @@ -70,6 +71,14 @@ class AppState: ObservableObject { } } + @Published var useSoftNetIsolation: Bool = UserDefaults.standard.bool(forKey: Keys.useSoftNetIsolation) { + didSet { + reconfigure() + guard persistent else { return } + UserDefaults.standard.set(useSoftNetIsolation, forKey: Keys.useSoftNetIsolation) + } + } + @Published var skipHiddenIconAlert: Bool = UserDefaults.standard.bool(forKey: Keys.skipHiddenIconAlert) { didSet { guard persistent else { return } @@ -81,11 +90,18 @@ class AppState: ObservableObject { if !hasSession { return nil } let proto = NETunnelProviderProtocol() proto.providerBundleIdentifier = "\(appId).VPN" - // HACK: We can't write to the system keychain, and the user keychain - // isn't accessible, so we'll use providerConfiguration, which is over XPC. - proto.providerConfiguration = ["token": sessionToken!] - if useLiteralHeaders, let headers = try? JSONEncoder().encode(literalHeaders) { - proto.providerConfiguration?["literalHeaders"] = headers + + proto.providerConfiguration = [ + // HACK: We can't write to the system keychain, and the user keychain + // isn't accessible, so we'll use providerConfiguration, which + // writes to disk. + VPNConfigurationKeys.token: sessionToken!, + VPNConfigurationKeys.useSoftNetIsolation: useSoftNetIsolation, + ] + if useLiteralHeaders { + proto.providerConfiguration?[ + VPNConfigurationKeys.literalHeaders + ] = literalHeaders.map { ($0.name, $0.value) } } proto.serverAddress = baseAccessURL!.absoluteString return proto @@ -188,6 +204,7 @@ class AppState: ObservableObject { } public func clearSession() { + logger.info("clearing session") hasSession = false sessionToken = nil refreshTask?.cancel() @@ -216,6 +233,7 @@ class AppState: ObservableObject { static let useLiteralHeaders = "UseLiteralHeaders" static let literalHeaders = "LiteralHeaders" + static let useSoftNetIsolation = "UseSoftNetIsolation" static let stopVPNOnQuit = "StopVPNOnQuit" static let startVPNOnLaunch = "StartVPNOnLaunch" diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift index d830e74..158f819 100644 --- a/Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift +++ b/Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift @@ -4,11 +4,30 @@ struct NetworkTab: View { var body: some View { Form { LiteralHeadersSection() + SoftNetIsolationSection() } .formStyle(.grouped) } } +struct SoftNetIsolationSection: View { + @EnvironmentObject var state: AppState + @EnvironmentObject var vpn: VPN + var body: some View { + Section { + Toggle(isOn: $state.useSoftNetIsolation) { + Text("Enable support for corporate VPNs") + if !vpn.state.canBeStarted { Text("Cannot be modified while Coder Connect is enabled.") } + } + Text("This setting loosens the VPN loop protection in Coder Connect, allowing traffic to flow to a " + + "Coder deployment behind a corporate VPN. We only recommend enabling this option if Coder Connect " + + "doesn't work with your Coder deployment behind a corporate VPN.") + .font(.subheadline) + .foregroundStyle(.secondary) + }.disabled(!vpn.state.canBeStarted) + } +} + #if DEBUG #Preview { NetworkTab() diff --git a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift index 3289360..27eabdf 100644 --- a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift +++ b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift @@ -40,11 +40,13 @@ class HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface let startSymbol = "OpenTunnel" + // swiftlint:disable:next function_parameter_count func startDaemon( accessURL: URL, token: String, tun: FileHandle, headers: Data?, + useSoftNetIsolation: Bool, reply: @escaping (Error?) -> Void ) { logger.info("startDaemon called") @@ -57,6 +59,7 @@ class HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface apiToken: token, serverUrl: accessURL, tunFd: tun.fileDescriptor, + useSoftNetIsolation: useSoftNetIsolation, literalHeaders: headers.flatMap { try? JSONDecoder().decode([HTTPHeader].self, from: $0) } ?? [] ) ) diff --git a/Coder-Desktop/Coder-DesktopHelper/Manager.swift b/Coder-Desktop/Coder-DesktopHelper/Manager.swift index e2d47b8..5882741 100644 --- a/Coder-Desktop/Coder-DesktopHelper/Manager.swift +++ b/Coder-Desktop/Coder-DesktopHelper/Manager.swift @@ -160,6 +160,7 @@ actor Manager { resp = try await speaker.unaryRPC( .with { msg in msg.start = .with { req in + req.tunnelUseSoftNetIsolation = cfg.useSoftNetIsolation req.tunnelFileDescriptor = cfg.tunFd req.apiToken = cfg.apiToken req.coderURL = cfg.serverUrl.absoluteString @@ -234,6 +235,7 @@ struct ManagerConfig { let apiToken: String let serverUrl: URL let tunFd: Int32 + let useSoftNetIsolation: Bool let literalHeaders: [HTTPHeader] } diff --git a/Coder-Desktop/VPN/HelperXPCSpeaker.swift b/Coder-Desktop/VPN/HelperXPCSpeaker.swift index 0549fc8..7110ca3 100644 --- a/Coder-Desktop/VPN/HelperXPCSpeaker.swift +++ b/Coder-Desktop/VPN/HelperXPCSpeaker.swift @@ -58,7 +58,13 @@ final class HelperXPCSpeaker: NEXPCInterface, @unchecked Sendable { // These methods are called to start and stop the daemon run by the Helper. extension HelperXPCSpeaker { - func startDaemon(accessURL: URL, token: String, tun: FileHandle, headers: Data?) async throws { + func startDaemon( + accessURL: URL, + token: String, + tun: FileHandle, + headers: Data?, + useSoftNetIsolation: Bool + ) async throws { let conn = connect() return try await withCheckedThrowingContinuation { continuation in guard let proxy = conn.remoteObjectProxyWithErrorHandler({ err in @@ -69,7 +75,13 @@ extension HelperXPCSpeaker { continuation.resume(throwing: XPCError.wrongProxyType) return } - proxy.startDaemon(accessURL: accessURL, token: token, tun: tun, headers: headers) { err in + proxy.startDaemon( + accessURL: accessURL, + token: token, + tun: tun, + headers: headers, + useSoftNetIsolation: useSoftNetIsolation + ) { err in if let error = err { self.logger.error("Failed to start daemon: \(error.localizedDescription, privacy: .public)") continuation.resume(throwing: error) diff --git a/Coder-Desktop/VPN/PacketTunnelProvider.swift b/Coder-Desktop/VPN/PacketTunnelProvider.swift index 6f54381..606255b 100644 --- a/Coder-Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder-Desktop/VPN/PacketTunnelProvider.swift @@ -48,27 +48,31 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { ) async throws { globalHelperXPCSpeaker.ptp = self guard let proto = protocolConfiguration as? NETunnelProviderProtocol, - let baseAccessURL = proto.serverAddress + let accessURL = proto.serverAddress else { logger.error("startTunnel called with nil protocolConfiguration") throw makeNSError(suffix: "PTP", desc: "Missing Configuration") } // HACK: We can't write to the system keychain, and the NE can't read the user keychain. - guard let token = proto.providerConfiguration?["token"] as? String else { + guard let token = proto.providerConfiguration?[VPNConfigurationKeys.token] as? String else { logger.error("startTunnel called with nil token") throw makeNSError(suffix: "PTP", desc: "Missing Token") } - let headers = proto.providerConfiguration?["literalHeaders"] as? Data - logger.debug("retrieved token & access URL") + let headers = proto.providerConfiguration?[VPNConfigurationKeys.literalHeaders] as? Data + let useSoftNetIsolation = proto.providerConfiguration?[ + VPNConfigurationKeys.useSoftNetIsolation + ] as? Bool ?? false + logger.debug("retrieved vpn configuration settings") guard let tunFd = tunnelFileDescriptor else { logger.error("startTunnel called with nil tunnelFileDescriptor") throw makeNSError(suffix: "PTP", desc: "Missing Tunnel File Descriptor") } try await globalHelperXPCSpeaker.startDaemon( - accessURL: .init(string: baseAccessURL)!, + accessURL: .init(string: accessURL)!, token: token, tun: FileHandle(fileDescriptor: tunFd), - headers: headers + headers: headers, + useSoftNetIsolation: useSoftNetIsolation ) } diff --git a/Coder-Desktop/VPNLib/Configuration.swift b/Coder-Desktop/VPNLib/Configuration.swift new file mode 100644 index 0000000..3a93a88 --- /dev/null +++ b/Coder-Desktop/VPNLib/Configuration.swift @@ -0,0 +1,9 @@ +// Keys for the `providerConfiguration` dictionary in the VPN configuration plist. +public enum VPNConfigurationKeys { + // String + public static let token = "token" + // [(String, String)] + public static let literalHeaders = "literalHeaders" + // Bool + public static let useSoftNetIsolation = "useSoftNetIsolation" +} diff --git a/Coder-Desktop/VPNLib/Download.swift b/Coder-Desktop/VPNLib/Download.swift index 16a9203..63b0b96 100644 --- a/Coder-Desktop/VPNLib/Download.swift +++ b/Coder-Desktop/VPNLib/Download.swift @@ -150,15 +150,15 @@ extension DownloadManager: URLSessionDownloadDelegate { } public required convenience init?(coder: NSCoder) { - let written = coder.decodeInt64(forKey: "written") - let total = coder.containsValue(forKey: "total") ? coder.decodeInt64(forKey: "total") : nil + let written = coder.decodeInt64(forKey: Keys.written) + let total = coder.containsValue(forKey: Keys.total) ? coder.decodeInt64(forKey: Keys.total) : nil self.init(totalBytesWritten: written, totalBytesToWrite: total) } public func encode(with coder: NSCoder) { - coder.encode(totalBytesWritten, forKey: "written") + coder.encode(totalBytesWritten, forKey: Keys.written) if let total = totalBytesToWrite { - coder.encode(total, forKey: "total") + coder.encode(total, forKey: Keys.total) } } @@ -169,4 +169,9 @@ extension DownloadManager: URLSessionDownloadDelegate { let total = totalBytesToWrite.map { fmt.string(fromByteCount: $0) } ?? "Unknown" return "\(done) / \(total)" } + + enum Keys { + static let written = "written" + static let total = "total" + } } diff --git a/Coder-Desktop/VPNLib/XPC.swift b/Coder-Desktop/VPNLib/XPC.swift index 3ec3c26..4fae6f9 100644 --- a/Coder-Desktop/VPNLib/XPC.swift +++ b/Coder-Desktop/VPNLib/XPC.swift @@ -25,8 +25,16 @@ public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperN // This is the XPC interface the Helper exposes to the Network Extension. @preconcurrency @objc public protocol HelperNEXPCInterface { - // headers is a JSON `[HTTPHeader]` - func startDaemon(accessURL: URL, token: String, tun: FileHandle, headers: Data?, reply: @escaping (Error?) -> Void) + // swiftlint:disable:next function_parameter_count + func startDaemon( + accessURL: URL, + token: String, + tun: FileHandle, + // headers is a JSON encoded `[HTTPHeader]` + headers: Data?, + useSoftNetIsolation: Bool, + reply: @escaping (Error?) -> Void + ) func stopDaemon(reply: @escaping (Error?) -> Void) } diff --git a/Coder-Desktop/VPNLib/vpn.pb.swift b/Coder-Desktop/VPNLib/vpn.pb.swift index 3f630d0..d569d53 100644 --- a/Coder-Desktop/VPNLib/vpn.pb.swift +++ b/Coder-Desktop/VPNLib/vpn.pb.swift @@ -757,6 +757,8 @@ public struct Vpn_StartRequest: Sendable { public var tunnelFileDescriptor: Int32 = 0 + public var tunnelUseSoftNetIsolation: Bool = false + public var coderURL: String = String() public var apiToken: String = String() @@ -2156,6 +2158,7 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme public static let protoMessageName: String = _protobuf_package + ".StartRequest" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "tunnel_file_descriptor"), + 8: .standard(proto: "tunnel_use_soft_net_isolation"), 2: .standard(proto: "coder_url"), 3: .standard(proto: "api_token"), 4: .same(proto: "headers"), @@ -2177,6 +2180,7 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme case 5: try { try decoder.decodeSingularStringField(value: &self.deviceID) }() case 6: try { try decoder.decodeSingularStringField(value: &self.deviceOs) }() case 7: try { try decoder.decodeSingularStringField(value: &self.coderDesktopVersion) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.tunnelUseSoftNetIsolation) }() default: break } } @@ -2204,11 +2208,15 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme if !self.coderDesktopVersion.isEmpty { try visitor.visitSingularStringField(value: self.coderDesktopVersion, fieldNumber: 7) } + if self.tunnelUseSoftNetIsolation != false { + try visitor.visitSingularBoolField(value: self.tunnelUseSoftNetIsolation, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Vpn_StartRequest, rhs: Vpn_StartRequest) -> Bool { if lhs.tunnelFileDescriptor != rhs.tunnelFileDescriptor {return false} + if lhs.tunnelUseSoftNetIsolation != rhs.tunnelUseSoftNetIsolation {return false} if lhs.coderURL != rhs.coderURL {return false} if lhs.apiToken != rhs.apiToken {return false} if lhs.headers != rhs.headers {return false} diff --git a/Coder-Desktop/VPNLib/vpn.proto b/Coder-Desktop/VPNLib/vpn.proto index 59ea193..bd00027 100644 --- a/Coder-Desktop/VPNLib/vpn.proto +++ b/Coder-Desktop/VPNLib/vpn.proto @@ -213,6 +213,7 @@ message NetworkSettingsResponse { // StartResponse. message StartRequest { int32 tunnel_file_descriptor = 1; + bool tunnel_use_soft_net_isolation = 8; string coder_url = 2; string api_token = 3; // Additional HTTP headers added to all requests diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml index 52056f5..d32092a 100644 --- a/Coder-Desktop/project.yml +++ b/Coder-Desktop/project.yml @@ -181,7 +181,7 @@ targets: # so that macOS stops complaining about the app being run from an # untrusted folder. DEPLOYMENT_LOCATION: YES - DSTROOT: $(LOCAL_APPS_DIR)/Coder + DSTROOT: $(LOCAL_APPS_DIR) INSTALL_PATH: / SKIP_INSTALL: NO LD_RUNPATH_SEARCH_PATHS: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy