Files

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