mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
251 lines
11 KiB
Swift
251 lines
11 KiB
Swift
import Foundation
|
|
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
final class ProxyACLTests: XCTestCase {
|
|
func testDirectACLUsesBypassAllAndFixedLocalSafetyRules() {
|
|
let acl = ProxyACLDocument.direct(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"))
|
|
|
|
XCTAssertTrue(acl.isWellFormed)
|
|
XCTAssertTrue(acl.content.hasPrefix("[bypass_all]\n[bypass_list]\n"))
|
|
XCTAssertFalse(acl.content.contains("[proxy_list]"))
|
|
XCTAssertTrue(acl.content.contains("127.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("10.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("172.16.0.0/12\n"))
|
|
XCTAssertTrue(acl.content.contains("192.168.0.0/16\n"))
|
|
XCTAssertTrue(acl.content.contains("169.254.0.0/16\n"))
|
|
XCTAssertTrue(acl.content.contains("::1/128\n"))
|
|
XCTAssertTrue(acl.content.contains("fe80::/10\n"))
|
|
XCTAssertTrue(acl.content.contains("fc00::/7\n"))
|
|
XCTAssertTrue(acl.content.contains("||localhost\n"))
|
|
XCTAssertTrue(acl.content.contains("||local\n"))
|
|
XCTAssertTrue(acl.content.contains("^[^.]+$\n"))
|
|
XCTAssertFalse(acl.content.contains("100.64.0.0/10"))
|
|
}
|
|
|
|
/// 全局模式:只含固定本地绕过的最小 ACL;不混入中国列表、
|
|
/// GFWList 或自定义规则。公网目标默认代理,本地目标固定直连。
|
|
func testGlobalACLUsesProxyAllAndOnlyFixedLocalSafetyRules() {
|
|
let acl = ProxyACLDocument.global(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"))
|
|
|
|
XCTAssertTrue(acl.isWellFormed)
|
|
XCTAssertEqual(acl.summary, "global")
|
|
XCTAssertTrue(acl.content.hasPrefix("[proxy_all]\n[bypass_list]\n"))
|
|
XCTAssertFalse(acl.content.contains("[proxy_list]"))
|
|
XCTAssertFalse(acl.content.contains("[bypass_all]"))
|
|
XCTAssertTrue(acl.content.contains("127.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("10.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("172.16.0.0/12\n"))
|
|
XCTAssertTrue(acl.content.contains("192.168.0.0/16\n"))
|
|
XCTAssertTrue(acl.content.contains("169.254.0.0/16\n"))
|
|
XCTAssertTrue(acl.content.contains("::1/128\n"))
|
|
XCTAssertTrue(acl.content.contains("fe80::/10\n"))
|
|
XCTAssertTrue(acl.content.contains("fc00::/7\n"))
|
|
XCTAssertTrue(acl.content.contains("||localhost\n"))
|
|
XCTAssertTrue(acl.content.contains("||local\n"))
|
|
XCTAssertTrue(acl.content.contains("^[^.]+$\n"))
|
|
XCTAssertFalse(acl.content.contains("100.64.0.0/10"), "不加入 CGNAT")
|
|
// 仅固定本地绕过:不携带任何来源规则或用户规则痕迹。
|
|
let rules = Array(acl.content.split(separator: "\n").map(String.init).dropFirst(2))
|
|
XCTAssertEqual(
|
|
rules, FixedLocalProxyRanges.aclBypassRules,
|
|
"全局 ACL 的规则区应与固定本地绕过完全一致")
|
|
}
|
|
|
|
func testACLRejectsChangedDigestAndNonAbsolutePath() {
|
|
let valid = ProxyACLDocument.direct(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/acl-active.ini"))
|
|
let badDigest = ProxyACLDocument(
|
|
path: valid.path,
|
|
summary: valid.summary,
|
|
content: valid.content,
|
|
sha256: String(repeating: "0", count: 64))
|
|
let relativePath = ProxyACLDocument(
|
|
path: "acl-active.ini",
|
|
summary: valid.summary,
|
|
content: valid.content,
|
|
sha256: valid.sha256)
|
|
|
|
XCTAssertFalse(badDigest.isWellFormed)
|
|
XCTAssertFalse(relativePath.isWellFormed)
|
|
}
|
|
|
|
/// ADR-0011:契约只传身份字段。解码后的文档没有 content,等式与 well-formed
|
|
/// 校验只看 path/summary/sha256;生成侧 content 存在时仍自证摘要。
|
|
func testACLIdentityEqualityIgnoresContentPresence() {
|
|
let generated = ProxyACLDocument.direct(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/acl-active.ini"))
|
|
let decodedShape = ProxyACLDocument(
|
|
path: generated.path,
|
|
summary: generated.summary,
|
|
content: "",
|
|
sha256: generated.sha256)
|
|
|
|
XCTAssertEqual(generated, decodedShape, "content 不参与身份等式")
|
|
XCTAssertTrue(decodedShape.isWellFormed, "无 content 时按身份形状校验")
|
|
XCTAssertFalse(
|
|
ProxyACLDocument(
|
|
path: generated.path, summary: "", content: "", sha256: generated.sha256
|
|
).isWellFormed,
|
|
"summary 为空仍拒绝")
|
|
XCTAssertFalse(
|
|
ProxyACLDocument(
|
|
path: generated.path, summary: generated.summary, content: "",
|
|
sha256: "not-a-digest"
|
|
).isWellFormed,
|
|
"摘要形状无效仍拒绝")
|
|
let mismatchedContent = ProxyACLDocument(
|
|
path: generated.path, summary: generated.summary, content: "[proxy_all]\n",
|
|
sha256: generated.sha256)
|
|
XCTAssertFalse(mismatchedContent.isWellFormed, "携带 content 时仍自证摘要")
|
|
}
|
|
|
|
func testACLIdentityChangeBreaksEquality() {
|
|
let base = ProxyACLDocument.direct(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/acl-active.ini"))
|
|
let otherPath = ProxyACLDocument.direct(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/other/acl-active.ini"))
|
|
let otherSummary = ProxyACLDocument.global(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/acl-active.ini"))
|
|
|
|
XCTAssertNotEqual(base, otherPath)
|
|
XCTAssertNotEqual(base, otherSummary)
|
|
}
|
|
|
|
// MARK: - 规则模式
|
|
|
|
/// 规则+未匹配时代理:proxy_all + 固定本地绕过 + 中国域名直连候选。
|
|
func testRuleProxyDefaultACLUsesProxyAllAndChinaDirectCandidates() throws {
|
|
let rules = [
|
|
ProxyRule(action: .direct, match: try RuleMatch(nationalDomainSuffix: "cn")),
|
|
ProxyRule(action: .direct, match: try RuleMatch(domainSuffix: "qq.com")),
|
|
ProxyRule(
|
|
action: .direct, match: try RuleMatch(domainExact: "api.example.cn")),
|
|
]
|
|
let acl = ProxyACLDocument.rule(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
|
|
defaultAction: .proxyWhenUnmatched,
|
|
rules: rules)
|
|
|
|
XCTAssertTrue(acl.isWellFormed)
|
|
XCTAssertEqual(acl.summary, "rule-proxy-default")
|
|
XCTAssertTrue(acl.content.hasPrefix("[proxy_all]\n[bypass_list]\n"))
|
|
XCTAssertFalse(acl.content.contains("[bypass_all]"))
|
|
XCTAssertFalse(acl.content.contains("[proxy_list]"))
|
|
// 固定本地绕过仍在。
|
|
XCTAssertTrue(acl.content.contains("127.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("||localhost\n"))
|
|
// 中国域名直连候选。
|
|
XCTAssertTrue(acl.content.contains("||cn\n"))
|
|
XCTAssertTrue(acl.content.contains("||qq.com\n"))
|
|
XCTAssertTrue(acl.content.contains("|api.example.cn\n"))
|
|
XCTAssertFalse(acl.content.contains("100.64.0.0/10"), "不加入 CGNAT")
|
|
}
|
|
|
|
/// 规则+未匹配时直连:bypass_all + 固定本地 + GFWList 可生效
|
|
/// 代理规则。两种默认动作不把全部内置来源无条件并集,因此不混入中国直连项。
|
|
func testRuleDirectDefaultACLUsesBypassAllAndGFWListProxyCandidates() throws {
|
|
let rules = [
|
|
ProxyRule(
|
|
action: .proxy, match: try RuleMatch(domainSuffix: "blocked.example")),
|
|
ProxyRule(
|
|
action: .proxy, match: try RuleMatch(domainSuffix: "also-blocked.example")),
|
|
ProxyRule(
|
|
action: .direct, match: try RuleMatch(domainSuffix: "unshadowed.example")),
|
|
]
|
|
let acl = ProxyACLDocument.rule(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
|
|
defaultAction: .directWhenUnmatched,
|
|
rules: rules)
|
|
|
|
XCTAssertTrue(acl.isWellFormed)
|
|
XCTAssertEqual(acl.summary, "rule-direct-default")
|
|
XCTAssertTrue(acl.content.hasPrefix("[bypass_all]\n[bypass_list]\n"))
|
|
XCTAssertEqual(
|
|
acl.content.components(separatedBy: "[proxy_list]\n").last,
|
|
"||also-blocked.example\n||blocked.example\n")
|
|
XCTAssertTrue(acl.content.contains("||unshadowed.example\n"), "未遮蔽例外参与编译进 bypass_list")
|
|
XCTAssertFalse(acl.content.contains("[proxy_all]"))
|
|
// 不无条件并集中国来源。
|
|
XCTAssertFalse(acl.content.contains("||cn\n"))
|
|
// 固定本地绕过仍在。
|
|
XCTAssertTrue(acl.content.contains("127.0.0.0/8\n"))
|
|
XCTAssertTrue(acl.content.contains("||localhost\n"))
|
|
// 未匹配目标走 bypass_all 默认直连。
|
|
XCTAssertTrue(acl.content.hasPrefix("[bypass_all]"))
|
|
}
|
|
|
|
/// 规则+未匹配时直连:IP 字面目标无可用规则命中时直连(无 IP 代理规则时
|
|
/// proxy_list 只有域名项)。
|
|
func testRuleDirectDefaultACLLeavesIPLiteralsUnmatchedDirect() throws {
|
|
let rules = [
|
|
ProxyRule(
|
|
action: .proxy, match: try RuleMatch(domainSuffix: "blocked.example"))
|
|
]
|
|
let acl = ProxyACLDocument.rule(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
|
|
defaultAction: .directWhenUnmatched,
|
|
rules: rules)
|
|
|
|
let proxySection =
|
|
(acl.content.components(separatedBy: "[proxy_list]\n").last ?? "")
|
|
.split(whereSeparator: \.isNewline).map(String.init)
|
|
XCTAssertTrue(
|
|
proxySection.allSatisfy { line in
|
|
line.hasPrefix("||") || line.hasPrefix("|") || line.hasPrefix("^")
|
|
},
|
|
"GFWList 代理候选只含域名规则;IP 字面目标未命中即直连")
|
|
}
|
|
|
|
/// 规则+未匹配时代理:proxy_all 同时含中国域名与 IPv4 CIDR
|
|
/// 直连候选,固定本地绕过仍优先。
|
|
func testRuleProxyDefaultACLIncludesChinaIPv4CIDRCandidates() throws {
|
|
let rules = [
|
|
ProxyRule(
|
|
action: .direct, match: try RuleMatch(nationalDomainSuffix: "cn")),
|
|
ProxyRule(
|
|
action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.0/24")),
|
|
ProxyRule(
|
|
action: .direct, match: try RuleMatch(ipv4CIDR: "198.51.100.0/24")),
|
|
]
|
|
let acl = ProxyACLDocument.rule(
|
|
at: URL(fileURLWithPath: "/tmp/ssxng-test/sslocal-active.acl"),
|
|
defaultAction: .proxyWhenUnmatched,
|
|
rules: rules)
|
|
|
|
XCTAssertTrue(acl.isWellFormed)
|
|
XCTAssertTrue(acl.content.hasPrefix("[proxy_all]\n[bypass_list]\n"))
|
|
XCTAssertTrue(acl.content.contains("||cn\n"), "中国域名直连候选")
|
|
XCTAssertTrue(acl.content.contains("203.0.113.0/24\n"), "中国 IPv4 CIDR 直连候选")
|
|
XCTAssertTrue(acl.content.contains("198.51.100.0/24\n"), "中国 IPv4 CIDR 直连候选")
|
|
// 固定本地绕过仍排在中国候选之前(优先级)。
|
|
let lines = acl.content.split(separator: "\n").map(String.init)
|
|
let localIndex = lines.firstIndex(of: "127.0.0.0/8") ?? -1
|
|
let cidrIndex = lines.firstIndex(of: "203.0.113.0/24") ?? -1
|
|
XCTAssertTrue(localIndex >= 0 && cidrIndex > localIndex, "固定本地绕过应优先于中国 CIDR")
|
|
}
|
|
|
|
/// 冲突元数据不影响 ACL 行;同匹配只输出一行。
|
|
func testRuleACLLineMapping() throws {
|
|
XCTAssertEqual(
|
|
ProxyRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
|
|
.aclLine,
|
|
"||example.com")
|
|
XCTAssertEqual(
|
|
ProxyRule(action: .direct, match: try RuleMatch(domainExact: "a.example.com"))
|
|
.aclLine,
|
|
"|a.example.com")
|
|
XCTAssertEqual(
|
|
ProxyRule(action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.0/24"))
|
|
.aclLine,
|
|
"203.0.113.0/24")
|
|
XCTAssertEqual(
|
|
ProxyRule(action: .direct, match: try RuleMatch(ipv6CIDR: "2001:db8::/32"))
|
|
.aclLine,
|
|
"2001:db8::/32")
|
|
}
|
|
}
|