Files

104 lines
5.2 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 CFNetwork
import Darwin
import Foundation
/// 本机的代理环境:VPN / 增强模式隧道、系统代理、DNS 是否被代理接管、正在运行的代理软件。
/// 只读系统配置,不发任何请求
public struct ProxyEnvironment: Sendable, Codable, Equatable {
public struct Proxy: Sendable, Codable, Equatable {
public enum Kind: String, Sendable, Codable { case http, https, socks }
public var kind: Kind
public var host: String
public var port: Int
}
/// 默认路由所在隧道的服务名(例如 Surge)与接口(utun6)
public var tunnelName: String?
public var tunnelInterface: String?
public var proxies: [Proxy]
/// 自动代理配置(PAC)地址
public var pacURL: String?
public var dnsServers: [String]
/// 承载流量的物理网卡,绕开代理时绑定它
public var physicalInterface: String?
public var physicalName: String?
public var localIPv4: [String]
/// 正在运行的代理 / VPN 软件
public var apps: [String]
public init(tunnelName: String? = nil, tunnelInterface: String? = nil, proxies: [Proxy] = [], pacURL: String? = nil,
dnsServers: [String] = [], physicalInterface: String? = nil, physicalName: String? = nil,
localIPv4: [String] = [], apps: [String] = []) {
self.tunnelName = tunnelName
self.tunnelInterface = tunnelInterface
self.proxies = proxies
self.pacURL = pacURL
self.dnsServers = dnsServers
self.physicalInterface = physicalInterface
self.physicalName = physicalName
self.localIPv4 = localIPv4
self.apps = apps
}
/// 配置了任何一种代理:隧道、系统代理或 PAC
public var hasProxy: Bool { tunnelInterface != nil || !proxies.isEmpty || pacURL != nil }
/// DNS 指向 198.18.0.0/15:Surge、Clash 等的 fake-ip,域名解析由代理接管
public var usesFakeIPDNS: Bool { dnsServers.contains(where: Self.isFakeIP) }
/// 网络环境的指纹:隧道、代理、网卡或本地地址变了,之前的检测结果就不再代表当前网络
public var signature: String {
[tunnelInterface ?? "", proxies.map { "\($0.kind.rawValue)\($0.host):\($0.port)" }.joined(separator: ","),
pacURL ?? "", physicalInterface ?? "", localIPv4.joined(separator: ","), dnsServers.joined(separator: ",")]
.joined(separator: "|")
}
static func isFakeIP(_ address: String) -> Bool {
var v4 = in_addr()
guard inet_pton(AF_INET, address, &v4) == 1 else { return false }
let value = UInt32(bigEndian: v4.s_addr)
return value >> 17 == (198 << 24 | 18 << 16) >> 17
}
}
public enum ProxyEnvironmentReader {
/// 按应用名识别的常见代理与 VPN 软件
static let knownApps = [
"Surge", "ClashX", "ClashX Pro", "ClashX Meta", "Clash Verge", "Clash Nyanpasu", "Mihomo Party", "FlClash",
"Stash", "Shadowrocket", "Quantumult X", "Loon", "sing-box", "SFM", "Hiddify", "V2rayU", "V2RayXS", "NekoRay",
"Tailscale", "WireGuard", "Cloudflare WARP", "Mullvad VPN", "NordVPN", "ExpressVPN", "Proton VPN", "Surfshark",
"Astrill", "Outline", "Tunnelblick", "Viscosity", "OpenVPN Connect", "GlobalProtect", "Cisco Secure Client",
"FortiClient", "Zscaler",
]
/// `runningAppNames` 由界面层从 NSWorkspace 取,这里不依赖 AppKit
public static func read(details: NetworkDetails, runningAppNames: [String]) -> ProxyEnvironment {
let settings = CFNetworkCopySystemProxySettings()?.takeRetainedValue() as? [String: Any] ?? [:]
let (proxies, pac) = parseProxies(settings)
return ProxyEnvironment(tunnelName: details.tunnel?.name, tunnelInterface: details.tunnel?.bsdName,
proxies: proxies, pacURL: pac, dnsServers: details.dnsServers,
physicalInterface: details.physical?.bsdName, physicalName: details.physical?.displayName,
localIPv4: details.physical?.ipv4 ?? [], apps: proxyApps(in: runningAppNames))
}
static func parseProxies(_ settings: [String: Any]) -> ([ProxyEnvironment.Proxy], pac: String?) {
func enabled(_ key: String) -> Bool { (settings[key] as? NSNumber)?.boolValue == true }
var proxies: [ProxyEnvironment.Proxy] = []
for (kind, prefix) in [(ProxyEnvironment.Proxy.Kind.http, "HTTP"), (.https, "HTTPS"), (.socks, "SOCKS")] {
guard enabled("\(prefix)Enable"), let host = settings["\(prefix)Proxy"] as? String, !host.isEmpty else { continue }
let port = (settings["\(prefix)Port"] as? NSNumber)?.intValue ?? 0
proxies.append(.init(kind: kind, host: host, port: port))
}
let pac = enabled("ProxyAutoConfigEnable") ? (settings["ProxyAutoConfigURLString"] as? String).flatMap { $0.isEmpty ? nil : $0 } : nil
return (proxies, pac)
}
static func proxyApps(in names: [String]) -> [String] {
var found: [String] = []
for name in names where knownApps.contains(where: { name == $0 || name.hasPrefix($0 + " ") }) && !found.contains(name) {
found.append(name)
}
return found
}
}