mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
169 lines
7.2 KiB
Swift
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])
|
|
}
|
|
}
|