# 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 章节: - 上游 ACL 源码的 section、mode 和 rule 注释: ### 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 语法。 来源: - 规则形式的上游注释: - 读取、ASCII 过滤、`||`/`|`、IP/CIDR 与正则分流: ### 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 命中与默认模式: - 域名目标的 DNS fallback 与直连/代理结果: - `local-dns` 的官方说明: ### 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: - UDP association: ## 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`;`locals[]` 例子也明确展示了 instance-specific ACL。`ServiceContext` 内保存的是 `Option>`,local server 创建时把解析好的 ACL 放进 context;因此运行期使用的是已加载的对象,不是每次请求重新读文件。 来源: - 顶层与 `locals[]` schema:、 - 官方多 local 配置示例: - ACL 加载失败被转成配置错误: - context 保存 ACL 以及 local-specific override:、、 ### 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 的错误/警告路径: - CLI `--acl` 的失败映射: ### 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: - reload task 只 reset servers: - `SIGUSR1` 监听: ## 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。因此“可达性/失败时是否允许直连”必须作为迁移决策单独定义。 来源:、。 | 维度 | 当前 NG2 PAC | sslocal ACL | | --- | --- | --- | | macOS 系统入口 | PAC 模式把 PAC URL 交给系统代理;`ProxyMode.pac` 生成 PAC target | ACL 不写 macOS SystemConfiguration;需要把系统代理设为 `127.0.0.1:`,即 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 的直连/代理实现见:。远端 outbound block 的区别见:。 ## 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、密码与插件组合。 来源:、、。 ### 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 `; - 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:`。因此实现上可以保留 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 的无损替换。**