mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 17:24:50 +00:00
Add Mainland China proxy routing with independent rule workspaces. Preserve existing routing choices through equivalent preset mappings. BREAKING CHANGE: existing custom rules and enablement settings are not migrated. Recreate them in the appropriate preset.
112 lines
4.0 KiB
Swift
112 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
/// Coverage is explanatory, never a persistence rejection or a routing priority.
|
|
struct RuleRelationship: Equatable, Sendable {
|
|
enum Kind: Hashable, Sendable { case absorption, shadowing }
|
|
enum Extent: Sendable { case full, partial }
|
|
let rule: ProxyRule
|
|
let kind: Kind
|
|
let extent: Extent
|
|
let covering: [ProxyRule]
|
|
}
|
|
|
|
/// Analyze the complete candidate set. Optional subjects limit explanation work
|
|
/// to custom entries at the existing validation seam without narrowing context.
|
|
struct RuleAnalysis: Equatable, Sendable {
|
|
let identities: [ProxyRule]
|
|
let relationships: [RuleRelationship]
|
|
|
|
var contentVersion: String {
|
|
String(
|
|
ProxyACLDocument.digest(identities.map(\.contentToken).joined(separator: "\n")).prefix(12))
|
|
}
|
|
|
|
init(rules: [ProxyRule], subjects: [ProxyRule]? = nil) {
|
|
let identities = Set(rules).map { ($0, $0.contentToken) }
|
|
.sorted { $0.1 < $1.1 }.map(\.0)
|
|
self.identities = identities
|
|
let targets = Set(subjects ?? rules)
|
|
relationships = identities.filter { targets.contains($0) }.flatMap { target in
|
|
[RuleRelationship.Kind.absorption, .shadowing].compactMap { kind in
|
|
let covering = identities.filter { other in
|
|
guard other != target else { return false }
|
|
switch kind {
|
|
case .absorption:
|
|
guard other.action == target.action else { return false }
|
|
case .shadowing:
|
|
guard other.action != target.action,
|
|
RuleCoverage.winningAction(for: target.match) == other.action
|
|
else { return false }
|
|
}
|
|
return RuleCoverage.intersects(other.match, target.match)
|
|
}
|
|
guard !covering.isEmpty else { return nil }
|
|
return RuleRelationship(
|
|
rule: target, kind: kind,
|
|
extent: RuleCoverage.fullyCovered(target.match, by: covering.map(\.match))
|
|
? .full : .partial,
|
|
covering: covering)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fixed policy includes a semantic simple-hostname condition, not an editable
|
|
/// regex identity. Coverage keeps that condition separate from ordinary rules.
|
|
struct FixedRuleCoverage: Equatable, Sendable {
|
|
let extent: RuleRelationship.Extent
|
|
let matches: [RuleMatch]
|
|
let includesSimpleHostname: Bool
|
|
}
|
|
|
|
extension RuleCoverage {
|
|
static let fixedLocalMatches: [RuleMatch] = {
|
|
FixedLocalProxyRanges.ipRanges.compactMap { value in
|
|
value.contains(":") ? try? RuleMatch(ipv6CIDR: value) : try? RuleMatch(ipv4CIDR: value)
|
|
} + [.localhostSuffix, .localSuffix]
|
|
}()
|
|
|
|
static func fixedLocalCoverage(of match: RuleMatch) -> FixedRuleCoverage? {
|
|
let matches = fixedLocalMatches.filter { intersects($0, match) }
|
|
let simple: Bool
|
|
let simpleIsFull: Bool
|
|
switch match.pattern {
|
|
case .domainExact(let value):
|
|
simple = !value.contains(".")
|
|
simpleIsFull = simple
|
|
case .domainSuffix(let value):
|
|
simple = !value.contains(".")
|
|
simpleIsFull = false
|
|
case .ipv4CIDR, .ipv6CIDR:
|
|
simple = false
|
|
simpleIsFull = false
|
|
}
|
|
guard simple || !matches.isEmpty else { return nil }
|
|
return FixedRuleCoverage(
|
|
extent: simpleIsFull || fullyCovered(match, by: matches) ? .full : .partial,
|
|
matches: matches, includesSimpleHostname: simple)
|
|
}
|
|
}
|
|
|
|
extension RuleAnalysis {
|
|
/// Select the actual ACL skeleton before calculating coverage.
|
|
static func runtimeCandidates(_ rules: [ProxyRule], defaultAction: RuleDefaultAction)
|
|
-> [ProxyRule]
|
|
{
|
|
let expressed = rules.filter { defaultAction.expresses($0.action) }
|
|
let index = RulesOverlapIndex(rules: expressed)
|
|
return expressed.filter { rule in
|
|
let analysis = RuleAnalysis(rules: index.overlapping(rule.match), subjects: [rule])
|
|
return !analysis.relationships.contains {
|
|
$0.extent == .full
|
|
&& ($0.kind == .shadowing
|
|
|| $0.covering.contains { other in
|
|
RuleCoverage.fullyCovered(rule.match, by: [other.match])
|
|
&& (!RuleCoverage.fullyCovered(other.match, by: [rule.match])
|
|
|| other.contentToken < rule.contentToken)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|