Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: add toggle for coder deployments behind a vpn#209

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes fromall 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
28 changes: 23 additions & 5 deletionsCoder-Desktop/Coder-Desktop/State.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import KeychainAccess
import NetworkExtension
import os
import SwiftUI
import VPNLib

@MainActor
class AppState: ObservableObject {
Expand DownExpand Up@@ -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 }
Expand All@@ -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
Expand DownExpand Up@@ -188,6 +204,7 @@ class AppState: ObservableObject {
}

public func clearSession() {
logger.info("clearing session")
hasSession = false
sessionToken = nil
refreshTask?.cancel()
Expand DownExpand Up@@ -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"

Expand Down
19 changes: 19 additions & 0 deletionsCoder-Desktop/Coder-Desktop/Views/Settings/NetworkTab.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,11 +4,30 @@ struct NetworkTab<VPN: VPNService>: View {
var body: some View {
Form {
LiteralHeadersSection<VPN>()
SoftNetIsolationSection<VPN>()
}
.formStyle(.grouped)
}
}

struct SoftNetIsolationSection<VPN: VPNService>: 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<PreviewVPN>()
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,13 @@ class HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface

let startSymbol = "OpenTunnel"

// swiftlint:disable:next function_parameter_count
Copy link
MemberAuthor

@ethanndicksonethanndicksonJul 28, 2025
edited
Loading

Choose a reason for hiding this comment

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

Moving these parameters behind a struct introduces a surprising amount of complexity, as it needs to manually beNSSecureCodable - I don't see any immediate value in doing so.

func startDaemon(
accessURL: URL,
token: String,
tun: FileHandle,
headers: Data?,
useSoftNetIsolation: Bool,
reply: @escaping (Error?) -> Void
) {
logger.info("startDaemon called")
Expand All@@ -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) } ?? []
)
)
Expand Down
2 changes: 2 additions & 0 deletionsCoder-Desktop/Coder-DesktopHelper/Manager.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand DownExpand Up@@ -234,6 +235,7 @@ struct ManagerConfig {
let apiToken: String
let serverUrl: URL
let tunFd: Int32
let useSoftNetIsolation: Bool
let literalHeaders: [HTTPHeader]
}

Expand Down
16 changes: 14 additions & 2 deletionsCoder-Desktop/VPN/HelperXPCSpeaker.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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)
Expand Down
16 changes: 10 additions & 6 deletionsCoder-Desktop/VPN/PacketTunnelProvider.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -48,27 +48,31 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
) async throws {
globalHelperXPCSpeaker.ptp = self
guard let proto = protocolConfiguration as? NETunnelProviderProtocol,
letbaseAccessURL = proto.serverAddress
letaccessURL = 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
)
}

Expand Down
9 changes: 9 additions & 0 deletionsCoder-Desktop/VPNLib/Configuration.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -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"
}
13 changes: 9 additions & 4 deletionsCoder-Desktop/VPNLib/Download.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
}
}

Expand All@@ -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"
}
}
12 changes: 10 additions & 2 deletionsCoder-Desktop/VPNLib/XPC.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
}

Expand Down
8 changes: 8 additions & 0 deletionsCoder-Desktop/VPNLib/vpn.pb.swift
View file
Open in desktop

Some generated files are not rendered by default. Learn more abouthow customized files appear on GitHub.

1 change: 1 addition & 0 deletionsCoder-Desktop/VPNLib/vpn.proto
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
2 changes: 1 addition & 1 deletionCoder-Desktop/project.yml
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp