Files
Qiu Yuzhou f6abd4343e feat(rules)!: add isolated routing presets
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.
2026-10-25 14:32:12 +08:00

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)
})
}
}
}
}