Files

335 lines
11 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Effective binding policy used by runtime documents and port probes.
/// `disabled` represents the local loopback binding; LAN settings use
/// `LANListenerMode` and an independent enable switch.
enum ListenerMode: String, Codable, CaseIterable, Equatable, Sendable {
case disabled
case allIPv4Interfaces = "all_ipv4_interfaces"
case allIPv4AndIPv6Interfaces = "all_ipv4_and_ipv6_interfaces"
case allIPv6Interfaces = "all_ipv6_interfaces"
var bindAddress: String {
switch self {
case .disabled: "127.0.0.1"
case .allIPv4Interfaces: "0.0.0.0"
case .allIPv4AndIPv6Interfaces, .allIPv6Interfaces: "::"
}
}
/// `nil` omits the upstream option for IPv4 listeners. IPv6 wildcard modes
/// explicitly select dual-stack or IPv6-only behavior.
var ipv6Only: Bool? {
switch self {
case .disabled, .allIPv4Interfaces: nil
case .allIPv4AndIPv6Interfaces: false
case .allIPv6Interfaces: true
}
}
var bindingHint: String {
switch self {
case .disabled: "127.0.0.1"
case .allIPv4Interfaces: "0.0.0.0"
case .allIPv4AndIPv6Interfaces: ":: / IPV6_V6ONLY=false"
case .allIPv6Interfaces: ":: / IPV6_V6ONLY=true"
}
}
var displayName: String {
switch self {
case .disabled: String(localized: "关闭")
case .allIPv4Interfaces: String(localized: "所有 IPv4 接口")
case .allIPv4AndIPv6Interfaces: String(localized: "所有 IPv4 与 IPv6 接口")
case .allIPv6Interfaces: String(localized: "所有 IPv6 接口")
}
}
var exposesNetworkInterfaces: Bool { self != .disabled }
}
/// `sslocal` 的一个本地入站。上游 v1.26.0 通过 `locals[]` 同时承载 SOCKS5
/// 与 HTTP;HTTP 直接由 shadowsocks-rust 提供,不经过 Legacy Privoxy。
struct SslocalLocalDocument: Codable, Equatable, Sendable {
let inboundProtocol: String
let localAddress: String
let localPort: Int
let mode: String
var socks5AuthConfigPath: String?
var httpAuthConfigPath: String?
enum CodingKeys: String, CodingKey {
case mode
case socks5AuthConfigPath = "socks5_auth_config_path"
case httpAuthConfigPath = "http_auth_config_path"
case inboundProtocol = "protocol"
case localAddress = "local_address"
case localPort = "local_port"
}
}
/// GUI 内存中的监听事实;不进入 sslocal 配置或 Agent 监管资料。
struct RuntimeListenDocument: Encodable, Equatable, Sendable {
let listenerMode: ListenerMode
let bindAddress: String
var lan = LANListeningFacts()
var authentication: RuntimeListenerAuthentication?
enum CodingKeys: String, CodingKey {
case lan, authentication
case listenerMode = "listener_mode"
case bindAddress = "bind_address"
}
}
/// GUI 派生的运行意图;上游 JSON 由独立输出表示生成。
/// 密码与插件参数已在此解析为明文;私有意图编码不落盘,永不入日志。
struct SslocalRuntimeDocument: Equatable, Sendable {
let servers: [SslocalServerDocument]
var locals: [SslocalLocalDocument]
let ipv6Only: Bool?
var listen: RuntimeListenDocument
/// 两个入站共用的稳定 ACL 路径;变体身份只供 GUI 准备和发布。
let aclFilePath: String?
let aclRuntime: ProxyACLDocument?
init(
servers: [SslocalServerDocument],
listen: ProxyListeningConfiguration,
acl: ProxyACLDocument? = nil
) {
self.servers = servers
locals = listen.locals
self.listen = RuntimeListenDocument(
listenerMode: listen.listenerMode,
bindAddress: listen.bindAddress, lan: LANListeningFacts(listen.lan))
ipv6Only = listen.listenerMode.ipv6Only
aclFilePath = acl?.path
aclRuntime = acl
}
private init(
servers: [SslocalServerDocument],
locals: [SslocalLocalDocument],
listen: RuntimeListenDocument,
ipv6Only: Bool?,
acl: ProxyACLDocument?
) {
self.servers = servers
self.locals = locals
self.listen = listen
self.ipv6Only = ipv6Only
aclFilePath = acl?.path
aclRuntime = acl
}
func jsonData() throws -> Data {
try Self.jsonEncoder.encode(
SslocalConfigurationDocument(
servers: servers, locals: locals, ipv6Only: ipv6Only, aclFilePath: aclFilePath))
}
var intentSHA256: String? {
try? ProxyACLDocument.digest(intentData())
}
/// 私有意图编码只用于等价比较;不落盘,也不作为 sslocal 的输入。
fileprivate func intentData() throws -> Data {
struct Intent: Encodable {
let servers: [SslocalServerDocument]
let locals: [SslocalLocalDocument]
let ipv6Only: Bool?
let listen: RuntimeListenDocument
let acl: ProxyACLDocument?
}
return try Self.jsonEncoder.encode(
Intent(servers: servers, locals: locals, ipv6Only: ipv6Only, listen: listen, acl: aclRuntime))
}
func replacingACL(_ acl: ProxyACLDocument?) -> SslocalRuntimeDocument {
SslocalRuntimeDocument(
servers: servers,
locals: locals,
listen: listen,
ipv6Only: ipv6Only,
acl: acl)
}
private static let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return encoder
}()
}
/// sslocal 的输出格式;GUI 运行意图中的监听事实和 ACL 身份不进入此文档。
struct SslocalConfigurationDocument: Codable, Equatable, Sendable {
let servers: [SslocalServerDocument]
var locals: [SslocalLocalDocument]
let ipv6Only: Bool?
let aclFilePath: String?
enum CodingKeys: String, CodingKey {
case servers, locals
case ipv6Only = "ipv6_only"
case aclFilePath = "acl"
}
}
/// 上游 `servers[]` 条目。id 携带叶子身份,供上游稳定标识;显示名是目录
/// 元数据,不进契约(GLOSSARY.md「Runtime configuration file」)。
/// 插件字段在无插件时整体省略。
struct SslocalServerDocument: Codable, Equatable, Sendable {
let id: String
let server: String
let serverPort: Int
let password: String
let method: String
let plugin: String?
let pluginOpts: String?
var pluginArgs: [String]?
var pluginMode: PluginTransportMode?
enum CodingKeys: String, CodingKey {
case id, server
case serverPort = "server_port"
case password, method, plugin
case pluginOpts = "plugin_opts"
case pluginArgs = "plugin_args"
case pluginMode = "plugin_mode"
}
}
extension SslocalLocalDocument {
/// 探测用主机名:通配绑定地址按回环探测(wrapper 监听判定与 GUI 健康门
/// 共用口径)。
var probeHost: String {
switch localAddress {
case "0.0.0.0": "127.0.0.1"
case "::": "::1"
default: localAddress
}
}
}
extension SslocalRuntimeDocument {
var listenFingerprint: SslocalListenFingerprint {
SslocalListenFingerprint(
locals: locals, ipv6Only: ipv6Only, acl: aclRuntime,
authentication: listen.authentication)
}
var socksLocal: SslocalLocalDocument? {
locals.first { $0.inboundProtocol == "socks" }
}
var socksAddress: String { socksLocal?.localAddress ?? "" }
var socksPort: Int { socksLocal?.localPort ?? 0 }
var socksMode: String { socksLocal?.mode ?? "" }
/// HTTP 入站端口;缺 HTTP 入站时为 0。完整入站组由 `isWellFormed` 在
/// 跨进程契约缝拒绝,端口合法性由 settings store 保证。
var httpPort: Int {
locals.first { $0.inboundProtocol == "http" }?.localPort ?? 0
}
/// GUI 准备侧校验:端口、协议与共享范围必须一致。空 `servers` 合法:
/// 无活动目标时 agent 以空服务器列表提供本地监听;上游 sslocal v1.26.0
/// 接受空 servers 并照常绑定本地入站。
var isWellFormed: Bool {
guard
(aclFilePath == nil) == (aclRuntime == nil),
aclRuntime.map({ $0.isWellFormed && $0.path == aclFilePath }) ?? true
else { return false }
guard listen.bindAddress == "127.0.0.1", listen.listenerMode == listen.lan.effectiveMode,
ipv6Only == listen.lan.effectiveMode.ipv6Only
else { return false }
let local = locals.filter { $0.localAddress == "127.0.0.1" }
let network = locals.filter { $0.localAddress != "127.0.0.1" }
guard local.count == 2, local.filter({ $0.inboundProtocol == "socks" }).count == 1,
local.filter({ $0.inboundProtocol == "http" }).count == 1,
local.allSatisfy({
$0.mode == ($0.inboundProtocol == "socks" ? "tcp_and_udp" : "tcp_only")
&& $0.socks5AuthConfigPath == nil && $0.httpAuthConfigPath == nil
})
else { return false }
var expected = ProxyListeningConfiguration()
expected.local.socksPort = socksPort
expected.local.httpPort = httpPort
expected.lan = listen.lan.configuration
guard network.count == (listen.lan.isEnabled ? 2 : 0),
expected.configuredPorts.allSatisfy({ ProxyPortRange.valid.contains($0) }),
Set(expected.configuredPorts).count == 4
else { return false }
for (actual, desired) in zip(locals, expected.locals) {
guard actual.inboundProtocol == desired.inboundProtocol,
actual.localAddress == desired.localAddress, actual.localPort == desired.localPort,
actual.mode == desired.mode
else { return false }
}
if listen.lan.isEnabled && listen.lan.authenticationEnabled {
guard let auth = listen.authentication, auth.isValid, auth.username == listen.lan.username,
network.first(where: { $0.inboundProtocol == "socks" })?.socks5AuthConfigPath
== auth.socksPath,
network.first(where: { $0.inboundProtocol == "http" })?.httpAuthConfigPath == auth.httpPath
else { return false }
} else if listen.authentication != nil
|| network.contains(where: {
$0.socks5AuthConfigPath != nil || $0.httpAuthConfigPath != nil
})
{
return false
}
return servers.allSatisfy { server in
(1...65535).contains(server.serverPort)
&& !server.id.isEmpty
&& !server.server.isEmpty
&& !server.method.isEmpty
}
}
}
/// GUI 准备的运行意图与私有摘要;Agent 回执使用独立随机部署身份。
struct PreparedRuntimeContract: Sendable {
let document: SslocalRuntimeDocument
let sha256: String
init(_ document: SslocalRuntimeDocument) throws {
self.document = document
sha256 = try ProxyACLDocument.digest(document.intentData())
}
}
extension Redactor {
/// 运行时文档只允许暴露数量与模式元数据。
static func documentSummary(_ document: SslocalRuntimeDocument) -> String {
let protocols = document.locals.map(\.inboundProtocol).joined(separator: ",")
return "servers=\(document.servers.count) protocols=\(protocols) mode=\(document.socksMode)"
}
}
/// 控制器在准备完成后仅保留健康检查与界面所需事实,不缓存完整秘密文档。
struct RuntimeDeploymentFacts: Equatable, Sendable {
let locals: [SslocalLocalDocument]
let listen: RuntimeListenFacts
let hasServers: Bool
let aclSummary: String?
let intentSHA256: String?
let configurationIntentSHA256: String?
init(_ document: SslocalRuntimeDocument) {
locals = document.locals.map {
SslocalLocalDocument(
inboundProtocol: $0.inboundProtocol, localAddress: $0.localAddress,
localPort: $0.localPort, mode: $0.mode)
}
listen = RuntimeListenFacts(document: document)
hasServers = !document.servers.isEmpty
aclSummary = document.aclRuntime?.summary
intentSHA256 = document.intentSHA256
configurationIntentSHA256 = document.replacingACL(nil).intentSHA256
}
}