Skip to content

Commit 4bb3a24

Browse files
committed
fix: add toggle for Coder deployments behind a VPN
1 parent 5bf788f commit 4bb3a24

File tree

12 files changed

+109
-20
lines changed

12 files changed

+109
-20
lines changed

Coder-Desktop/Coder-Desktop/State.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import KeychainAccess
44
import NetworkExtension
55
import os
66
import SwiftUI
7+
import VPNLib
78

89
@MainActor
910
class AppState: ObservableObject {
@@ -70,6 +71,14 @@ class AppState: ObservableObject {
7071
}
7172
}
7273

74+
@Published var useSoftNetIsolation: Bool = UserDefaults.standard.bool(forKey: Keys.useSoftNetIsolation) {
75+
didSet {
76+
reconfigure()
77+
guard persistent else { return }
78+
UserDefaults.standard.set(useSoftNetIsolation, forKey: Keys.useSoftNetIsolation)
79+
}
80+
}
81+
7382
@Published var skipHiddenIconAlert: Bool = UserDefaults.standard.bool(forKey: Keys.skipHiddenIconAlert) {
7483
didSet {
7584
guard persistent else { return }
@@ -81,11 +90,18 @@ class AppState: ObservableObject {
8190
if !hasSession { return nil }
8291
let proto = NETunnelProviderProtocol()
8392
proto.providerBundleIdentifier = "\(appId).VPN"
84-
// HACK: We can't write to the system keychain, and the user keychain
85-
// isn't accessible, so we'll use providerConfiguration, which is over XPC.
86-
proto.providerConfiguration = ["token": sessionToken!]
87-
if useLiteralHeaders, let headers = try? JSONEncoder().encode(literalHeaders) {
88-
proto.providerConfiguration?["literalHeaders"] = headers
93+
94+
proto.providerConfiguration = [
95+
// HACK: We can't write to the system keychain, and the user keychain
96+
// isn't accessible, so we'll use providerConfiguration, which
97+
// writes to disk.
98+
VPNConfigurationKeys.token: sessionToken!,
99+
VPNConfigurationKeys.useSoftNetIsolation: useSoftNetIsolation,
100+
]
101+
if useLiteralHeaders {
102+
proto.providerConfiguration?[
103+
VPNConfigurationKeys.literalHeaders
104+
] = literalHeaders.map { ($0.name, $0.value) }
89105
}
90106
proto.serverAddress = baseAccessURL!.absoluteString
91107
return proto
@@ -188,6 +204,7 @@ class AppState: ObservableObject {
188204
}
189205

190206
public func clearSession() {
207+
logger.info("clearing session")
191208
hasSession = false
192209
sessionToken = nil
193210
refreshTask?.cancel()
@@ -216,6 +233,7 @@ class AppState: ObservableObject {
216233

217234
static let useLiteralHeaders = "UseLiteralHeaders"
218235
static let literalHeaders = "LiteralHeaders"
236+
static let useSoftNetIsolation = "UseSoftNetIsolation"
219237
static let stopVPNOnQuit = "StopVPNOnQuit"
220238
static let startVPNOnLaunch = "StartVPNOnLaunch"
221239

Coder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,30 @@ struct NetworkTab<VPN: VPNService>: View {
44
var body: some View {
55
Form {
66
LiteralHeadersSection<VPN>()
7+
SoftNetIsolationSection<VPN>()
78
}
89
.formStyle(.grouped)
910
}
1011
}
1112

13+
struct SoftNetIsolationSection<VPN: VPNService>: View {
14+
@EnvironmentObject var state: AppState
15+
@EnvironmentObject var vpn: VPN
16+
var body: some View {
17+
Section {
18+
Toggle(isOn: $state.useSoftNetIsolation) {
19+
Text("Enable support for corporate VPNs")
20+
if !vpn.state.canBeStarted { Text("Cannot be modified while Coder Connect is enabled.") }
21+
}
22+
Text("This setting loosens the VPN loop protection in Coder Connect, allowing traffic to flow to a " +
23+
"Coder deployment behind a corporate VPN. We only recommend enabling this option if Coder Connect " +
24+
"doesn't work with your Coder deployment behind a corporate VPN.")
25+
.font(.subheadline)
26+
.foregroundStyle(.secondary)
27+
}.disabled(!vpn.state.canBeStarted)
28+
}
29+
}
30+
1231
#if DEBUG
1332
#Preview {
1433
NetworkTab<PreviewVPN>()

Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ class HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface
4040

4141
let startSymbol = "OpenTunnel"
4242

43+
// swiftlint:disable:next function_parameter_count
4344
func startDaemon(
4445
accessURL: URL,
4546
token: String,
4647
tun: FileHandle,
4748
headers: Data?,
49+
useSoftNetIsolation: Bool,
4850
reply: @escaping (Error?) -> Void
4951
) {
5052
logger.info("startDaemon called")
@@ -57,6 +59,7 @@ class HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface
5759
apiToken: token,
5860
serverUrl: accessURL,
5961
tunFd: tun.fileDescriptor,
62+
useSoftNetIsolation: useSoftNetIsolation,
6063
literalHeaders: headers.flatMap { try? JSONDecoder().decode([HTTPHeader].self, from: $0) } ?? []
6164
)
6265
)

Coder-Desktop/Coder-DesktopHelper/Manager.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ actor Manager {
160160
resp = try await speaker.unaryRPC(
161161
.with { msg in
162162
msg.start = .with { req in
163+
req.tunnelUseSoftNetIsolation = cfg.useSoftNetIsolation
163164
req.tunnelFileDescriptor = cfg.tunFd
164165
req.apiToken = cfg.apiToken
165166
req.coderURL = cfg.serverUrl.absoluteString
@@ -234,6 +235,7 @@ struct ManagerConfig {
234235
let apiToken: String
235236
let serverUrl: URL
236237
let tunFd: Int32
238+
let useSoftNetIsolation: Bool
237239
let literalHeaders: [HTTPHeader]
238240
}
239241

Coder-Desktop/VPN/HelperXPCSpeaker.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@ final class HelperXPCSpeaker: NEXPCInterface, @unchecked Sendable {
5858

5959
// These methods are called to start and stop the daemon run by the Helper.
6060
extension HelperXPCSpeaker {
61-
func startDaemon(accessURL: URL, token: String, tun: FileHandle, headers: Data?) async throws {
61+
func startDaemon(
62+
accessURL: URL,
63+
token: String,
64+
tun: FileHandle,
65+
headers: Data?,
66+
useSoftNetIsolation: Bool
67+
) async throws {
6268
let conn = connect()
6369
return try await withCheckedThrowingContinuation { continuation in
6470
guard let proxy = conn.remoteObjectProxyWithErrorHandler({ err in
@@ -69,7 +75,13 @@ extension HelperXPCSpeaker {
6975
continuation.resume(throwing: XPCError.wrongProxyType)
7076
return
7177
}
72-
proxy.startDaemon(accessURL: accessURL, token: token, tun: tun, headers: headers) { err in
78+
proxy.startDaemon(
79+
accessURL: accessURL,
80+
token: token,
81+
tun: tun,
82+
headers: headers,
83+
useSoftNetIsolation: useSoftNetIsolation
84+
) { err in
7385
if let error = err {
7486
self.logger.error("Failed to start daemon: \(error.localizedDescription, privacy: .public)")
7587
continuation.resume(throwing: error)

Coder-Desktop/VPN/PacketTunnelProvider.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,31 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
4848
) async throws {
4949
globalHelperXPCSpeaker.ptp = self
5050
guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
51-
let baseAccessURL = proto.serverAddress
51+
let accessURL = proto.serverAddress
5252
else {
5353
logger.error("startTunnel called with nil protocolConfiguration")
5454
throw makeNSError(suffix: "PTP", desc: "Missing Configuration")
5555
}
5656
// HACK: We can't write to the system keychain, and the NE can't read the user keychain.
57-
guard let token = proto.providerConfiguration?["token"] as? String else {
57+
guard let token = proto.providerConfiguration?[VPNConfigurationKeys.token] as? String else {
5858
logger.error("startTunnel called with nil token")
5959
throw makeNSError(suffix: "PTP", desc: "Missing Token")
6060
}
61-
let headers = proto.providerConfiguration?["literalHeaders"] as? Data
62-
logger.debug("retrieved token & access URL")
61+
let headers = proto.providerConfiguration?[VPNConfigurationKeys.literalHeaders] as? Data
62+
let useSoftNetIsolation = proto.providerConfiguration?[
63+
VPNConfigurationKeys.useSoftNetIsolation
64+
] as? Bool ?? false
65+
logger.debug("retrieved vpn configuration settings")
6366
guard let tunFd = tunnelFileDescriptor else {
6467
logger.error("startTunnel called with nil tunnelFileDescriptor")
6568
throw makeNSError(suffix: "PTP", desc: "Missing Tunnel File Descriptor")
6669
}
6770
try await globalHelperXPCSpeaker.startDaemon(
68-
accessURL: .init(string: baseAccessURL)!,
71+
accessURL: .init(string: accessURL)!,
6972
token: token,
7073
tun: FileHandle(fileDescriptor: tunFd),
71-
headers: headers
74+
headers: headers,
75+
useSoftNetIsolation: useSoftNetIsolation
7276
)
7377
}
7478

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Keys for the `providerConfiguration` dictionary in the VPN configuration plist.
2+
public enum VPNConfigurationKeys {
3+
// String
4+
public static let token = "token"
5+
// [(String, String)]
6+
public static let literalHeaders = "literalHeaders"
7+
// Bool
8+
public static let useSoftNetIsolation = "useSoftNetIsolation"
9+
}

Coder-Desktop/VPNLib/Download.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,15 @@ extension DownloadManager: URLSessionDownloadDelegate {
150150
}
151151

152152
public required convenience init?(coder: NSCoder) {
153-
let written = coder.decodeInt64(forKey: "written")
154-
let total = coder.containsValue(forKey: "total") ? coder.decodeInt64(forKey: "total") : nil
153+
let written = coder.decodeInt64(forKey: Keys.written)
154+
let total = coder.containsValue(forKey: Keys.total) ? coder.decodeInt64(forKey: Keys.total) : nil
155155
self.init(totalBytesWritten: written, totalBytesToWrite: total)
156156
}
157157

158158
public func encode(with coder: NSCoder) {
159-
coder.encode(totalBytesWritten, forKey: "written")
159+
coder.encode(totalBytesWritten, forKey: Keys.written)
160160
if let total = totalBytesToWrite {
161-
coder.encode(total, forKey: "total")
161+
coder.encode(total, forKey: Keys.total)
162162
}
163163
}
164164

@@ -169,4 +169,9 @@ extension DownloadManager: URLSessionDownloadDelegate {
169169
let total = totalBytesToWrite.map { fmt.string(fromByteCount: $0) } ?? "Unknown"
170170
return "\(done) / \(total)"
171171
}
172+
173+
enum Keys {
174+
static let written = "written"
175+
static let total = "total"
176+
}
172177
}

Coder-Desktop/VPNLib/XPC.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperN
2525
// This is the XPC interface the Helper exposes to the Network Extension.
2626
@preconcurrency
2727
@objc public protocol HelperNEXPCInterface {
28-
// headers is a JSON `[HTTPHeader]`
29-
func startDaemon(accessURL: URL, token: String, tun: FileHandle, headers: Data?, reply: @escaping (Error?) -> Void)
28+
// swiftlint:disable:next function_parameter_count
29+
func startDaemon(
30+
accessURL: URL,
31+
token: String,
32+
tun: FileHandle,
33+
// headers is a JSON encoded `[HTTPHeader]`
34+
headers: Data?,
35+
useSoftNetIsolation: Bool,
36+
reply: @escaping (Error?) -> Void
37+
)
3038
func stopDaemon(reply: @escaping (Error?) -> Void)
3139
}
3240

Coder-Desktop/VPNLib/vpn.pb.swift

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
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