Files
proxy-station/docs/WIREGUARD-DEVICES.md
YANG JIANKUAN a5d980a5e9 feat: 添加 WireGuard 设备管理并完善客户端分流
实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。

完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
2026-11-02 22:44:23 +08:00

134 lines
18 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.

# WireGuard 设备管理:实现与生产验收
2026-11-02:已部署 LA 前后端及 njcq 网关 agent,两个原有 peer 已只读核对并确认纳管。**未切换本机 Surge,未对现有 peer 执行暂停、恢复或重置测试**。产品设计见 [DESIGN](design/wireguard-devices/DESIGN.md),历史设备网络验收见 [WIREGUARD-LAN](WIREGUARD-LAN.md)。本文件记录实现及后续运维边界。
2026-11-02 用户后续验收:已在 macOS、iPadOS、iOS 上全面验证 Surge + WireGuard 可工作。由于 Shadowrocket 的 Apple VNC 路径未通过,停止其 WG 下发兼容,仅保留普通代理能力。本轮清理已完成本地验证,尚未重新部署,详见文末。
## 身份、迁移和密钥
- `wg_networks` / `wg_gateways` 保存网段和现有网关;不初始化示例网关、设备、凭据。新建只支持 njcq `10.203.0.0/24`、MTU 1280;`.1` 为网关,`.2/.3` 为现有设备保留,新设备从 `.4` 开始。归档后地址不复用。
- `wg_devices` 独立保存名称、系统、公钥、固定地址、期望状态和应用版本。私钥及 PSK 随完整 profile 使用 AES-256-GCM 加密。没有客户端字段。
- 启动迁移仅纳管旧库 `sub_tokens.wireguard` 中实际存在的身份,保留 key、PSK、IP、MTU 及旧 URL 的入站设定(新派生 Surge 链接统一启用入站);将旧 token 映射到 `wg_device_id` 并清空旧 JSON。只创建只读 `adopt` 核对任务,不写远端。迁移事务幂等,冲突则失败,不替换已有身份。
- 加密主密钥默认在 `DATA_DIR/wg-master.key`(600),也可由启动环境提供 32 字节 Base64 的 `WG_MASTER_KEY`。已经存在加密身份时,丢失/错误密钥会报错,不生成替代身份。数据库新建目录 700、数据库 600。`secure_delete` 清理迁移后的数据库旧字段,但历史备份/WAL 仍按含真实密钥处理。
- **备份必须同时保全 SQLite 和主密钥**,主密钥建议另存受控离线备份。不能只恢复数据库,也不能在部署时重新生成/随意轮换主密钥。首次生产迁移前已按 update-db 技能在线备份,迁移后另备份加密数据库与主密钥,路径见下方生产记录。
- `/api/tokens` 列表不返回旧 profile;POST/PUT 不再接受 `wireguard` 写入。普通订阅编辑只改名称/节点范围,不改变旧设备兼容映射。新增设备与绑定均走独立 API。
## 订阅与导出
普通共享入口逐字保持 `/sub/:token/surge`,未附加时无 WG 节点、段落、私网规则或设备密钥。历史设备专属 token 是明确的兼容例外,仍沿用原 URL 下发其原设备身份。
管理员在 Surge 格式下选择设备,通过 `POST /api/tokens/:id/wg-bindings`(`{deviceId}`)签发 `/sub/:token/surge?wg=<32字节随机凭证>`。数据库只存 SHA-256 哈希、订阅 ID、设备 ID、用途和创建/访问/撤销时间;每次签发独立凭证,当前页面内复用同一组合,不在浏览器持久化。没有全局当前设备或共享 token 身份切换。
`wg` 校验订阅绑定、Surge 用途、撤销状态和设备可下发状态;裸 ID、空参数、篡改/跨订阅凭证、暂停/归档均拒绝,不降级纯代理。未知/重复 Surge 查询参数返回 400。托管 header 保留完整的、已经验证过的 `wg` 参数及请求 origin。切换设备/清除选择/切换格式不改变其他已生成 URL。新设备身份不附加到其他格式。
- 设备下拉保留不可选状态;采集未知不等于暂停,已生效的离线/未知设备仍可使用。
- 设备详情 `POST /api/wg/devices/:id/exports` 返回标准 `.conf`。二维码使用浏览器本地 `qrcode` 包生成,不调用在线服务;关闭导出框清理页面内配置。两种导出引用同一身份,不创建新 peer。
- `DELETE /api/tokens/:id/wg-bindings/:bindingId` 单独撤销链接;网页可撤销当前领取的链接并重新签发。撤销链接不等于撤销已导入 peer,停止网络接入必须暂停设备。
- 敏感管理/订阅响应 `private, no-store`,全站响应 `Referrer-Policy: no-referrer`,不记录配置正文。生产反代需让 `/wg-agent/` 到达 server,并关闭/脱敏 `/sub/` 的完整 URI 访问日志,避免路径 token 和查询凭证进入日志;不把凭证接入分析系统。
- WG 不加入公网动态/静态代理池;动态 regex 额外排除 WG 策略名。只增加自己的 `/32` DIRECT(入站)和组网 `/24` 规则,原节点范围、DNS、FFSB 和 FINAL 保持原流水线。
## 任务和状态
`wg_jobs` 是持久化的每设备版本任务,`wg_events` 保存操作记录,`wg_observations` 保存最近采样、基线和累计有效增量。创建请求使用 UUID `requestId`,重复请求返回同一设备/IP/任务。暂停/恢复/重置/移除互斥执行,未完成任务先重试;重试不更换身份或任务 ID。
网关确认 `runtime=true` 且 `persistent=true` 后才推进 `appliedVersion`。失败显示配置异常;连接丢失保留待确认,不报完成。暂停立即阻止后续私密下载,但只有真实撤销确认后才显示“已暂停”。移除确认后归档并撤销所有绑定;重置保留 IP、先删除旧 key 再应用新 key,既有绑定在确认后可刷新取得新身份,标准配置需重新导入。
旧 token 在只读纳管待确认阶段仍可按原身份下载,避免迁移直接中断自动更新;新绑定/新导出等网关确认后开放。旧身份纳管失败则拒绝下载,不改用其他身份。设备暂停与归档同样约束旧 URL。停用/删除一个订阅 token 仍只撤销该订阅下载,不自动暂停其关联设备。
agent 和页面默认每 15 秒更新。采集超过 45 秒显示未知并隐藏速率;相对握手年龄换算到中心接收时间,180 秒内握手/有效流量增长为近期在线,无握手为待接入。设备上行对应网关 RX、下行对应 TX。计数下降/agent 进程重启会重建基线,不输出负速率;采集间隔过长不显示平均值为实时速率。累计量为纳管后的有效增量,重启或漏采期间可能少计,不是账单计费。
## 网关 agent 接入与重建参考(本次已获授权并部署)
源码为 [agent.py](../scripts/wireguard/agent.py),[systemd unit](../scripts/wireguard/proxy-station-wg-agent.service)。第一版直接由单个受限 root 服务执行固定接口操作,不开放 shell/RPC,也不需要全站管理员 token。它只处理 `wg-njcq` 和上述私网,校验本机公钥与管理员登记值;不修改防火墙、路由、DNS、NAT 或重启接口,不读取 FFSB 身份。
本次按以下顺序部署,后续重建仍须先核对实际状态与授权:
1. 按生产维护约定备份应用库、网关配置及客户端交付记录,核对真实 Mac/iPad key/IP/PSK 与网关;不使用原型设备。
2. 部署应用后先检查迁移元数据。通过设备页“网关”登记实际 endpoint/publicKey,匹配已迁移的网关;签发一次性展示的独立 agent 凭证。已有配置下再次操作会轮换旧 agent 凭证,须同步 agent 配置。
3. 将脚本放到 `/opt/proxy-station-wg/agent.py`,root 所有且不可由应用用户修改;unit 安装到 systemd。写 root/600 的 `/etc/proxy-station-wg-agent.json`,字段如下,值均为占位:
```json
{
"url": "https://proxy-station.example.net",
"gatewayId": "<管理 API 返回的网关 ID>",
"agentToken": "<仅用于此网关的独立凭证>",
"publicKey": "<现有 wg-njcq 公钥>"
}
```
4. 确认网关已有 `wg` 工具与 Python 3;`wg-njcq.conf` 不得开启 `SaveConfig=true`,避免停机覆盖管理结果。服务限制只写 `/etc/wireguard` 和运行锁,另有文件锁避免多个 agent 并行运行。确认现有运维也不要同时改此接口。
5. 启动 agent。它主动 HTTPS GET `/wg-agent/:id/tasks`,POST `/wg-agent/:id/report`,认证只接受此网关 token。HTTP 重定向被拒绝,连接使用 TLS 验证,不继承环境代理。采集/任务响应 no-store,日志不含密钥、PSK、端点或请求 URL。
6. 首先完成 `adopt`:只读核对运行和磁盘中 publicKey、AllowedIPs、PSK,不改写 peer。核对失败保留同一任务,排查后在设备页重试。
7. 再以新测试设备验收新增/暂停/恢复/重置/移除。helper 每任务私有备份一次,锁内原子替换、fsync 持久文件,并用 `wg set` 只变更目标 peer;保留 Interface 指令和非受管 peer,拒绝地址/路由冲突。重试幂等,不用整接口重启或全量覆盖。失败可能已部分应用,界面持续显示异常,不能当作回滚成功。
持久任务未确认时不要直接删库或删除 peer 来“修好 UI”;修复原因并重试原任务。agent token 只允许该网关的任务/采样,不能访问管理页面、设备私钥或其他网关。
## 已完成的本地验收与剩余边界
隔离 DATA_DIR、占位身份,不读/写本机真实 `server/data`、私有 njcq 交付或生产 API:
- Node 22 下三包类型检查通过、服务端全量 91 项通过 / 1 项原有跳过、前端构建通过;Python agent 5 项离线测试及语法编译通过。新增测试覆盖生命周期、凭证、节点范围、迁移、主密钥丢失、遥测与公网动态池隔离。
- 实际本地 HTTP 默认/设备订阅验证:默认内容逐字不变,两个设备身份隔离,托管 header 包含完整凭证。Surge 官方 CLI 原配置和规则集快照解析通过。
- 无头 Chrome 真实页面:选择/清除/切换格式、旧设备链接稳定、独立二维码、暂停等待确认、创建任务及 320/360/736/1024 窄屏布局;包括没有 `crypto.randomUUID()` 的创建路径,检查无页面 JS 异常。
尚未验收:真实 Linux 内核上的新增/暂停/恢复/重置/移除生命周期,暂停后的实际不可达、长期运行/睡眠/换网。用户已随后确认三端 Surge + WireGuard 组网可工作;此处不将用户结论扩写成助手逐项执行了所有生命周期、版本与网络场景的测试。生产身份迁移、跨机 agent HTTPS 与只读纳管已通过,见下。握手/CLI 通过不能替代这些验收。
语法依据:[Surge WireGuard](https://manual.nssurge.com/policies/wireguard.html)、[托管配置](https://manual.nssurge.com/profile/managed-profile.html)、[WireGuard 工具](https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8)。
## 2026-11-02 生产部署记录
- 用户明确授权 LA 应用发布,并确认同时部署 njcq agent。发布为工作区版本 `f741aee` 加未提交改动;前端产物及最终后端源码指纹与本地一致,服务 active,公网健康检查成功,未带 token 的管理接口 401。
- API 部署前后:30 节点(12 启用)、14 组、277 规则、3 订阅;节点/组/规则/settings 完整数据一致,订阅 token/name/nodeIds/enabled 与创建时间一致。只迁移已有 iPad `.2`、Mac `.3` 的独立身份,不初始化原型设备。
- 两份旧 profile 与 njcq 公钥、客户端公钥、PSK、AllowedIPs 在迁移前核对一致。迁移后 peer 的磁盘配置 SHA-256 不变,运行中的公钥、PSK 和 AllowedIPs 不变;两项 `adopt` 任务均 confirmed。未重启 WG 接口,未修改防火墙、路由、Tailscale、FFSB 或本机活动 Surge。
- `proxy-station-wg-agent` 已安装并启用,固定接口 `wg-njcq`;agent 独立认证 HTTPS 采样正常。已有设备是否近期在线以握手/流量为准,安装服务与生成链接不产生在线状态。
- 管理 API 实际签发两个临时设备绑定,实际 HTTPS 验证各自私钥/IP 隔离、完整带 `wg` 的托管 URL、入站字段、独立导出、跨订阅/裸 ID 拒绝、先前链接保持稳定及撤销后 404。两条正式测试绑定及一次重试产生的临时绑定均已撤销,不遗留测试设备或 peer。
- 原默认 Surge 与 Clash 正文逐字不变。旧设备 Surge 仅增加排除私网 WG 的动态筛选:`^(?!NJCQ WireGuard LAN$).*?(?:原正则)`;保留原正则在名称中段搜索及显式锚点的语义,逐个核对原有 30 个节点匹配结果不变,其他内容与身份不变。部署差异检查发现并修复了最初额外限制原表达式从名称开头匹配的问题,相关回归测试已通过。
- aaPanel 原站点的 extension 中新增 `/sub/`、`/wg-agent/` 独立代理,关闭 URI 访问/错误日志与缓存,保留原 HTTPS 转发头;nginx 配置校验及平滑 reload 成功。新 `wg` 凭证未出现在站点最近访问日志中,敏感响应 no-store、no-referrer 验证通过。
- 实际 HTTPS 原/派生配置均经本机官方 Surge CLI 解析通过。标准脚本的本机直接规则集下载因 skk CDN 超时未完成;独立拉取取得本机 37 项和 LA 补齐 12 项,共 49/49 个 HTTP 200、非空且非 HTML 的完整新快照,原默认/iPad 派生/Mac 派生配置替换为这些快照后均经同一官方 CLI 通过。静态域名遮蔽提示 13 项,与用同一新快照计算的部署前默认配置完全一致(原 FFSB 手动集合与内部集合的既有覆盖:🦀 Claude 优先于 Claude-Surge-Internal);未据此改规则。该结果不代表真机入站或媒体业务已验收。
受控备份(远端文件 root/600,本机目录 700、文件 600;均不入 Git):
| 位置 | 作用 |
|---|---|
| `la:/opt/proxy-station/data/backups/before-wireguard-device-management-20261102-140453.db` | 迁移前在线快照,保留旧身份 |
| `la:/opt/proxy-station/data/backups/before-wireguard-device-management-deployed-20261102-141750.db` | 迁移/纳管及测试绑定撤销后的在线快照(已用主密钥在隔离 DATA_DIR 中验证恢复,两个完整 profile 与旧值逐字段一致,三个临时绑定均已撤销) |
| `la:/root/backups/proxy-station-before-wg-devices-20261102-140551/` | 原应用源码、产物及服务配置 |
| `la:/root/backups/proxy-station-wg-key-20261102-140857/` | 加密主密钥独立备份 |
| `la:/root/backups/before-proxy-station-wg-nginx-20261102-141101/` | 原站点与代理配置 |
| `njcq:/root/backups/before-proxy-station-wg-agent-20261102-140742/` | 原 WG 持久/运行配置与身份清单 |
| `edge-nodes/private/njcq-edge-node-docs/management-20261102/` | 本机私有主密钥、加密库、agent 配置与脱敏核对记录 |
## 2026-11-02 单一主订阅收拢
用户随后明确只保留一个订阅。通过管理 API 删除 `NJCQ WireGuard - iPad Pro`、`NJCQ WireGuard - Mac Studio` 两条旧设备专属订阅;当前只剩原 `Proxy Station` 主订阅,保留原 token、URL、节点范围及启用状态。旧设备订阅及其标准 WG/Shadowrocket WG 下载 URL 均返回 404,不再作为当前兼容入口,也不跳转到其他设备身份。通用的旧 token 兼容代码仍保留,用于迁移其他已有库;这两条生产记录按用户最新要求退休,不恢复。
独立设备不依附于旧 token 的生命周期。删除两条旧订阅未删除/暂停设备,也未提交任何网关任务。Mac `.3` 与 iPad `.2` 的设备 ID、公钥、私钥、PSK、IP、网关任务状态和独立 WG 配置保持不变;两台设备都可在唯一主订阅的 Surge 下拉框选择。设备导出不再使用已删除 token 的 URL,改用设备页授权导出。
验证:生产列表只有一条;主订阅实际 HTTPS 配置与变更前逐字一致、不含 WG,并经官方 Surge CLI 通过;两份独立导出与删除前逐字一致;节点/组/规则/settings 全量数据不变。两个设备分别通过主订阅临时签发绑定,核对完整托管 URL 与对应身份后已撤销验证凭证,不遗留新订阅卡片或测试 peer。本次没有修改代码、重新部署或切换本机 Surge;规则集/规则顺序没有变化,完整 49 项快照验收沿用上面的本次部署记录。
备份:
- `la:/opt/proxy-station/data/backups/before-single-main-subscription-20261102-144457.db`
- `la:/opt/proxy-station/data/backups/before-single-main-subscription-20261102-144457.wg-master.key`
两份备份均为 root 所有、600 权限。旧 URL 已停止更新,仍使用它的客户端需从主订阅选择本机设备并重新导入派生配置;本轮不自动修改已安装客户端的托管地址。
## 2026-11-02 停止 Shadowrocket WireGuard 下发(已部署)
用户已实际确认 macOS、iPadOS、iOS 的 Surge + WireGuard 可工作;不再继续兼容此前 Apple VNC 未通过的 Shadowrocket WG 路径。历史排查与失败证据保留,不将具体实现原因推断为已解决或已确认。
- Shadowrocket 普通节点与 conf 使用纯代理 IR,不再读取旧 token 对应的 WG profile;即使旧设备已暂停,普通代理下载仍可用。`wg` 查询参数不会向 Shadowrocket 附加设备身份。
- conf 序列化器不再生成 WG 私网路由、WG 额外节点导入提示或 WG 特定的动态组筛选;即使离线调用方附带 IR.wireguard,产物也与未附加时一致。通用 LAN DIRECT 规则照常保留。
- 移除 Shadowrocket WG YAML 生成函数及 DTO 的 `shadowrocketWireguard` URL。旧 `/sub/:token/wireguard-shadowrocket` 路径明确返回 404,不再下发身份,也不落到 SPA HTML。
- Surge 带设备配置、标准 `.conf`/二维码导出、设备身份与 njcq peer 管理保留;不修改单一主订阅或生产数据。此前导入 Shadowrocket App 的独立 WG 节点属于设备本地数据,更新普通订阅不会自动删除这些手工导入记录。
本地回归验证覆盖旧设备/暂停设备、附加查询参数、普通节点下发、conf 身份完全隔离和废弃端点 404。后续已与 FlClash 更新一同部署,HTTPS 旧 WG 端点返回 404,默认 Shadowrocket 订阅与部署前逐字一致;没有操作客户端。
## FlClash Android 研究(2026-11-02)
已核对官方 v0.9.2 及其固定内核:WG 出站支持,标准 Android 配置/实现未提供 Surge 式本机 peer-access,尚无 Android 真机验收;不能将出站、allow-lan 或普通代理端口当成双向 WG 本机入站。版本、源码依据和后续边界见 [FlClash 能力核对](FLCLASH-WIREGUARD-AUDIT-2026-11-02.md)。
## FlClash 可选设备身份(已部署)
订阅页的 FlClash 使用原 `/clash` 纯代理入口,可选设备后签发仅用于 Clash 的独立凭证;仅该派生链接包含设备 WG 出站和优先私网规则,动态公网组排除 WG。Android 可在设备页建立独立身份,不能与其他同时在线的设备共用密钥。FlClash 的本机服务入站及真实私网连接未在 Android 真机验收。原有 Surge 链接不受设备切换或 FlClash 凭证撤销影响,Shadowrocket 不恢复 WG 下发。配置审查与验证边界见 [FlClash 兼容性](FLCLASH-COMPATIBILITY-2026-11-02.md)。