Files
ShadowsocksX-NG/docs/research/sslocal-acl-vs-pac.md
Qiu Yuzhou fb6be460b8 docs(research): assess sslocal ACL as PAC alternative
Document routing, reload, DNS, UDP, and compatibility boundaries
before considering an NG2 runtime migration.
2026-09-24 21:40:51 +08:00

272 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# sslocal ACL 与当前 PAC 方案对比研究
> 研究日期:2026-09-24
> 上游基线:`shadowsocks-rust` v1.25.0,commit `ab388c7466d21f979430e33cc9ef10e22fb05955`。
> 证据范围:上游仓库源码、上游 README/config schema、上游 release 与 issue;未使用博客或其他二手材料。本文只写研究结论,不修改 NG2 源码、配置或产品文档。
## 结论摘要
1. `sslocal` ACL 是本地入站代理收到目标地址后做的“直连 / 通过 Shadowsocks server”选择器。它支持默认黑名单/白名单、域名精确匹配、子域匹配、正则、IP 与 CIDR;它不是 macOS 系统代理配置器,也不是一个能直接输出 PAC 的规则引擎。
2. ACL 不支持本地客户端侧的 `BLOCK` 动作。`[outbound_block_*]` 属于 `ssserver` 的远端出站阻断语义,不能当作 sslocal 的本地阻断规则。
3. ACL 在启动/配置构造时读入并编译到内存。v1.25.0 的 `SIGUSR1` 重载路径只把新配置中的 `server` 列表交给 balancer,不替换已运行 local context 中的 ACL;修改 ACL 文件后需要重启 sslocal 才能可靠生效。
4. 对本仓库而言,“macOS 全局 SOCKS + sslocal ACL”技术上可行,但不是当前 PAC 的等价替换。它会把决策点从系统 PAC 的 `FindProxyForURL` 移到 sslocal 收到的目标地址;URL 路径、端口、HTTP 方法、进程和未使用 SOCKS 的应用不在同一个能力边界内。
5. 推荐把它作为一种新的、明确命名的“ACL 全局 SOCKS”运行模式进行验证;不要直接把现有 PAC 模式的产品语义改名为 ACL。至少应先验证域名/IP/DNS、UDP、多个 local、ACL 变更期间的重启收敛,以及真实 macOS SystemConfiguration 和应用兼容性。
## 1. ACL 文件格式与规则语义
### 1.1 上游定义的 section
上游 README 和 `crates/shadowsocks-service/src/acl/mod.rs` 都把 local 与 remote 的 section 分开。对 `sslocal` 有效的是:
| Section | 含义 |
| --- | --- |
| `[proxy_all]` | `BlackList`:未命中的目标默认走 Shadowsocks proxy;默认模式也是这个模式 |
| `[bypass_all]` | `WhiteList`:未命中的目标默认直连 |
| `[bypass_list]` | 命中后直连 |
| `[proxy_list]` | 命中后走 Shadowsocks proxy |
README 把 `[reject_all]`、`[accept_all]`、`[black_list]`、`[white_list]` 主要描述为 `ssserver` 客户端访问控制;但 v1.25.0 parser 同时把 `[black_list]` 当作 local 的 `bypass_list` 别名、把 `[white_list]` 当作 local 的 `proxy_list` 别名。`[outbound_block_all]`、`[outbound_allow_all]`、`[outbound_block_list]`、`[outbound_allow_list]` 才是明确的 `ssserver` 远端出站控制。NG2 生成 local ACL 时应只生成 `[proxy_all]`/`[bypass_all]` 与 `[bypass_list]`/`[proxy_list]`,避免复用有双重语义的 section 名。
来源:
- 上游 README 的 ACL 章节:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/README.md#L1112-L1132>
- 上游 ACL 源码的 section、mode 和 rule 注释:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L286-L337>
### 1.2 单条规则
解析顺序和语义如下:
| 写法 | 语义 |
| --- | --- |
| `10.0.0.0/8`、`127.0.0.1`、`::1` | IP/CIDR 规则 |
| `|example.com` | 精确域名匹配 |
| `||example.com` | `example.com` 及其子域匹配 |
| `(^|\\.)example\\.com$` | 正则域名匹配 |
| `# comment` | 注释;源码只把行首就是 `#` 的行当作注释 |
规则行必须是 ASCII;非 ASCII 行会记录 warning 后跳过。以 `|` 或 `||` 开头的规则先按精确/域名树处理;其他非 section 行先尝试 CIDR/IP,再作为正则保存。因此配置生成器不能把任意 Adblock 语法原样当作 ACL 语法。
来源:
- 规则形式的上游注释:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L328-L337>
- 读取、ASCII 过滤、`||`/`|`、IP/CIDR 与正则分流:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L351-L465>
### 1.3 命中优先级与 DNS
对域名目标,`check_target_bypassed` 先检查域名规则:`proxy_list` 命中返回 proxy,`bypass_list` 命中返回直连。未命中且 ACL 中存在 IP 规则时,sslocal 会调用其 `Context` 的 DNS resolver,把解析到的 IP 再与对应 IP 规则比较;解析失败或没有 IP 命中时回到默认模式。
对已经是 IP 的目标,不需要 DNS。源码中 IP 检查先看 `bypass_list`,再看 `proxy_list`,最后使用默认模式;因此“同一 IP 同时出现在两边”不是应该依赖的可移植配置,生成器应在生成前拒绝冲突。
这意味着 ACL 的 DNS 行为不是“所有 DNS 都通过远端 Shadowsocks server”:它是为了决定某个目标是否 bypass 而可能进行本地 resolver 查询。另有独立的 `local-dns` 入站协议,它才是按照 ACL 在 local DNS 与 remote DNS 之间选择的 DNS relay。
来源:
- host/IP 命中与默认模式:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L475-L548>
- 域名目标的 DNS fallback 与直连/代理结果:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L555-L589>
- `local-dns` 的官方说明:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/README.md#L41-L55>
### 1.4 “直连 / proxy”在 sslocal 中如何落地
TCP 路径调用 ACL 后,命中 bypass 就直接连接目标,未 bypass 才经 configured Shadowsocks server;UDP association 也在发送数据时调用同一个 `check_target_bypassed`。ACL 的结果不是返回给 macOS 的 `DIRECT`/`SOCKS` 字符串,而是由 sslocal 自己选择 outbound socket 路径。
来源:
- TCP auto-proxy:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs#L145-L161>
- UDP association:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/net/udp/association.rs#L515-L522>
## 2. sslocal 的加载、应用、重载与失败行为
### 2.1 配置入口和作用域
v1.25.0 有三种相关入口:
- CLI `--acl PATH`:加载一个全局 ACL。
- 配置顶层 `acl`:同样形成全局 local context 的 ACL。
- `locals[]` 内的 `acl`:为单个 SOCKS/HTTP/DNS 等 local 实例指定 ACL;它会覆盖从全局 context clone 来的 ACL。
上游 `SSConfig` 与 `SSLocalExtConfig` 都声明了 `acl: Option<String>`;`locals[]` 例子也明确展示了 instance-specific ACL。`ServiceContext` 内保存的是 `Option<Arc<AccessControl>>`,local server 创建时把解析好的 ACL 放进 context;因此运行期使用的是已加载的对象,不是每次请求重新读文件。
来源:
- 顶层与 `locals[]` schema:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/config.rs#L400-L428>、<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/config.rs#L419-L525>
- 官方多 local 配置示例:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/README.md#L620-L705>
- ACL 加载失败被转成配置错误:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/config.rs#L2078-L2096>
- context 保存 ACL 以及 local-specific override:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/context.rs#L22-L39>、<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/mod.rs#L163-L180>、<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/mod.rs#L272-L285>
### 2.2 初始加载失败行为
可以确认的失败行为:
- ACL 文件打不开、权限不足或路径不存在:`File::open` 返回错误,配置加载失败。
- 正则无法编译:最终 `into_rules()?` 返回错误,配置加载失败。
- 单条规则含非 ASCII:不是全局失败;记录 warning 并跳过该行。
- 域名用于 IP 规则 fallback 时 DNS 解析失败:不把请求判为错误,而是回到该 ACL 的默认 proxy/bypass 策略。
- 未知 section 不会被当作一个严格 schema 错误;解析器的默认分支会把它继续当作 IP/域名/正则规则处理。这是配置生成器应自行校验的风险,而不是可以依赖的容错协议。
`--acl` CLI 入口同样直接调用 `AccessControl::load_from_file`,失败会变成 `LoadAclFailure`。因此 NG2 若生成 ACL,应先原子写完并自检,再把新路径交给 sslocal;不能把一个半写入或不可读文件交给正在重启的 child。
来源:
- ACL parser 的错误/警告路径:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L351-L465>
- CLI `--acl` 的失败映射:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/src/service/local.rs#L888-L899>
### 2.3 是否支持 ACL 热加载
结论:**v1.25.0 的 sslocal 不提供 ACL 文件热加载。**
上游的 reload task 只有 `config_path` 和 `balancer`。收到 Unix `SIGUSR1` 后,它重新读取配置文件,但只把 `source_config.server` 交给 `balancer.reset_servers(...)`;已运行的 `Server`/local context 不会因为 ACL 文件改变而重建。配置文件本身被重新解析时,如果新 ACL 已损坏,reload task 会记录加载失败并保留旧运行实例;如果新 ACL 有效,也只刷新服务器列表,旧 ACL 仍继续用于现有 local context。
这与 NG2 当前 wrapper 的协议正好形成风险:`ShadowsocksX-NG2/Agent/main.swift:13-15` 把“仅 servers 变化”转成向 sslocal 转发 `SIGUSR1`,`main.swift:164-192` 也只在监听结构变化时重启。引入 ACL 后,ACL 内容/path/hash 必须成为 restart fingerprint;不能沿用“只 forward SIGUSR1”的分支。
来源:
- sslocal 建立一次 `Server`,再单独启动 reload task:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/src/service/local.rs#L1001-L1018>
- reload task 只 reset servers:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/src/service/local.rs#L1068-L1111>
- `SIGUSR1` 监听:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/src/service/local.rs#L1117-L1129>
## 3. ACL 与 PAC 的能力边界
这里的 PAC 对比以本仓库当前 NG2 实现为准,而不是把一个未实现的 PAC 规则子集算进产品能力。
系统边界也不同:Apple 的 CFNetwork 文档把 PAC URL/JavaScript 自动配置和 SOCKS 作为不同的 proxy 类型;自动配置脚本由系统执行来决定某个 URL 使用什么 proxy,而 SOCKS 是一个固定的 proxy endpoint。Apple 的 `CFNetworkCopyProxiesForURL` 也明确以 URL 查询代理列表。因此,ACL 接入的系统形状应是“系统 SOCKS endpoint + sslocal 内部路由”,不是把 ACL 文件交给 macOS 当 PAC。
当前 PAC 生成的返回链还包含 `SOCKS5`、`SOCKS` 和 `DIRECT`:当 PAC 客户端无法使用前两个代理时,客户端可能继续尝试 DIRECT。ACL 则是在 sslocal 内部选定 bypass 或 proxy outbound;它不会从 ACL 规则中自动产生同一个按请求的 PAC fallback。因此“可达性/失败时是否允许直连”必须作为迁移决策单独定义。
来源:<https://developer.apple.com/documentation/cfnetwork/proxy-types>、<https://developer.apple.com/documentation/cfnetwork/global-proxy-settings-constants>。
| 维度 | 当前 NG2 PAC | sslocal ACL |
| --- | --- | --- |
| macOS 系统入口 | PAC 模式把 PAC URL 交给系统代理;`ProxyMode.pac` 生成 PAC target | ACL 不写 macOS SystemConfiguration;需要把系统代理设为 `127.0.0.1:<SOCKS>`,即 NG2 已有的 global SOCKS 投影 |
| 决策位置 | 应用请求 PAC 时按 URL 调用 `FindProxyForURL` | 请求进入 sslocal SOCKS/HTTP 后,按 target address 决定 outbound |
| 当前默认策略 | NG2 的用户规则实现只提取 `@@` 例外为 DIRECT,其余规则维持 SOCKS 优先 | `[proxy_all]` 默认 proxy,或 `[bypass_all]` 默认直连 |
| 域名 | 当前最小实现只保留可规范化的例外 host/suffix | 精确 `|`、子域 `||`、正则;域名未命中时还可能 DNS 后检查 IP |
| IP/CIDR | 当前 PAC 用户规则适配器没有把普通规则编译成 IP ACL | 原生支持 IPv4/IPv6 地址与 CIDR |
| 直连 | PAC 返回 DIRECT,应用自己直连;当前链还包含 proxy 失败后的 DIRECT fallback | sslocal 自己建立直连 outbound socket;没有同一个 PAC-style fallback |
| 代理 | PAC 返回 SOCKS endpoint | sslocal 选择 configured Shadowsocks server/balancer |
| 阻断 | 当前 NG2 PAC 生成器没有 BLOCK 分支 | local ACL 没有 block 动作; remote `[outbound_block_*]` 是 ssserver 语义 |
| URL 路径/query/HTTP 方法 | PAC 输入是 URL,理论上可按这些信息编写 JS;当前 NG2 规则适配器没有实现这些规则 | ACL 只处理 target host/IP,不能表达 URL path、query、HTTP method |
| 端口 | 当前 PAC 生成器没有端口策略 | ACL parser 的规则类型没有端口字段;不要把 `host:port` 当作已支持的 ACL 语法 |
| 进程/应用 | PAC 也不会天然知道进程;由应用是否遵守 PAC 决定 | ACL 也没有进程维度;未使用 SOCKS 的应用不会经过它 |
| UDP | PAC 本身是应用代理选择;NG2 当前 PAC/系统代理验证重点是 TCP | sslocal SOCKS UDP 在 `tcp_and_udp` 时也调用 ACL,但需真实测试 DNS/UDP 应用行为 |
| DNS | PAC 与应用/系统 resolver 的交互由客户端实现决定 | ACL 为 IP 规则判断可能调用 sslocal resolver;`local-dns` 是另外的 DNS relay 功能 |
本仓库证据:`PACRuleSet` 明确说只有 `@@` 例外变成 DIRECT,`ProxyMode` 只有 `.pac` 和 `.global`,其中 global 目标是本地 SOCKS;runtime document 当前也只把 PAC 放在 wrapper 自有的 `x_shadowsocksx_ng_pac` 区域。见 `ShadowsocksX-NG2/Domain/PACUserRules.swift:3-37`、`ShadowsocksX-NG2/Domain/ProxyMode.swift:3-80`、`ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift:37-78`。
当前 `gfwListURL` 已被设置模型持久化和校验,但 active source 中没有对应的下载/编译调用;当前运行时实际消费的是 `pacUserRules` 的最小 `@@` 例外适配。迁移研究不能把 GFW List 当成现成的 PAC 或 ACL 规则输入。见 [`ProxySettings.swift`](../../ShadowsocksX-NG2/Domain/ProxySettings.swift#L6-L19)、[`PACUserRules.swift`](../../ShadowsocksX-NG2/Domain/PACUserRules.swift#L3-L17)、[`SslocalRuntimeDocument.swift`](../../ShadowsocksX-NG2/Domain/SslocalRuntimeDocument.swift#L112-L127)。
还要处理 SystemConfiguration 的 `ExceptionsList`:当前 controller 把 `settings.proxyExceptionList` 传给 PAC 和 global 两种模式;切到 global SOCKS 后,这些目标会在 macOS 层直接绕过 sslocal,ACL 不会看到它们。迁移时应明确例外项由系统代理还是 ACL 作为唯一事实来源,不要让两套规则悄悄产生不同结果。见 [`ProxyRuntimeController.swift`](../../ShadowsocksX-NG2/App/ProxyRuntimeController.swift#L771-L780)、[`ProxyMode.swift`](../../ShadowsocksX-NG2/Domain/ProxyMode.swift#L63-L80)、[`SystemProxyPropertyList.swift`](../../ShadowsocksX-NG2/Domain/SystemProxyPropertyList.swift#L18-L41)。
上游 local ACL 的直连/代理实现见:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs#L155-L161>。远端 outbound block 的区别见:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/acl/mod.rs#L607-L625>。
## 4. 版本、协议与 macOS 集成接口
### 4.1 版本基线与 feature
本仓库当前 vendor manifest 已固定:
`ShadowsocksX-NG2/Vendor/sslocal/manifest.json:2-11`:官方 `shadowsocks-rust` v1.25.0、`aarch64-apple-darwin`、官方 release asset、SHA-256、bundle 内 `Helpers/sslocal`。
上游 v1.25.0 的 Cargo facts:
- package version `1.25.0`,Rust MSRV `1.91`;`sslocal` binary 需要 `local` feature。
- `local-http` 才提供 HTTP local;`local-dns` 才提供 DNS relay;`local-tun` 是 TUN 相关功能。
- 默认 `full` feature 包含 local、local-http、local-dns、local-tun 等;如果未来改为自编译精简 binary,必须显式保留实际使用的 feature。
- ACL 只影响本地路由,不改变 Shadowsocks wire protocol;服务器端仍必须支持配置的 method、密码与插件组合。
来源:<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/Cargo.toml#L1-L22>、<https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/Cargo.toml#L57-L97>、<https://github.com/shadowsocks/shadowsocks-rust/releases/tag/v1.25.0>。
### 4.2 建议的配置形状
如果使用全局 ACL,最小形状可以是:
```json
{
"acl": "/absolute/path/to/sslocal.acl",
"servers": [
{
"server": "example.invalid",
"server_port": 8388,
"password": "...",
"method": "aes-256-gcm"
}
],
"locals": [
{
"protocol": "socks",
"local_address": "127.0.0.1",
"local_port": 11086,
"mode": "tcp_and_udp"
}
]
}
```
若 SOCKS 与 HTTP local 的规则需要独立,则应给每个 `locals[]` 项设置同一个或不同的 `acl`;`locals[].acl` 会覆盖 global ACL。NG2 当前 `SslocalLocalDocument` 没有 `acl` 字段,因此接入前必须扩展 runtime contract 和生成/校验边界;这不是现有 PAC 扩展字段可以隐式解决的事情。
### 4.3 进程接口与重启策略
上游没有用于变更 ACL 的 JSON-RPC 或 control socket。对 NG2 可用的接口仍是:
- wrapper 以绝对路径启动 `sslocal -c <config>`;
- SIGTERM 做 graceful stop;
- SIGUSR1 仅用于上游实现的 server-list refresh;
- ACL 变更应由 wrapper 完整重启 sslocal,再等待 SOCKS/HTTP listener 健康后让 macOS system proxy 指向新实例。
当前 NG2 已有可复用的生命周期边界:`ShadowsocksX-NG2/Agent/main.swift:3-17` 说明 wrapper 管理 PAC endpoint 与 sslocal 生命周期,`main.swift:221-249` 负责停止 child;`ShadowsocksX-NG2/Tests/RealSslocalSmokeTests.swift:5-8,143-160` 已验证 bundled v1.25.0 的 local listener 与 SOCKS5 handshake。需要补充的是 ACL 真实 e2e,而不是把现有 handshake smoke 当成 ACL 证据。
## 5. 对 NG2 替代当前 PAC 的可行性判断
### 5.1 可行的目标定义
可行目标是:
> 将 macOS 系统代理设置为 NG2 的 local SOCKS;对所有确实把连接交给该 SOCKS 的 TCP/UDP 流量,由 sslocal ACL 决定直连或通过 Shadowsocks server。
这与当前 global 投影相符:`ShadowsocksX-NG2/Domain/ProxyMode.swift:63-80` 把 global 映射到 `127.0.0.1:<socksPort>`。因此实现上可以保留 wrapper/LaunchAgent/health supervision,新增 ACL 文件生成、runtime schema 字段、ACL 变化检测和 restart classification。
这里的“按配置”若是指“按域名规则选择直连或当前激活配置的 Shadowsocks server”,ACL 可以覆盖;若是指“不同规则选择 `servers[]` 中不同的服务器/目录配置”,ACL 不能覆盖。上游 ACL 只决定 bypass 或进入当前 balancer,NG2 当前则把活动目标展开成一个 `servers[]` 列表;规则到服务器的映射需要多实例、额外 dispatcher,或继续使用 PAC/其他路由层。见 [`ActivationStateMachine.swift`](../../ShadowsocksX-NG2/Domain/ActivationStateMachine.swift#L129-L138)、上游 [`AutoProxyClientStream`](https://github.com/shadowsocks/shadowsocks-rust/blob/v1.25.0/crates/shadowsocks-service/src/local/net/tcp/auto_proxy_stream.rs#L130-L161)。
### 5.2 不可直接承诺的目标
以下说法不能从 v1.25.0 ACL 得出,也不应在 UI/文档中暗示:
- “完全等价于 PAC”:不等价;决策输入从 URL 变成 target address。
- “所有应用都按配置代理”:不成立;应用必须使用系统 SOCKS,或显式连接 NG2 的 local。
- “ACL 可以阻断本机请求”:local ACL 没有这个动作;需要应用侧拒绝、网络过滤器或远端 `ssserver` outbound ACL。
- “修改规则立即生效”:不成立;v1.25.0 需要重启 sslocal 才能可靠换掉 context 中的 ACL。
- “把现有 GFW/Adblock 规则直接写入 ACL”:不安全;当前 NG2 只实现了极小的 `@@` 例外抽取,其他语法必须经过明确转换和校验。
- “按规则选择不同的 Shadowsocks server”:不成立;ACL 只选择 bypass 或当前 `servers[]` balancer,不提供规则到 server 的映射。
### 5.3 主要风险
1. **规则语义漂移**:现有 PAC 规则和 ACL 的 exact/suffix/regex/默认模式不同;尤其是 `@@` 例外、普通 Adblock pattern、IP 规则、尾点/IDNA、冲突规则。
2. **DNS 泄漏与决策不一致**:ACL 为 IP 规则可能进行 resolver 查询;应用把 hostname 交给 SOCKS、应用先自行解析、`local-dns` relay 和 fake DNS 又是不同路径。
3. **重启窗口**:ACL 更新期间若先切换 system proxy 或删除旧 ACL,可能出现 SOCKS 不可用、旧规则继续生效或 launchd 反复拉起。
4. **UDP/应用覆盖率**:`tcp_and_udp` 的源码路径存在,但系统 SOCKS、应用 UDP 使用方式和 DNS 行为仍需在真实 macOS 上验证。
5. **安全默认值**:`[proxy_all]` 与 `[bypass_all]` 的选错会产生全量直连或全量代理;ACL 文件路径和生成过程还涉及权限、原子替换及崩溃恢复。
6. **多 local 一致性**:如果 SOCKS 与 HTTP local 使用不同 ACL,用户看到的“按配置代理”会随入口而改变;如果使用全局 ACL,则应测试 local-specific override 不会被意外写入。
### 5.4 必须先验证的未知点
建议把下列项目列为实现前的验收矩阵:
1. `proxy_all` + `bypass_list`、`bypass_all` + `proxy_list` 的域名 exact/suffix/regex 命中和同名冲突。
2. IPv4/IPv6/CIDR、hostname 解析到多个 A/AAAA、解析失败、尾点域名与 IDNA。
3. hostname 目标与已解析 IP 目标是否得到产品预期的相同结果;分别记录 resolver 来源。
4. SOCKS TCP、SOCKS UDP、HTTP local 三条入口是否都使用预期 ACL;`local-dns` 是否按预期选择 local/remote DNS。
5. 修改 ACL 后不重启只发 SIGUSR1 的结果,确认旧 ACL 保持;再做 wrapper 原子替换 + 完整重启,确认新 ACL 生效且 system proxy 无错误窗口。
6. 真实 macOS SystemConfiguration 写入与恢复,以及 Safari、URLSession、浏览器、命令行、忽略系统代理的应用矩阵。现有 fake/unit 代理测试不能替代这项证据。
7. 仍使用官方 v1.25.0 arm64 binary 时的签名、bundle、Gatekeeper 和 release packaging;若升级上游版本,应重新核对 ACL 源码、配置 schema、reload 行为和 release notes。
## 6. 建议决策
短期不建议删除当前 PAC 方案。建议先做一个隔离的 ACL 运行时实验:
1. 仅在现有 `.global` system SOCKS 投影下接入生成的 ACL;
2. 将 ACL 路径、内容 hash、生成器版本写入 runtime snapshot,ACL 变化触发完整 sslocal restart;
3. 先支持一个受控的 `proxy_all`/`bypass_list` 子集,拒绝未知 Adblock 语法、非 ASCII、冲突 IP/域名规则;
4. 通过真实 SOCKS/HTTP/UDP/DNS 和 macOS 应用矩阵后,再决定是否把它作为产品级“按配置代理”模式;
5. PAC 与 ACL 若都保留,应明确其语义:PAC 适合需要系统 PAC 选择的场景,ACL 全局 SOCKS 适合把目标路由集中到 sslocal 的场景。
在当前证据边界内,最终判断是:**可实现,但应作为“ACL 驱动的全局 SOCKS 路由”重新定义和验证,不能作为当前 PAC 的无损替换。**