diff --git a/CONTEXT.md b/CONTEXT.md index d2ae74c2..1437b997 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -19,6 +19,8 @@ - **Proxy mode selector**: The common user action for choosing rule, global, or direct mode; it is distinct from enabling or disabling the proxy runtime, from the system proxy switch, and from custom-rule management. Its choice persists across GUI restarts. - **Agent switch**: The user's independent on/off choice for the proxy agent (the per-user background proxy runtime). It defaults to off: a fresh user has no server configuration, so neither install nor first launch starts the agent until the user turns it on. An explicit choice is persisted, survives GUI restarts, and quitting the GUI never stops the launchd-hosted agent once it is running. The settings view does not expose it. Turning the agent off first restores the system proxy settings 2.0 holds and then stops the local listeners. _Avoid_: proxy switch (ambiguous with the system proxy switch). - **System proxy switch**: The user's independent choice to have 2.0 take over macOS system proxy settings. It defaults to off, so a first run never rewrites the user's proxy dictionary. Turning it off restores only the settings 2.0 holds and never unregisters the agent or stops the local SOCKS/HTTP listeners. Its intent (persisted), actual application, and typed failures are presented as separate facts, distinct from the agent's run state. _Avoid_: proxy switch (ambiguous with the agent switch). +- **Additional system-proxy exception**: A user-added hostname, domain, IP address, or CIDR that macOS keeps outside 2.0's system proxy. The list defaults to empty and does not change the app's fixed local bypass policy or ACL rules. _Avoid_: bypass list (blurs user additions with app-fixed policy). +- **Fixed local bypass policy**: App-owned local IP ranges and hostnames that stay direct whether traffic is handled by macOS system-proxy settings or reaches a local proxy inbound. Users can inspect this policy but cannot edit it through additional system-proxy exceptions. - **Login item**: The GUI app's registration in the user's login items. It is an independent preference domain: off until the user enables it, and its state is only ever the system login-item registration status. - **Independently saved setting item**: A user-facing setting or coherent group of related values that the user explicitly saves as one unit; saving persists and applies that item without committing unrelated unsaved settings. _Avoid_: immediate edit (can imply applying on every keystroke). - **HTTP proxy inbound**: The permanent local inbound that accepts HTTP/HTTPS proxy requests through the external tunnel service and its active ACL; it is not a user preference and cannot be disabled, and it is distinct from proxy modes and from the Legacy Privoxy adapter. _Avoid_: HTTP proxy mode. @@ -84,7 +86,7 @@ - Exported GUI event lines are an allowlisted, field-cleaned subset of runtime events; raw event detail remains available only through the explicit diagnostic log view. - A diagnostic report presents fixed proxy-state categories rather than free-form failure detail. If its safe projection cannot be constructed, no report is emitted; an unavailable individual fact source remains a marked partial fact instead. - A diagnostic export is considered complete only after the user-selected report file is successfully written; opening the export panel or preparing report text is not an export completion. -- Proxy modes are mutually exclusive: rule mode with proxy-when-unmatched writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with a `[proxy_all]` ACL whose bypass list is the fixed local safety rules plus China domain and China IPv4 CIDR direct candidates from the built-in snapshots; rule mode with direct-when-unmatched runs sslocal with a `[bypass_all]` ACL whose proxy list holds only expressible, unshadowed GFWList proxy candidates (the two default actions never unconditionally union all built-in sources), so unmatched targets and IP literals with no matching rule go direct; global writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with a `[proxy_all]` ACL whose bypass list is only the fixed local safety rules (no China list, GFWList, or custom rules); direct mode writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with `[bypass_all]`, so SOCKS and HTTP requests go direct without a Shadowsocks server. IP-CIDR matching may cause sslocal to issue local DNS queries for unmatched hostnames; the product does not promise that all DNS queries travel through the remote Shadowsocks server. In every mode the SystemConfiguration exceptions include the same fixed local scope — IPv4 loopback, RFC1918, link-local, IPv6 loopback, link-local, unique-local, `localhost`, `.local`, and no-dot hostnames — merged with the user's exception list; CGNAT is excluded. IPv6 CIDR strings in system exceptions are not a verified bypass; the ACL is the routing guarantee for requests that enter sslocal, including LAN-sharing clients. GFWList `@@` exceptions shadowed by a broader proxy rule are omitted from the ACL and recorded in the conversion report; the product does not claim complete reproduction of GFWList URL/protocol conditions. A change to the active ACL's summary or digest requires a full sslocal restart; the ACL's runtime path is a stable reference and does not change when the proxy mode switches. The wrapper publishes the ACL-backed receipt only after the child owns every configured TCP listener; the controller retains the previous mode, runtime, and applied system proxy until this instance is verified, and restores the previous runtime on verification failure. System proxy writes are allowed only while the system proxy switch is on and the agent is healthy with an available exit; 2.0 stores the complete per-service Proxies dictionaries it owns and restores them only while the applied values still match, otherwise it reports an ownership conflict. When a rule/global active target becomes invalid, the agent keeps listening but the held system proxy configuration is safely withdrawn while the system proxy intent stays on. Only traffic that honors the system SOCKS or HTTP/HTTPS proxy settings or explicitly connects to a local inbound is routed; the product does not claim interception of all application or UDP traffic. +- Proxy modes are mutually exclusive: rule mode with proxy-when-unmatched writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with a `[proxy_all]` ACL whose bypass list is the fixed local safety rules plus China domain and China IPv4 CIDR direct candidates from the built-in snapshots; rule mode with direct-when-unmatched runs sslocal with a `[bypass_all]` ACL whose proxy list holds only expressible, unshadowed GFWList proxy candidates (the two default actions never unconditionally union all built-in sources), so unmatched targets and IP literals with no matching rule go direct; global writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with a `[proxy_all]` ACL whose bypass list is only the fixed local safety rules (no China list, GFWList, or custom rules); direct mode writes the local SOCKS and HTTP/HTTPS system proxies and runs sslocal with `[bypass_all]`, so SOCKS and HTTP requests go direct without a Shadowsocks server. IP-CIDR matching may cause sslocal to issue local DNS queries for unmatched hostnames; the product does not promise that all DNS queries travel through the remote Shadowsocks server. In every mode, the system proxy ExceptionsList contains fixed local IP ranges, `localhost` and `.local` hosts, plus the user's additional system-proxy exceptions (empty by default); while 2.0 owns those settings it also fixes simple-hostname exclusion on. User-added exceptions affect only macOS system-proxy traffic and never enter the ACL. The ACL keeps its fixed local IP and host rules, including the no-dot hostname rule, as the direct-routing guarantee for requests that reach sslocal, including LAN-sharing clients. CGNAT is excluded. IPv6 CIDR strings in system exceptions are not a verified bypass. GFWList `@@` exceptions shadowed by a broader proxy rule are omitted from the ACL and recorded in the conversion report; the product does not claim complete reproduction of GFWList URL/protocol conditions. A change to the active ACL's summary or digest requires a full sslocal restart; the ACL's runtime path is a stable reference and does not change when the proxy mode switches. The wrapper publishes the ACL-backed receipt only after the child owns every configured TCP listener; the controller retains the previous mode, runtime, and applied system proxy until this instance is verified, and restores the previous runtime on verification failure. System proxy writes are allowed only while the system proxy switch is on and the agent is healthy with an available exit; 2.0 stores the complete per-service Proxies dictionaries it owns and restores them only while the applied values still match, otherwise it reports an ownership conflict. When a rule/global active target becomes invalid, the agent keeps listening but the held system proxy configuration is safely withdrawn while the system proxy intent stays on. Only traffic that honors the system SOCKS or HTTP/HTTPS proxy settings or explicitly connects to a local inbound is routed; the product does not claim interception of all application or UDP traffic. - Preferences have no single restore-all-factory-defaults action. Where a reset exists, it is scoped to one configuration item, never to the whole preference set at once. - The proxy mode selector's choice and the rule default action persist with the settings snapshot and survive GUI restarts; a persistence failure keeps the previous mode and sub-option in force and names the reason. Rule, global, and direct mode are always available. - The GUI is the user-facing manager; the Shadowsocks tunnel service is an external runtime boundary rather than part of the GUI's domain model. diff --git a/ShadowsocksX-NG2/App/SettingsView.swift b/ShadowsocksX-NG2/App/SettingsView.swift index ee82a714..fc0cf978 100644 --- a/ShadowsocksX-NG2/App/SettingsView.swift +++ b/ShadowsocksX-NG2/App/SettingsView.swift @@ -1,54 +1,35 @@ import SwiftUI -/// 设置分区(issue #33/#44,地图 #52 票 #57):按原型重排为常规 / 代理端点 / -/// 高级三张分组卡,行式呈现(主文案 + 次说明 + 行尾控件);保存是表单级提交 -/// 动作,固定在本视图内容顶部行(不进窗口工具栏,避免动作与表单脱节)。 -/// 字段绑定编辑 workflow 的扁平 UI-shaped draft;issues 按字段渲染;端口行读 -/// typed field state;登录项开关绑定独立控制器。 +/// 设置分区(issue #33/#44,地图 #52 票 #57):常规、代理端点和系统代理三张 +/// 分组卡,行式呈现(主文案 + 次说明 + 行尾控件)。设置项由各自编辑器独立保存; +/// 登录项开关绑定独立控制器。 struct SettingsView: View { @ObservedObject var workflow: SettingsWorkflow @ObservedObject var loginController: LaunchAtLoginController - @State private var portSettingsEditor: PortSettingsEditorSession? - @State private var listenerModeEditor: ListenerModeEditorSession? + @State private var presentedEditor: SettingsEditorSheet? var body: some View { - VStack(spacing: 0) { - commitActionsRow - Form { - generalSection - endpointSection - advancedSection - } - .formStyle(.grouped) + Form { + generalSection + endpointSection + systemProxySection } + .formStyle(.grouped) .padding(.leading, 20) .padding(.trailing, 24) .onAppear { Task { _ = await workflow.reloadFromCommitted() } } - .sheet(item: $portSettingsEditor) { session in - PortSettingsEditorSheet(workflow: workflow, initialDraft: session.initialDraft) - } - .sheet(item: $listenerModeEditor) { session in - ListenerModeEditorSheet(workflow: workflow, initialMode: session.initialMode) - } - } - - // MARK: - 提交动作行(内容顶部,固定不随表单滚动) - - /// 保存(原壳动作槽位,票 #57):表单级提交动作与表单同置;保存走 Return - /// 默认键位。 - private var commitActionsRow: some View { - HStack(spacing: 8) { - Spacer(minLength: 0) - Button(workflow.isCommitting ? "保存中…" : "保存设置") { - Task { _ = await workflow.save() } + .sheet(item: $presentedEditor) { editor in + switch editor { + case .ports(let session): + PortSettingsEditorSheet(workflow: workflow, initialDraft: session.initialDraft) + case .listener(let session): + ListenerModeEditorSheet(workflow: workflow, initialMode: session.initialMode) + case .proxyExceptions(let session): + ProxyExceptionsEditorSheet(workflow: workflow, initialValue: session.initialValue) } - .keyboardShortcut(.defaultAction) - .disabled(!workflow.canSave) } - .padding(.top, 12) - .padding(.bottom, 8) } // MARK: - 常规 @@ -92,8 +73,8 @@ struct SettingsView: View { } settingRow("监听方式") { Button { - listenerModeEditor = ListenerModeEditorSession( - initialMode: workflow.beginListenerModeEditing()) + presentedEditor = .listener( + ListenerModeEditorSession(initialMode: workflow.beginListenerModeEditing())) } label: { HStack(spacing: 8) { Text(workflow.committedListenerMode.displayName) @@ -111,28 +92,31 @@ struct SettingsView: View { } } - // MARK: - 高级 + // MARK: - 系统代理 - private var advancedSection: some View { + private var systemProxySection: some View { Section { - TextField( - "绕过列表", text: $workflow.draft.proxyExceptions, - prompt: Text("127.0.0.1, 192.168.0.0/16, localhost …")) - Text("逗号或空格分隔域名,这些目标不走代理。") - .font(.caption) - .foregroundStyle(.secondary) - if workflow.hasBlockingPortOccupancy { - Label("检测到端口已被占用;请在「端口设置」中选择建议空闲端口并保存。", systemImage: "exclamationmark.triangle") - .font(.footnote) - .foregroundStyle(.orange) - } - if workflow.isDirty { - Label("存在未保存的修改;点击顶部「保存设置」生效。", systemImage: "pencil") - .font(.footnote) - .foregroundStyle(.secondary) + settingRow( + "额外系统代理例外", + note: "只追加到系统代理;应用固定绕过规则仍生效" + ) { + Button { + presentedEditor = .proxyExceptions( + ProxyExceptionsEditorSession(initialValue: workflow.beginProxyExceptionsEditing())) + } label: { + HStack(spacing: 8) { + Text("\(workflow.committedProxyExceptionCount) 项") + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + } + } + .buttonStyle(.bordered) + .accessibilityLabel("额外系统代理例外") + .accessibilityValue("\(workflow.committedProxyExceptionCount) 项") + .accessibilityHint("编辑并保存额外的系统代理例外") } } header: { - sectionHeader("高级", subtitle: "细化系统代理行为") + sectionHeader("系统代理", subtitle: "附加例外与应用固定的绕过规则") } } @@ -197,8 +181,8 @@ extension SettingsView { let httpPort = ports.httpPort.formatted(.number.grouping(.never)) return settingRow("端口设置") { Button { - portSettingsEditor = PortSettingsEditorSession( - initialDraft: workflow.beginPortSettingsEditing()) + presentedEditor = .ports( + PortSettingsEditorSession(initialDraft: workflow.beginPortSettingsEditing())) } label: { HStack(spacing: 8) { Text("SOCKS \(socksPort) / HTTP \(httpPort)") @@ -215,11 +199,129 @@ extension SettingsView { } +private enum SettingsEditorSheet: Identifiable { + case ports(PortSettingsEditorSession) + case listener(ListenerModeEditorSession) + case proxyExceptions(ProxyExceptionsEditorSession) + + var id: UUID { + switch self { + case .ports(let session): session.id + case .listener(let session): session.id + case .proxyExceptions(let session): session.id + } + } +} + private struct PortSettingsEditorSession: Identifiable { let id = UUID() let initialDraft: SettingsPortDraft } +private struct ProxyExceptionsEditorSession: Identifiable { + let id = UUID() + let initialValue: String +} + +private struct ProxyExceptionsEditorSheet: View { + @ObservedObject var workflow: SettingsWorkflow + @Environment(\.dismiss) private var dismiss + @State private var draft: String + + init(workflow: SettingsWorkflow, initialValue: String) { + self.workflow = workflow + _draft = State(initialValue: initialValue) + } + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + Text("额外系统代理例外") + .font(.title2) + Text("添加希望 macOS 系统代理跳过的主机名、域名或 IP/CIDR。留空并确定会清除用户添加的条目。") + .font(.caption) + .foregroundStyle(.secondary) + + TextEditor(text: $draft) + .font(.system(.body, design: .monospaced)) + .scrollContentBackground(.hidden) + .padding(6) + .frame(height: 112) + .background(.background, in: RoundedRectangle(cornerRadius: 8)) + .overlay { + RoundedRectangle(cornerRadius: 8) + .stroke(.quaternary, lineWidth: 1) + } + .disabled(workflow.isCommitting) + .accessibilityLabel("额外系统代理例外") + + Text( + "格式:域名、主机名或 IP/CIDR,以逗号、顿号、空格或换行分隔。示例:127.0.0.1, 192.168.0.0/16, localhost。" + ) + .font(.caption) + .foregroundStyle(.secondary) + + Divider() + + VStack(alignment: .leading, spacing: 6) { + Text("应用固定的系统代理例外") + .font(.headline) + Text(FixedLocalProxyRanges.systemProxyExceptions.joined(separator: "\n")) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + Text("应用接管系统代理时,还会固定开启“不包括简单主机名(Exclude simple hostnames)”。") + .font(.caption) + .foregroundStyle(.secondary) + } + + VStack(alignment: .leading, spacing: 6) { + Text("应用固定的 ACL 绕过规则") + .font(.headline) + Text(FixedLocalProxyRanges.aclBypassRules.joined(separator: "\n")) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + Text("这些规则用于本机 SOCKS/HTTP 入站;上方输入只追加到系统代理例外,不会修改 ACL。") + .font(.caption) + .foregroundStyle(.secondary) + } + + if let failure = workflow.lastFailure { + Label(failure.presentableMessage, systemImage: "exclamationmark.triangle.fill") + .font(.footnote) + .foregroundStyle(.red) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 16) + } + + HStack { + Spacer() + Button("取消", role: .cancel) { + dismiss() + } + .keyboardShortcut(.cancelAction) + .disabled(workflow.isCommitting) + + Button(workflow.isCommitting ? "保存中…" : "确定") { + Task { + if case .persisted = await workflow.saveProxyExceptions(draft) { + dismiss() + } + } + } + .keyboardShortcut(.defaultAction) + .disabled(workflow.isCommitting) + } + .padding(.top, 12) + } + .padding(24) + .frame(width: 560, height: 640) + .interactiveDismissDisabled(workflow.isCommitting) + } +} + private struct PortSettingsEditorSheet: View { @ObservedObject var workflow: SettingsWorkflow @Environment(\.dismiss) private var dismiss diff --git a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflow.swift b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflow.swift index 62c34315..62a7dac2 100644 --- a/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflow.swift +++ b/ShadowsocksX-NG2/App/SettingsWorkflow/SettingsWorkflow.swift @@ -92,11 +92,20 @@ final class SettingsWorkflow: ObservableObject { committing.committedSettings.listen.listenerMode } + var committedProxyExceptionCount: Int { + committing.committedSettings.proxyExceptionList.count + } + func beginListenerModeEditing() -> ListenerMode { lastFailure = nil return committedListenerMode } + func beginProxyExceptionsEditing() -> String { + lastFailure = nil + return committing.committedSettings.proxyExceptions + } + /// 打开独立端口设置项时,从已提交值开始,丢弃上次编辑器探测的暂存事实。 func beginPortSettingsEditing() -> SettingsPortDraft { lastFailure = nil @@ -215,6 +224,38 @@ final class SettingsWorkflow: ObservableObject { return await commitPortSettings(proposed) } + /// Save only the user-added system-proxy exceptions. Fixed local ACL rules + /// and unrelated setting-item drafts remain untouched. + @discardableResult + func saveProxyExceptions(_ rawValue: String) async -> SettingsCommandOutcome { + guard !isCommitting else { return record(.rejected(.inProgress)) } + let value = + rawValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "" : rawValue + guard value != committing.committedSettings.proxyExceptions else { + lastFailure = nil + return record(.persisted) + } + + var proposed = committing.committedSettings + proposed.proxyExceptions = value + isCommitting = true + lastFailure = nil + do { + try await committing.updateSettings(proposed) + var nextDraft = draft + nextDraft.proxyExceptions = committing.committedSettings.proxyExceptions + draft = nextDraft + isCommitting = false + lastFailure = nil + return record(.persisted) + } catch { + isCommitting = false + let failure = Self.workflowFailure(for: error) + lastFailure = failure + return record(.persistenceFailed(Self.persistenceFailure(for: error))) + } + } + /// 明确阻塞保存的端口。 var blockingPortIDs: [SettingsPortID] { SettingsPortID.allCases.filter { id in diff --git a/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift b/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift index f1ee4d7c..d9c5c397 100644 --- a/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift +++ b/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift @@ -20,7 +20,7 @@ struct ParsedCIDR: Equatable, Sendable { /// sslocal 域名代理 / IP 直连优先级遮蔽。每条附可解释说明。 enum CustomRuleRejection: Equatable, Sendable { /// 代理动作覆盖固定本地范围(回环/私有/链路本地/localhost/*.local/无点主机名)。 - /// 系统绕过列表始终高于用户规则,该规则不会按用户意图生效。 + /// 应用固定的本地绕过规则始终优先于用户代理规则;冲突规则不会生效。 case conflictsWithFixedLocalScope /// 域名直连规则被更宽(或同覆盖)的域名代理规则遮蔽:sslocal 域名匹配 /// 时 proxy_list 优先于 bypass_list。 @@ -314,7 +314,7 @@ enum CustomRuleValidator { return RejectedCustomRule( rule: rule, reason: .conflictsWithFixedLocalScope, - explanation: "系统绕过列表始终高于用户规则;代理固定本地目标不会生效") + explanation: "应用固定的本地绕过规则优先;这些本地目标不会经代理") } /// ACL 优先级遮蔽检查: diff --git a/ShadowsocksX-NG2/Domain/ProxyACL.swift b/ShadowsocksX-NG2/Domain/ProxyACL.swift index e1f0f24b..969c0e6b 100644 --- a/ShadowsocksX-NG2/Domain/ProxyACL.swift +++ b/ShadowsocksX-NG2/Domain/ProxyACL.swift @@ -1,8 +1,8 @@ import CryptoKit import Foundation -/// Fixed local targets bypass the system proxy. Their IP ranges are also -/// included in the direct-mode sslocal ACL for requests reaching either inbound. +/// Fixed local targets bypass the system proxy. Their IP ranges and host rules +/// also enter the sslocal ACL for requests reaching either inbound. enum FixedLocalProxyRanges { static let ipRanges = [ "127.0.0.0/8", @@ -15,7 +15,7 @@ enum FixedLocalProxyRanges { "fc00::/7", ] - static let systemProxyHostExceptions = ["localhost", "*.local", ""] + static let systemProxyHostExceptions = ["localhost", "*.local"] static var systemProxyExceptions: [String] { ipRanges + systemProxyHostExceptions diff --git a/ShadowsocksX-NG2/Domain/ProxySettings.swift b/ShadowsocksX-NG2/Domain/ProxySettings.swift index 2bc515c8..7ff3545d 100644 --- a/ShadowsocksX-NG2/Domain/ProxySettings.swift +++ b/ShadowsocksX-NG2/Domain/ProxySettings.swift @@ -3,10 +3,10 @@ import Foundation /// #33 的用户偏好快照。监听端点仍是运行时契约的输入,但不会把偏好文件 /// 当作运行时文件使用;代理启动前由控制器从快照重新派生完整契约。 struct ProxySettings: Equatable, Sendable { - static let defaultProxyExceptions = - "127.0.0.1, localhost, 192.168.0.0/16, 10.0.0/8, FE80::/64, ::1, FD00::/8" + static let defaultProxyExceptions = "" var listen: SslocalListenSettings + /// User-added system-proxy exceptions; these entries do not become ACL rules. var proxyExceptions: String /// The persisted current mode: the mode selector's choice survives GUI /// restarts. First run defaults to rule mode (issue #67). diff --git a/ShadowsocksX-NG2/Domain/SystemProxyPropertyList.swift b/ShadowsocksX-NG2/Domain/SystemProxyPropertyList.swift index 2c4fb704..ce6f5965 100644 --- a/ShadowsocksX-NG2/Domain/SystemProxyPropertyList.swift +++ b/ShadowsocksX-NG2/Domain/SystemProxyPropertyList.swift @@ -4,6 +4,7 @@ import Foundation /// key mapping here makes the mutually exclusive enablement rules testable /// without opening a real SCPreferences session. PAC projection is gone /// (issue #67);SOCKS 与 HTTP/HTTPS 同时指向本地入站(ADR 0012)。 +/// The app fixes simple-hostname exclusion on whenever it owns this dictionary. enum SystemProxyPropertyList { static let httpEnabled = "HTTPEnable" static let httpsEnabled = "HTTPSEnable" @@ -19,6 +20,7 @@ enum SystemProxyPropertyList { static let pacJavaScript = "ProxyAutoConfigJavaScript" static let autoDiscoveryEnabled = "ProxyAutoDiscoveryEnable" static let exceptionsList = "ExceptionsList" + static let excludeSimpleHostnames = "ExcludeSimpleHostnames" static func applying( _ configuration: SystemProxyConfiguration, to original: [String: Any] @@ -43,6 +45,7 @@ enum SystemProxyPropertyList { dictionary[httpsEnabled] = 1 dictionary[httpsProxy] = configuration.http.host dictionary[httpsPort] = configuration.http.port + dictionary[excludeSimpleHostnames] = 1 if let exceptions = configuration.exceptions { dictionary[exceptionsList] = exceptions