177 lines
8.7 KiB
Swift
177 lines
8.7 KiB
Swift
import Darwin
|
|
import Foundation
|
|
|
|
/// 把三条路线的测量结果归纳成结论:分流是否生效、各网站从哪个出口出去、IPv6 与系统代理有没有漏洞。
|
|
///
|
|
/// 关键在“原始出口”:绑定物理网卡、绕开 VPN 与代理测到的地址。某个目标按系统代理访问时的出口
|
|
/// 与原始出口相同就是直连,不同就是经过了代理。网页版读不到原始出口,只能拿国内回显去猜
|
|
public struct EgressAnalysis: Sendable, Equatable {
|
|
public enum Verdict: Sendable, Equatable {
|
|
/// 所有目标都连不上
|
|
case offline
|
|
/// 没有配置代理,全部直连
|
|
case noProxy
|
|
/// 配置了代理,但测试的目标都在直连
|
|
case proxyInactive
|
|
/// 国内直连,国际网站全部经代理
|
|
case split
|
|
/// 按规则分流:有的直连、有的经代理,但不是“国内直连、国际代理”这种标准分法
|
|
case mixed
|
|
/// 全部经代理
|
|
case fullProxy
|
|
/// VPN 不让绕开它连接,读不到原始出口,分不清哪个出口是代理
|
|
case unverified
|
|
}
|
|
|
|
public enum Path: Sendable, Equatable {
|
|
case direct, proxy, unknown
|
|
}
|
|
|
|
/// 一个出口与经它出去的目标
|
|
public struct Exit: Sendable, Equatable, Identifiable {
|
|
public struct Entry: Sendable, Equatable {
|
|
public var targetID: String
|
|
/// 实测:读到了这次连接的出口;否则是按国内 / 国际规则推断的
|
|
public var measured: Bool
|
|
}
|
|
|
|
public var ip: String
|
|
public var path: Path
|
|
public var entries: [Entry]
|
|
/// Cloudflare 给出的国家,归属地查不到时用它
|
|
public var country: String?
|
|
public var id: String { ip }
|
|
}
|
|
|
|
public enum Finding: Sendable, Equatable {
|
|
/// 隧道接管了 IPv4,IPv6 却没经过它,暴露了这个地址
|
|
case ipv6Leak(String)
|
|
/// 代理开着时 IPv6 连不出去:被拦截了,不会泄露
|
|
case ipv6Blocked
|
|
/// IPv6 也经过代理
|
|
case ipv6Proxied
|
|
/// 只设了系统代理,不读代理设置的程序仍在直连
|
|
case socketBypass
|
|
/// 本机在中国大陆,国内网站却走了代理
|
|
case domesticProxied
|
|
}
|
|
|
|
public var verdict: Verdict
|
|
/// 原始出口:绑定物理网卡测到的地址
|
|
public var rawAddresses: [String]
|
|
public var rawCountry: String?
|
|
/// 按系统代理访问各目标时用到的出口,经代理的在前
|
|
public var exits: [Exit]
|
|
/// 按系统代理连不上的目标(IPv6 基准目标不算,没有 IPv6 很常见)
|
|
public var failedTargetIDs: [String]
|
|
/// 连得上,但读不到出口,也没有可参照的出口
|
|
public var unassignedTargetIDs: [String]
|
|
public var findings: [Finding]
|
|
/// 结论依据:实测出口里直连与经代理的数量
|
|
public var directCount: Int
|
|
public var proxyCount: Int
|
|
|
|
public func path(of ip: String) -> Path {
|
|
guard !rawAddresses.isEmpty else { return .unknown }
|
|
return rawAddresses.contains { Self.sameExit($0, ip) } ? .direct : .proxy
|
|
}
|
|
|
|
public static func hit(_ hits: [EgressHit], _ target: EgressTarget, _ route: EgressRoute) -> EgressHit? {
|
|
hits.first { $0.targetID == target.id && $0.route == route }
|
|
}
|
|
|
|
public static func evaluate(_ hits: [EgressHit], environment: ProxyEnvironment) -> EgressAnalysis {
|
|
func ip(_ target: EgressTarget, _ route: EgressRoute) -> String? { hit(hits, target, route)?.ip }
|
|
|
|
let raw = [ip(.domestic, .interface), ip(.cloudflare, .interface), ip(.cloudflareIPv6, .interface)].compactMap { $0 }
|
|
let rawCountry = hits.lazy.filter { $0.route == .interface }.compactMap(\.country).first
|
|
var analysis = EgressAnalysis(verdict: .offline, rawAddresses: raw, rawCountry: rawCountry, exits: [],
|
|
failedTargetIDs: [], unassignedTargetIDs: [], findings: [],
|
|
directCount: 0, proxyCount: 0)
|
|
let system = hits.filter { $0.route == .system }
|
|
let ipv6 = EgressTarget.cloudflareIPv6.id
|
|
|
|
var exits: [Exit] = []
|
|
func add(_ address: String, _ entry: Exit.Entry, country: String?) {
|
|
if let index = exits.firstIndex(where: { $0.ip == address }) {
|
|
exits[index].entries.append(entry)
|
|
exits[index].country = exits[index].country ?? country
|
|
} else {
|
|
exits.append(Exit(ip: address, path: analysis.path(of: address), entries: [entry], country: country))
|
|
}
|
|
}
|
|
for hit in system {
|
|
guard let address = hit.ip else { continue }
|
|
add(address, .init(targetID: hit.targetID, measured: true), country: hit.country)
|
|
}
|
|
// 读不到出口的网站:国内的归到国内回显的出口,国外的归到 Cloudflare 的出口(没有就取国外网站最常见的出口)
|
|
let international = ip(.cloudflare, .system) ?? system
|
|
.filter { $0.targetID != EgressTarget.domestic.id && $0.targetID != ipv6 }
|
|
.compactMap(\.ip)
|
|
.reduce(into: [String: Int]()) { $0[$1, default: 0] += 1 }
|
|
.max { $0.value < $1.value }?.key
|
|
for hit in system where hit.ip == nil && hit.milliseconds != nil {
|
|
let region = EgressTarget.all.first { $0.id == hit.targetID }?.region
|
|
if let reference = region == .domestic ? ip(.domestic, .system) : international {
|
|
add(reference, .init(targetID: hit.targetID, measured: false), country: nil)
|
|
} else {
|
|
analysis.unassignedTargetIDs.append(hit.targetID)
|
|
}
|
|
}
|
|
// 经代理的在前,其次读不出的,直连最后;同类按目标数从多到少,再按出现顺序
|
|
let rank: (Path) -> Int = { $0 == .proxy ? 0 : $0 == .unknown ? 1 : 2 }
|
|
analysis.exits = exits.enumerated().sorted { lhs, rhs in
|
|
let left = (rank(lhs.element.path), -lhs.element.entries.count, lhs.offset)
|
|
let right = (rank(rhs.element.path), -rhs.element.entries.count, rhs.offset)
|
|
return left < right
|
|
}.map(\.element)
|
|
analysis.failedTargetIDs = system.filter { $0.milliseconds == nil && $0.targetID != ipv6 }.map(\.targetID)
|
|
|
|
// 结论只数实测出口(IPv4 基准与接入 Cloudflare 的网站);IPv6 基准单独在下面判断
|
|
let counted = system.compactMap { hit in hit.targetID == ipv6 ? nil : hit.ip.map { (hit.targetID, analysis.path(of: $0)) } }
|
|
analysis.directCount = counted.filter { $0.1 == .direct }.count
|
|
analysis.proxyCount = counted.filter { $0.1 == .proxy }.count
|
|
let domestic = counted.first { $0.0 == EgressTarget.domestic.id }?.1
|
|
let internationalDirect = counted.contains { $0.0 != EgressTarget.domestic.id && $0.1 == .direct }
|
|
|
|
if counted.isEmpty {
|
|
analysis.verdict = .offline
|
|
} else if raw.isEmpty {
|
|
analysis.verdict = .unverified
|
|
} else if analysis.proxyCount == 0 {
|
|
analysis.verdict = environment.hasProxy ? .proxyInactive : .noProxy
|
|
} else if analysis.directCount == 0 {
|
|
analysis.verdict = .fullProxy
|
|
} else if domestic == .direct && !internationalDirect {
|
|
analysis.verdict = .split
|
|
} else {
|
|
analysis.verdict = .mixed
|
|
}
|
|
|
|
guard analysis.proxyCount > 0 else { return analysis }
|
|
// 不读代理设置的连接:IPv4 也直连说明只有系统代理,IPv6 泄露就无从谈起;IPv4 被接管了才看 IPv6
|
|
if let socketV4 = ip(.cloudflare, .socket), analysis.path(of: socketV4) == .direct {
|
|
analysis.findings.append(.socketBypass)
|
|
} else if let rawV6 = ip(.cloudflareIPv6, .interface) {
|
|
if let socketV6 = ip(.cloudflareIPv6, .socket) {
|
|
analysis.findings.append(sameExit(socketV6, rawV6) ? .ipv6Leak(socketV6) : .ipv6Proxied)
|
|
} else {
|
|
analysis.findings.append(.ipv6Blocked)
|
|
}
|
|
}
|
|
if rawCountry == "CN", domestic == .proxy, analysis.verdict == .mixed {
|
|
analysis.findings.append(.domesticProxied)
|
|
}
|
|
return analysis
|
|
}
|
|
|
|
/// IPv4 比整个地址;IPv6 比前 64 位:同一个网络里,不同连接可能用不同的临时地址
|
|
static func sameExit(_ lhs: String, _ rhs: String) -> Bool {
|
|
if lhs == rhs { return true }
|
|
var left = in6_addr()
|
|
var right = in6_addr()
|
|
guard inet_pton(AF_INET6, lhs, &left) == 1, inet_pton(AF_INET6, rhs, &right) == 1 else { return false }
|
|
return withUnsafeBytes(of: &left) { a in withUnsafeBytes(of: &right) { b in a.prefix(8).elementsEqual(b.prefix(8)) } }
|
|
}
|
|
}
|