mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-12 23:35:06 +00:00
非「仅本机」监听方式下,首页命令卡在复制按钮上方提供单选 IP 下拉框:
候选仅含回环与当前启用 Wi‑Fi/Ethernet 接口单播地址(严格 SCNetworkInterface
类型白名单,排除 VPN/虚拟/未知类型),按监听地址族过滤(双栈含两个回环,
127.0.0.1 置顶;仅 IPv6 默认 ::1),标签 {IP} - {name}(本地化名称缺失回退
BSD 名)。两种 shell 的 HTTP/HTTPS/SOCKS 端点共用选中地址,端口取已保存
监听事实,IPv6 URL 方括号;选择会话内有效(跨首页返回保留、重启重置),
失效自动回退默认回环。进入首页、网络变化(SCDynamicStore 监视+去抖)与
复制前刷新。
- 新缝 LocalInterfaceFactsReading:原始接口事实(地址/类型/名称)+ 变化
通知;生产适配器封装 getifaddrs/SCNetworkInterface/SCDynamicStore 细节
- 运行时缝以 listenFacts 替换 terminalProxyEnvironmentCommands;命令派生
移至工作流层(监听事实 + 生效选择)
- 替身测试证明策略行为;真实主机非视觉验证证明系统适配器整合
(回环识别/类型映射/地址规范化/链路本地判定)
140 lines
4.9 KiB
Swift
140 lines
4.9 KiB
Swift
import Combine
|
||
import Foundation
|
||
|
||
/// 生产运行时 adapter(issue #47):现有 `ProxyRuntimeController` 的唯一
|
||
/// 包装,由应用组合根接线。只把控制器既有能力与观察投影到 workflow 缝上,
|
||
/// 不复制状态机、generation、健康门禁与系统代理生命周期语义。
|
||
@MainActor
|
||
final class ControllerProxyRuntimeAdapter: ProxyRuntimeAdapting {
|
||
private let controller: ProxyRuntimeController
|
||
|
||
init(controller: ProxyRuntimeController) {
|
||
self.controller = controller
|
||
}
|
||
|
||
var runtimeFacts: ProxyRuntimeFacts { controller.runtimeFacts }
|
||
|
||
var agentIntentEnabled: Bool { controller.agentIntentEnabled }
|
||
|
||
var activationFailure: ActivationFailure? { controller.lastActivationFailure }
|
||
|
||
var systemProxyIntentEnabled: Bool { controller.systemProxyIntentEnabled }
|
||
|
||
var systemProxyApplication: SystemProxyApplicationFacts {
|
||
switch controller.systemProxyState {
|
||
case .idle: return .idle
|
||
case .pending: return .pending
|
||
case .applied: return .applied
|
||
case .failed(let facts): return .failed(facts)
|
||
}
|
||
}
|
||
|
||
var systemProxyApprovalRequired: Bool { controller.systemProxyApprovalRequired }
|
||
|
||
var proxyMode: ProxyMode { controller.proxyMode }
|
||
|
||
var ruleDefaultAction: RuleDefaultAction { controller.ruleDefaultAction }
|
||
|
||
var skippedInvalidServerCount: Int { controller.skippedServers.count }
|
||
|
||
var activeTargetID: NodeID? { controller.activeTargetID }
|
||
|
||
var httpExportCapability: HTTPExportCapability {
|
||
HTTPExportCapability(listen: controller.listenSettings)
|
||
}
|
||
|
||
var listenFacts: RuntimeListenFacts {
|
||
RuntimeListenFacts(listen: controller.listenSettings)
|
||
}
|
||
|
||
/// 控制器的 `objectWillChange` 是 willChange 语义;主队列 hop 落地时被
|
||
/// 发布的新值已可读,workflow 重观察不会读到半程状态。
|
||
var changes: AnyPublisher<Void, Never> {
|
||
controller.objectWillChange
|
||
.map { _ in () }
|
||
.receive(on: DispatchQueue.main)
|
||
.eraseToAnyPublisher()
|
||
}
|
||
|
||
func resyncOnLaunch() async {
|
||
await controller.resyncOnLaunch()
|
||
}
|
||
|
||
func setAgentEnabled(_ enabled: Bool) async {
|
||
await controller.setAgentEnabled(enabled)
|
||
}
|
||
|
||
func setSystemProxyEnabled(_ enabled: Bool) async {
|
||
await controller.setSystemProxyEnabled(enabled)
|
||
}
|
||
|
||
func openSystemProxyHelperApproval() async {
|
||
await controller.openSystemProxyHelperApproval()
|
||
}
|
||
|
||
func setProxyMode(_ mode: ProxyMode) async {
|
||
await controller.setProxyMode(mode)
|
||
}
|
||
|
||
func setRuleDefaultAction(_ action: RuleDefaultAction) async {
|
||
await controller.setRuleDefaultAction(action)
|
||
}
|
||
}
|
||
|
||
/// HTTP 导出能力的唯一派生点:监听设置 → 本机终端使用的可复制导出行。
|
||
/// 环境变量始终指向回环地址,避免将命令误用于其他设备。
|
||
extension HTTPExportCapability {
|
||
init(listen: SslocalListenSettings) {
|
||
let urlHost = listen.proxyLoopbackURLHost
|
||
let endpoint = "http://\(urlHost):\(listen.httpPort)"
|
||
self.init(copyableLine: "export http_proxy=\(endpoint);export https_proxy=\(endpoint);")
|
||
}
|
||
}
|
||
|
||
/// 终端代理环境变量命令的唯一派生点(issue #72):已保存监听事实 + 生效
|
||
/// 命令地址 → 两种 shell 的可复制命令。HTTP/HTTPS 用已保存 HTTP 端口,
|
||
/// SOCKS 用已保存 SOCKS 端口;变量集合、小写变量名、单引号转义、socks5
|
||
/// 协议与 no_proxy 内容保持不变;IPv6 URL 使用方括号。
|
||
extension TerminalProxyEnvironmentCommands {
|
||
init(listen: RuntimeListenFacts, commandAddress: TerminalCommandAddress) {
|
||
let host = commandAddress.address
|
||
let urlHost = host.contains(":") ? "[\(host)]" : host
|
||
let httpEndpoint = "'http://\(urlHost):\(listen.httpPort)'"
|
||
let socksEndpoint = "'socks5://\(urlHost):\(listen.socksPort)'"
|
||
let noProxy = "'localhost,127.0.0.1,::1,.local'"
|
||
let zshBash =
|
||
[
|
||
"export http_proxy=\(httpEndpoint)",
|
||
"export https_proxy=\(httpEndpoint)",
|
||
"export all_proxy=\(socksEndpoint)",
|
||
"export no_proxy=\(noProxy)",
|
||
].joined(separator: "; ") + ";"
|
||
let fish =
|
||
[
|
||
"set -gx http_proxy \(httpEndpoint)",
|
||
"set -gx https_proxy \(httpEndpoint)",
|
||
"set -gx all_proxy \(socksEndpoint)",
|
||
"set -gx no_proxy \(noProxy)",
|
||
].joined(separator: "; ") + ";"
|
||
self.init(
|
||
zshBash: zshBash,
|
||
fish: fish)
|
||
}
|
||
}
|
||
|
||
extension SslocalListenSettings {
|
||
fileprivate var proxyLoopbackURLHost: String {
|
||
let host = listenerMode.proxyLoopbackAddress
|
||
return host.contains(":") ? "[\(host)]" : host
|
||
}
|
||
}
|
||
|
||
/// 目录侧唯一的目标事实出口(窄缝,story 28):存在性 + 显示名路径摘要;
|
||
/// 完整目录树、订阅对象与凭据不出此接口。
|
||
extension CatalogWorkflow: ProxyTargetFactsReading {
|
||
func activeTargetFacts(for targetID: NodeID?) -> ProxyActiveTargetFacts? {
|
||
guard let targetID else { return nil }
|
||
return ProxyActiveTargetFacts(id: targetID, pathSummary: tree.pathSummary(for: targetID))
|
||
}
|
||
}
|