Domain/ 根目录原有 62 个平铺源文件,无法从目录读出主题划分。 按「目录即主题」重组为 11 个目录(Rules/Custom 为嵌套子目录):Catalog / Subscription / Rules / Runtime / Activation / Settings / SystemProxy / Plugin / Credentials / LegacyImport / Diagnostics。纯移动:类型名、模块名 与 import 均未改动,Domain/ 根下不再有散落文件。目录口径取自 GLOSSARY.md 的章节划分(配置与订阅、代理规则、代理控制与网络、设置、 凭据与诊断),而非新造分类。 配套改动: - project.yml:wrapper 与 helper 两个 target 按文件引用的 15 条显式路径 随目录改名,并补注释固定这一约定——这 15 条指向不存在的文件时, xcodegen generate 以 "missing source directory" 直接失败(实测,不静默)。 - SkeletonTests 新增 DomainLayoutTests:断言 Domain/ 根下无平铺 .swift、 树下只有 .swift。后者防的是与 App/ 同源的坑——Domain/ 整目录是 app 与 Core 的 source path,任何非源码文件都会被 XcodeGen 收进 Resources 阶段 并封进签名后的 bundle。 - README 工程结构说明同步;docs/design 与 docs/research 属历史文档不改。 验证:xcodegen generate 后 app 164 / Core 163 源文件数与整理前一致;Agent 恰好 9 个、Helper 恰好 6 个 Domain 文件(无缺失无多出);app 与 Core 各覆盖 全部 62 个。clean build 0 error;task test 1024 用例全通过(整理前 1022, +2 为新增守卫);task format 未改写任何被移动文件(证明纯移动);task lint 与整理前同为 21 条内容型 warning;packaging-gate.sh ALL CHECKS PASSED。 布局守卫已做变异验证:临时投放松散 .swift 与非 .swift 文件时两个用例均 失败并点名违规文件,清理后复验为绿。
ShadowsocksX-NG2
ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配置放在本目录下。
依赖的二进制工具(sslocal、v2ray-plugin 等)不再以子模块方式在仓库内构建,而是直接复制外部项目发布好的可执行文件。HTTP 入站由 shadowsocks-rust 直接提供,2.0 不再携带 Legacy Privoxy。
旧版工程源码位于仓库根目录的 Legacy/ 下,仅供参考,不保证可构建。
工程结构
-
project.yml— XcodeGen 工程定义,唯一事实来源。.xcodeproj与App/Resources/Info.plist、*.entitlements、Tests/Info.plist都是生成物(已 gitignore),改工程结构一律改project.yml后重新生成。 -
App/— app target 源码(SwiftUI 菜单栏 app,LSUIElement)。目录即分层,层内按功能分组:Composition/— 组合根与进程生命周期:@main、依赖装配、应用形态与窗口激活策略。Application/— 应用层(无 SwiftUI 视图):*Workflow/工作流 module(UI-facing interface 与实现协作者)、ProxyRuntime/运行时控制器家族与事实投影、Services/LaunchAgent/登录项/helper/防火墙。Presentation/— 跨功能表现层:错误呈现、被多个界面面共用的状态模型。UI/— 视图层,按子视图分目录(Workspace/、Home/、Servers/、Subscriptions/、Rules/、Settings/、Diagnostics/、StatusMenu/,Servers/Plugin/为插件管理子视图)。PlatformEffects/— 平台效应 seam(协议 + InMemory 替身 + AppKit adapter),系统框架访问集中于此。Resources/—Info.plist、entitlements(生成物)与Assets.xcassets、*.xcstrings(入库)。
分层边界由
Tests/SkeletonTests.swift的正向圈定守卫断言:只扫描App/UI/,视图层不得引用运行时控制器、工作流实现协作者或原始目录/凭据存储类型;新增运行时适配器放进Application/即自动落在扫描范围外,无需登记豁免。注意App/下任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle,因此文档不放App/(project.yml已排除**/*.md)。 -
Agent/— 代理运行时 wrapper(独立可执行文件,装入Contents/MacOS/,由 LaunchAgent 常驻):读sslocal-active.json契约,以绝对路径启动和监管 sslocal(SIGTERM 链式停止、SIGUSR1 热重载/结构重启、崩溃时非零退出交 KeepAlive 重放,spec #21 D2/D5/D7)。PAC HTTP endpoint 已随 issue #67 移除。 -
LaunchAgent/—SMAppService.agent(plistName:)的注册清单,装入Contents/Library/LaunchAgents/;ProgramArguments用 bundle 相对路径,由 launchd 按注册 app 的 bundle 位置解析。 -
Domain/— 领域核心(配置目录树、凭据引用与持久化,spec #21 D3/D5),与 UI 无关。目录即主题,根下不留平铺文件,子目录口径取自GLOSSARY.md的章节划分:Catalog/— 配置目录树、结构不变量错误、目录文件存储、节点与凭据身份,以及ss://编解码SsUri。Subscription/— 订阅文档解析、抓取、订阅记录与刷新失败。Rules/— 代理模式、规则身份与匹配、内置规则快照、规则集合/浏览/分析、离线规则测试;Rules/Custom/为用户自定义规则的编辑草稿、校验与存储。Runtime/— 代理运行服务:运行时文档与监听设置、ACL 与部署收据、路径、原子写入、文件存储、日志与事件、端点/端口/socket 探针。Activation/— 激活候选校验、激活状态机与RuntimeConfiguration。Settings/— 用户设置的持久化:代理设置、监听设置、静默启动。SystemProxy/— 系统代理 typed 配置、property list 映射、计划器与特权 helper 的 XPC 契约/请求引擎。Plugin/— 托管插件、用户插件映射与插件安全校验。Credentials/、LegacyImport/、Diagnostics/— 凭据存储与写入日志、旧版导入、诊断报告。
跨进程契约按文件编入其它 target:
Runtime/的 9 个文件同时编入 wrapper(RuntimePaths、SslocalRuntimeDocument、RuntimeLog等),SystemProxy/全部 6 个文件编入特权 helper。project.yml里这 15 条是显式路径,在Domain/内移动文件必须同步,漏改会让xcodegen generate以缺文件直接失败。布局守卫(Tests/SkeletonTests.swift的DomainLayoutTests)断言根下无平铺.swift、树下只有.swift(非源码文件会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle)。 -
Tests/— 单元测试 target,随ShadowsocksX-NG2scheme 运行。 -
Vendor/<name>/manifest.json— 外部二进制的固定供应链清单(tag + 资产 URL + 归档 SHA-256 + bundle 内位置 + 签名 identifier);二进制本体与.fetched.sha256戳是构建缓存,不入库。 -
Vendor/rules/geolocation-cn/— 内置中国域名规则快照(issue #63):snapshot.json(规范化规则 + 元数据 + 损失报告)、manifest.json(上游版本与快照 SHA-256)、NOTICE(许可证与归属)。普通构建只读本地快照,绝不抓取或转换。 -
Vendor/rules/china-ipv4/— 内置中国 IPv4 CIDR 直连候选快照(issue #64):同上三件套,来源为 gaoyifan/china-operator-ipchina.txt固定 commit。规则模式的proxy_allACL 同时编入中国域名与 IPv4 CIDR 直连候选。 -
Vendor/rules/gfwlist/— 内置 GFWList 代理候选快照(issue #65):同上三件套,来源为 gfwlist/gfwlist 官方 Base64 AutoProxygfwlist.txt固定 commit。域名锚点转换为后缀匹配,满足条件的空路径或根路径 URL 前缀转换为精确域名匹配;「未匹配时直连」的bypass_allACL 编入这些代理候选。 -
Scripts/— 供应链脚本(见下节)与打包门槛断言;另有update-geolocation-cn.sh/update-china-ipv4.sh/update-gfwlist.sh(显式维护动作,抓取固定版上游并转换)与verify-rule-snapshots.sh(构建前离线校验快照完整性)。
外部二进制供应链
外部二进制只用上游官方构建产物:sslocal(shadowsocks-rust v1.25.0,装于 Contents/Helpers/)与 v2ray-plugin(v1.3.2,装于 Contents/Helpers/Plugins/)。信任根是提交在库里的清单哈希;构建链如下(spec #21 D6/D10):
- fetch —
Scripts/fetch-external-binaries.sh下载清单固定 URL 的归档、逐字节校验归档 SHA-256、只提取清单指名的成员并确认 arm64,落盘Vendor/<name>/并记录戳。归档哈希不符 → 立即失败;本地缓存漂移或清单变更 → 自动重新下载复验。 - generate —
xcodegen generate把两个二进制作为 Copy Files 项装入Contents/Helpers[/Plugins]。注意顺序:先 fetch 再 generate,文件在生成时必须已存在,否则 Copy Files 阶段会被静默丢弃(generate 也会直接报缺文件)。 - verify on every build — 预构建脚本阶段重跑 fetch 脚本(缓存命中时只对本地戳快速复验),哈希或清单漂移使构建即失败。
- sign —
Scripts/sign-embedded-helpers.sh(post-build 阶段)对每个二进制 Developer ID 重签:--options runtime --timestamp --identifier <清单 signIdentifier>(插件形如<bundle-id>.plugin.<name>);随后 Xcode 完成 app 外层签名,封存重签后的嵌套代码。 - gate — 构建产物必须过
Scripts/packaging-gate.sh <App.app>:嵌套代码签名严格校验、主应用 DevID + Hardened Runtime 且无 App Sandbox entitlement、逐二进制清单覆盖(Helpers 下每个 Mach-O 都在清单内且 DevID 重签通过)。篡改任一二进制会因签名失效被断言发现。
签名与运行形态基线
- bundle id
com.qiuyuzhou.ShadowsocksX-NG2,与 Legacy(com.qiuyuzhou.ShadowsocksX-NG)区分;运行时数据落~/Library/Application Support/ShadowsocksX-NG2/。macOS 15+ / arm64。 - Hardened Runtime + Developer ID 签名(
DEVELOPMENT_TEAM/ 证书见project.yml);不启用 App Sandbox,entitlements 为空 dict。 - 测试 target 用 Apple Development 证书签名:宿主 app 开启 Hardened Runtime 后 library validation 要求被注入的 xctest 包同 Team 签名。
- Release 配置关闭
CODE_SIGN_INJECT_BASE_ENTITLEMENTS,保证发布产物 entitlements 只来自项目文件。
开发、构建与测试
在本目录内,日常格式化、静态检查、工程生成、构建和测试优先使用 Taskfile.dist.yml 定义的任务:
task format
task lint
task lint:fix # 需要自动修复 SwiftLint 违规时使用;执行后再运行 task lint
task gen:prj # 生成 Xcode 工程
task build # 构建 Debug 版本 app
task test # 运行单元测试(不含真实进程冒烟)
task test:smoke # 运行真实进程冒烟测试(拉起真实 sslocal,见下节发布门槛)
task format 会格式化 App/、Tests/、Domain/ 和 Agent/ 下的 Swift 文件;task lint:fix 会修改源文件,执行后检查并复核 diff。
新 clone 或清单变更后,在本目录内先取外部二进制,再生成工程(顺序不可颠倒):
Scripts/fetch-external-binaries.sh
task gen:prj
Tests/Smoke/ 是真实进程冒烟(经 wrapper 拉起真实 sslocal 验证启动、握手、
ACL 路由与插件监管),不在默认 task test 内,由独立 scheme 按需运行。
发布前运行发布门槛检查:
# 发布门槛(必须全绿):真实进程冒烟 + 打包门槛
task test:smoke
Scripts/packaging-gate.sh \
.build/derivedData/Build/Products/Debug/ShadowsocksX-NG2.app
代码风格工具
swift format随 Xcode/Swift 工具链提供,无需单独安装(基线:Apple Swift 6.4);SwiftLint 需brew install swiftlint(基线 0.65.0)。工具缺失时,含本目录 Swift 文件的提交会被 pre-commit 钩子拒绝而不是放行。- 配置文件:
.swift-format是 Swift 6.4 工具链默认规则的快照(固定成文件,工具链升级不漂移);.swiftlint.yml默认规则起步,只声明排除项与个别和 format 基线冲突的关闭项(文件内有触发案例注释)。 - 仓库级
pre-commit钩子的启用方式和检查范围见根目录README.md。 - 调整规则只针对实际痛点:修改
.swift-format/.swiftlint.yml的 PR 须给出触发案例,并同步更新本节中的基线版本。
订阅资料
订阅可在 SIP-008 JSON 根级提供 bytes_used、bytes_remaining(非负整数,最大为 UInt64),以及自定义的 expires_at(服务到期时间)、traffic_reset_at(流量重置时间)。日期值使用 RFC 3339 字符串,省略或 null 表示未提供;无效资料仅忽略该项,不影响有效服务器或分组资料。
成功刷新完整替换白名单订阅资料,省略或无效的字段清除旧值;失败或取消保留最后成功资料与独立成功时间。资料和目录记录同文档原子保存,不另存原始响应或含密码的服务器明文快照;密码和插件参数沿用 Keychain 存储。旧目录文件缺少成功时间时不猜测。
订阅卡片显示返回的有效资料;仅在两项流量都有效且总量大于零时显示消耗进度。日期按用户当前语言与时区显示,到期和重置资料不改变代理启停或激活行为。
内置规则快照
规则模式使用的内置快照是显式维护产物:
- geolocation-cn(issue #63):人工复验上游后运行
Scripts/update-geolocation-cn.sh,从固定版 Loyalsoldier/domain-list-customgeosite.dat解析 typed 条目并生成Vendor/rules/geolocation-cn/snapshot.json。 - china-ipv4(issue #64):运行
Scripts/update-china-ipv4.sh,从固定版 gaoyifan/china-operator-ipchina.txt规范化并去重 IPv4 CIDR,生成Vendor/rules/china-ipv4/snapshot.json。 - gfwlist(issue #65):运行
Scripts/update-gfwlist.sh,从固定版 gfwlist/gfwlistgfwlist.txt解码官方 Base64 AutoProxy 列表并生成Vendor/rules/gfwlist/snapshot.json。||host/||host^转换为域名后缀匹配;|scheme://host(可含合法端口)在路径为空或仅为/,无查询、fragment 或用户信息,且 host 为无通配符的合法域名时,提取 host 转换为精确域名匹配,忽略协议和端口。仅含根路径/可转换,包含具体路径的规则继续跳过。@@保留直连动作。其他 URL 前缀、路径、通配符、正则、单标签前缀与 IP 字面量规则逐类计入损失报告。被代理规则完全覆盖的@@例外不写入无效 ACL 项(sslocal 域名匹配 proxy_list 优先于 bypass_list),保留代理规则,遮蔽数只计入absorbedCount;逐条取证输出到维护脚本 stdout。
快照使用 schema 2 / converter 2.0.0 的紧凑 JSON:每条规则只有 action 和 match,来源只在 metadata.source,转换报告只保留计数。被吸收的 .cn 条目与被遮蔽的 GFWList 例外不随快照分发,禁用宽规则后不再恢复;详见 ADR-0024。旧版本快照拒载。
Python 是唯一的上游转换实现;Swift 只加载、校验并消费快照,不另行解析上游输入。转换脚本的离线回归检查:PYTHONDONTWRITEBYTECODE=1 python3 Scripts/test-rule-snapshot-converters.py。默认 task test 也会运行两组 Python 检查;可单独运行 task test:rules。另运行 PYTHONDONTWRITEBYTECODE=1 python3 Scripts/test-rule-conversion-behavior.py,验证转换行为和单测 bundle 使用的固定快照夹具。审查转换变化后,可通过同一命令加 --write-fixtures 重建夹具。夹具时间固定,正常测试逐字节检查生成结果。快照完整性检查:Scripts/verify-rule-snapshots.sh。
更新失败保留上一份有效快照。geolocation-cn 转换会因未知语法/损坏输入失败;china-ipv4 转换还会因异常格式(拒绝率过高)、全部失效或相对上一份快照的异常规模变化失败;gfwlist 转换因未知语法、损坏 Base64、异常规模或相对上一份快照的异常规模变化失败。普通构建由 Scripts/verify-rule-snapshots.sh 离线校验快照存在、摘要匹配且 schema/转换器版本一致,缺失或损坏即构建失败。分发物必须携带各自的 NOTICE(许可证与归属)。
规则模式「未匹配时代理」生成的 proxy_all ACL 同时包含中国域名与中国 IPv4 CIDR 直连候选,固定本地绕过优先。规则模式「未匹配时直连」生成的 bypass_all ACL 只编入 GFWList 可生效代理候选;两种默认动作不把全部内置来源无条件并集。未匹配目标与无可用规则命中的 IP 字面目标直连。CIDR 判定可能触发本地 DNS 查询:sslocal 为未命中域名做 IP 匹配时可能发起本地 DNS 查询,产品不承诺 DNS 查询均经远端 Shadowsocks 服务器。
依赖升级
升级任一外部二进制是显式供应链动作:人工复验新 release(下载归档、静态观测哈希/架构/签名,不执行),随后在一次 PR 中同时改 Vendor/<name>/manifest.json 的 release/asset/url/archiveSHA256 与对应测试锚点。不接受 latest,不在仓库内构建,不做应用内更新通道(升级仅随 app 发版)。
用户插件目录
PluginCatalog 合成当前托管清单与本机 plugins.json 用户映射,名称精确区分大小写。用户映射优先,失效 override 不退回托管程序;删除 override 恢复当前 app 默认值。文件读取错误保留阻断事实,不按空映射处理。用户路径不进入服务器资料、订阅或分享文件,也不进入诊断报告。
设置页底部的“插件”区域统一展示内置与用户插件,支持新增、编辑路径、覆盖内置、删除与恢复内置。编辑名称只读;路径支持手动输入与文件选择。读取失败时隐藏条目并提供只读重试,管理 UI 不提供整体重置。映射提交与显式修复 interface 在组合根统一接入表单、激活与诊断。保存成功后刷新激活候选并异步收敛,运行失败不撤销保存。没有文件替换监控。
MacOSPluginInspection 只读取下载隔离、签名与系统评估事实,不启动用户插件、不修改系统策略。检查失败或不适用为未知;策略评估拒绝不等于实际执行禁令,也不阻止映射保存或激活。真实 macOS 拦截提示不由 hostless 替身测试证明。