Files

169 lines
7.2 KiB
Swift

import Foundation
import Testing
import XCTest
@testable import ShadowsocksX_NG2
/// 自定义规则合并进 ACL:两种规则子模式按各自内置来源与
/// 自定义规则合并;全局和直连模式不加载自定义规则。
final class CustomRuleMergeTests: XCTestCase {
private func custom(_ action: RuleAction, _ match: RuleMatch) -> CustomRule {
CustomRule(action: action, match: match)
}
func testACLIdentityDoesNotDependOnRuleOrderOrDuplicates() throws {
let first = ProxyRule(action: .direct, match: try RuleMatch(domainSuffix: "z.example"))
let equivalent = ProxyRule(
action: .direct, match: try RuleMatch(domainSuffix: "z.example"))
let second = ProxyRule(action: .proxy, match: try RuleMatch(domainExact: "a.example"))
let url = URL(fileURLWithPath: "/tmp/rules-order.acl")
for action in RuleDefaultAction.allCases {
let expected = ProxyACLDocument.rule(at: url, defaultAction: action, rules: [first, second])
let reordered = ProxyACLDocument.rule(
at: url, defaultAction: action, rules: [second, equivalent, first])
XCTAssertEqual(expected.content, reordered.content)
XCTAssertEqual(expected.sha256, reordered.sha256)
}
}
/// 「未匹配时代理」:中国直连候选 + 自定义直连进 bypass_list;自定义代理
/// 不写 proxy_list(默认已代理)。
func testProxyWhenUnmatchedMergesChinaDirectAndCustomDirect() throws {
let builtIn = [
ProxyRule(
action: .direct, match: try RuleMatch(nationalDomainSuffix: "cn")),
ProxyRule(
action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.0/24")),
]
let customRules = [
custom(.direct, try RuleMatch(domainSuffix: "internal.example")),
custom(.proxy, try RuleMatch(domainSuffix: "always-proxy.example")),
]
let result = CustomRuleValidator.validate(
custom: customRules, builtIn: builtIn, defaultAction: .proxyWhenUnmatched)
let merged = builtIn + result.accepted
XCTAssertTrue(result.rejected.isEmpty)
XCTAssertEqual(merged.count, 4)
let acl = ProxyACLDocument.rule(
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
defaultAction: .proxyWhenUnmatched,
rules: merged)
XCTAssertTrue(acl.content.contains("||cn\n"))
XCTAssertTrue(acl.content.contains("203.0.113.0/24\n"))
XCTAssertTrue(acl.content.contains("||internal.example\n"))
XCTAssertFalse(
acl.content.contains("||always-proxy.example\n"),
"未匹配时代理骨架不写 proxy_list;代理动作默认已覆盖")
XCTAssertTrue(acl.content.hasPrefix("[proxy_all]"))
}
/// 「未匹配时直连」:GFWList 代理候选 + 自定义代理进 proxy_list;自定义
/// 未遮蔽直连进 bypass_list。
func testDirectWhenUnmatchedMergesGFWListAndCustomRules() throws {
let builtIn = [
ProxyRule(
action: .proxy, match: try RuleMatch(domainSuffix: "blocked.example"))
]
let customRules = [
custom(.proxy, try RuleMatch(domainSuffix: "also-blocked.example")),
custom(.direct, try RuleMatch(domainSuffix: "unshadowed.example")),
]
let result = CustomRuleValidator.validate(
custom: customRules, builtIn: builtIn, defaultAction: .directWhenUnmatched)
let merged = builtIn + result.accepted
XCTAssertTrue(result.rejected.isEmpty)
let acl = ProxyACLDocument.rule(
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
defaultAction: .directWhenUnmatched,
rules: merged)
XCTAssertTrue(acl.content.contains("[proxy_list]\n"))
XCTAssertTrue(acl.content.contains("||blocked.example\n"))
XCTAssertTrue(acl.content.contains("||also-blocked.example\n"))
XCTAssertTrue(acl.content.contains("||unshadowed.example\n"))
XCTAssertTrue(acl.content.hasPrefix("[bypass_all]"))
}
/// 被遮蔽条目保留;ACL 优先级决定其实际作用。
func testShadowedCustomDirectIsRetainedAndExplained() throws {
let builtIn = [
ProxyRule(
action: .proxy, match: try RuleMatch(domainSuffix: "blocked.example"))
]
let customRules = [
custom(.direct, try RuleMatch(domainExact: "sub.blocked.example"))
]
let result = CustomRuleValidator.validate(
custom: customRules, builtIn: builtIn, defaultAction: .directWhenUnmatched)
let merged = builtIn + result.accepted
XCTAssertTrue(result.rejected.isEmpty)
XCTAssertEqual(result.relationships.first?.extent, .full)
let acl = ProxyACLDocument.rule(
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
defaultAction: .directWhenUnmatched,
rules: merged)
XCTAssertTrue(acl.content.contains("sub.blocked.example"), "保留可表达意图,路由由 sslocal 优先级决定")
}
/// 单模式遮蔽:在「未匹配时代理」下有效(不被遮蔽),在「未匹配时直连」下
/// 被 GFWList 代理遮蔽并返回原因。保存允许(另一模式可生效),编译按当前
/// 模式解释。
func testOneModeShadowedRuleIsValidInTheOtherMode() throws {
let gfwBuiltIn = [
ProxyRule(
action: .proxy, match: try RuleMatch(domainSuffix: "blocked.example"))
]
let chinaBuiltIn = [
ProxyRule(
action: .direct, match: try RuleMatch(nationalDomainSuffix: "cn"))
]
let customRules = [
custom(.direct, try RuleMatch(domainExact: "sub.blocked.example"))
]
let proxyDefault = CustomRuleValidator.validate(
custom: customRules, builtIn: chinaBuiltIn, defaultAction: .proxyWhenUnmatched)
XCTAssertTrue(
proxyDefault.rejected.isEmpty,
"未匹配时代理骨架无域名代理表,自定义直连不被遮蔽")
let directDefault = CustomRuleValidator.validate(
custom: customRules, builtIn: gfwBuiltIn, defaultAction: .directWhenUnmatched)
XCTAssertTrue(directDefault.rejected.isEmpty)
XCTAssertEqual(directDefault.relationships.first?.kind, .shadowing)
XCTAssertEqual(directDefault.accepted.count, 1)
}
}
struct RuleSchemeRuntimeTests {
@Test func reverseIPExceptionCannotOverrideSchemeAction() throws {
let proxy = ProxyRule(action: .proxy, match: try RuleMatch(ipv4CIDR: "203.0.113.0/24"))
let reverse = CustomRule(action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.8/32"))
let document = CustomRuleDocument(rules: [reverse])
let source = SslocalRuntimeDocument(servers: [], listen: ActivationFixture.listen)
let compiled = RuleRuntimeCompiler.compile(
.init(
source: source, mode: .rule, defaultAction: .directWhenUnmatched,
document: document, builtIn: [proxy], aclURL: URL(fileURLWithPath: "/tmp/scheme.acl")))
let content = try #require(compiled.aclRuntime?.content)
#expect(content.contains("203.0.113.0/24\n"))
#expect(!content.contains("203.0.113.8/32\n"))
#expect(content.contains("127.0.0.0/8\n"))
let collection = RulesCollection.load(
preset: .chinaProxy, custom: { [reverse] },
builtin: {
rulesFixture(
$0, rules: $0 == .chinaIPv4 ? [ProxyRule(action: .direct, match: proxy.match)] : [])
})
let result = try OfflineRuleMatcher.test(collection: collection, address: "203.0.113.8")
#expect(result.outcome == .proxy)
#expect(result.deciding.map(\.identity) == [proxy])
}
}