Skip to content

Commit 5de559f

Browse files
committed
spec: Added WireGuard endpoint migration support and updated the configuration generation and import process.
1 parent 65cce2f commit 5de559f

6 files changed

Lines changed: 278 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-08-20
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
## Context
2+
3+
参见 `proposal.md` 的动机与范围。T4A 已具有 WireGuardBean、编辑 Activity、wg-quick 导入、legacy outbound options 以及 libcore 的官方 WireGuard endpoint 注册,但这些部分处于“Android 生成 outbound、Go 仅接受 endpoint”的断裂状态。目标内核版本由 `nb4a.properties` 固定为 sing-box v1.13.16。
4+
5+
husi 的现行方案提供了可移植的最小闭环:
6+
7+
1. WireGuardBean 以版本化 Kryo 格式持久化 `listenPort``persistentKeepaliveInterval`
8+
2. `buildSingBoxEndpointWireGuardBean` 生成一个 endpoint 和一个 peer;
9+
3. 所有节点先进入统一的 tag/链构建流程,再按协议类型从 `outbounds` 分区到顶层 `endpoints`
10+
4. JSON 导入器从 `endpoints` 反向构造 WireGuardBean;
11+
5. libcore 只在 endpoint registry 注册官方 WireGuard 实现。
12+
13+
T4A 与 husi 的配置对象技术不同(Java options + Gson hack map 对比 Kotlin serialization/map),因此移植应采用其结构与语义,而不是逐文件复制。实施期不能在本地构建 Go/Android;每一批次必须由 GitHub Actions 或真机结果闭环后再继续。
14+
15+
## Goals / Non-Goals
16+
17+
**Goals:**
18+
19+
- 在 T4A 现有配置构建架构内引入顶层 endpoint 表达,并只把 endpoint 类型从 outbound 集合中分离。
20+
- 使用 sing-box v1.13.16 官方 WireGuard endpoint/peer 字段契约,保持既有 UI 与数据库数据可用。
21+
- 覆盖节点转换、两类导入、配置拓扑和 libcore 加载的可验证路径。
22+
- 使 WireGuard 的 tag 在主节点、selector/urltest 与支持的链式场景中保持既有引用语义。
23+
24+
**Non-Goals:**
25+
26+
- 不实现自定义 WireGuard 协议栈、内核模式 WireGuard 或另一个 Go 依赖。
27+
- 不扩展为任意多 peer 的单一 Android 节点;沿用每个 T4A 节点对应一个 peer,wg-quick 多 peer 拆为多个节点。
28+
- 不在本变更中重构所有协议的 options 生成方式,也不移除 legacy stub 对用户自定义旧 JSON 的诊断。
29+
- 不借此变更提升 sing-box 版本、重做 WireGuard 编辑 UI 或解决 SSR/Snell 等其他已知降级。
30+
31+
## Decisions
32+
33+
### 1. 采用“统一构建后分区 endpoints”,而非为 WireGuard 建立旁路
34+
35+
在根 options 增加 `endpoints`,WireGuard builder 返回 endpoint options;配置构建器继续为所有节点分配 tag、建立 selector/urltest/chain 引用,完成后根据集中式 `isEndpoint(type)` 判定把 endpoint 从 `outbounds` 移到 `endpoints`
36+
37+
选择原因:这与 husi 的 `partitionEndpoints()` 相同,能最大限度复用 T4A 当前围绕 `currentOutbound` 的 tag、流量映射与链构建流程,未来也可承载其他官方 endpoint。分区必须发生在自定义配置 merge 之前或采用明确的 merge 规则,以免用户配置中的 endpoint 被覆盖。
38+
39+
备选方案是在遇到 WireGuard 时立即写入独立 endpoint 集合。该方案类型更严格,但会迫使链构建的多个分支同时理解两个集合,扩大首轮迁移范围并增加 tag 漏配风险,因此不采用。
40+
41+
### 2. 新增 endpoint options,保留 legacy outbound options 仅作兼容读取/诊断
42+
43+
在 T4A 的 `SingBoxOptions` 中按 v1.13.16 官方定义加入根 `endpoints`、Endpoint 基类、WireGuardEndpointOptions 与对应 peer 字段;WireGuard 格式 builder 改为返回 endpoint options。legacy Outbound_WireGuardOptions 暂不删除,以减少对旧 JSON 解析和诊断路径的无关扰动,但产品生成器不再调用它。
44+
45+
选择原因:libcore 已注册 `wireguard.RegisterEndpoint` 并显式用 stub 拒绝 legacy outbound;让 Android 输出与这一边界对齐即可,无需修改协议运行时。备选的“恢复/维护自定义 outbound”会逆上游 1.13 API、增加安全和维护成本,故拒绝。
46+
47+
### 3. 保持 WireGuardBean 的兼容序列化,按需增量补字段
48+
49+
先核对 T4A 现有 Bean 已存字段和 Kryo 版本。若已有 `listenPort``persistentKeepaliveInterval`,保持二进制布局不变;若缺少,则只追加字段、递增 Bean 自身序列化版本,并对旧版本读取零值。字段命名保持 T4A 现状,转换边界负责映射官方 snake_case JSON。
50+
51+
选择原因:数据库兼容是用户可观察契约。替换 Bean 或重排字段会让已有节点不可读。备选的数据表迁移没有必要,因为该项目已经使用 Bean 内版本化序列化。
52+
53+
### 4. Reserved 转换集中在 WireGuard 格式层
54+
55+
提供纯函数把恰好三个十进制字节的常见列表形式转换为 base64;其他字符串原样返回。builder 与导入器共用该边界,UI 继续接受用户熟悉的两种表示。
56+
57+
选择原因:这是 husi 已验证的兼容行为,也避免 UI、订阅解析器和配置生成器各自实现不一致转换。严格拒绝未知字符串可能破坏旧数据,故采用保守透传。
58+
59+
### 5. 导入职责按格式归位
60+
61+
将 wg-quick 文本解析收敛到 WireGuard 格式模块(RawUpdater 仅负责识别与调用),并扩展 sing-box JSON 导入入口,使其遍历根 `endpoints`,对 WireGuard 调用 endpoint parser。解析使用安全类型检查:无 peer 返回 null;首个 peer 映射为单节点;数字同时接受 JSON 数字和字符串表示。
62+
63+
选择原因:格式模块可直接单元测试并与 builder 保持双向字段一致。保留复杂解析在 RawUpdater 会继续形成无法独立测试的大类。多 peer endpoint 仍只读首个 peer,以匹配 T4A 单节点模型;wg-quick 多 peer 则维持拆分行为。
64+
65+
### 6. 以配置检查和真机握手分层验证
66+
67+
单元测试验证字段/省略/导入/分区;CI 生成至少单节点、selector/urltest、支持链式三类配置并调用项目已有的 libcore 配置检查或启动测试;真机最终验证实际 WireGuard 握手、DNS 与流量。敏感密钥只使用测试 fixture 或 CI secret,日志证据必须脱敏。
68+
69+
选择原因:JSON 结构正确不等于 Android VPN 生命周期中可联网,而直接依赖真机也难定位字段错误。分层门禁可以在每个最小批次快速失败。
70+
71+
## Risks / Trade-offs
72+
73+
- [T4A 的链算法假定所有节点均为 outbound,endpoint 的 detour/引用方向可能与 legacy outbound 不完全等价] → 第一批先锁定官方 v1.13.16 schema 和最小 builder;拓扑批次为每种支持位置保存生成 JSON,并由配置检查门禁确认后再进入真机。
74+
- [`type` 分区可能误把用户自定义对象移动到 endpoints]`isEndpoint` 使用明确白名单,仅包含目标版本确认的 endpoint 类型;未知类型不自动移动。
75+
- [自定义配置 merge 可能覆盖自动生成的 endpoints] → 明确 merge 顺序并添加同时存在用户 endpoints 与自动 WireGuard endpoint 的测试,按 tag 处理冲突或沿用项目现有覆盖规则。
76+
- [WireGuard 私钥、PSK 等可能进入日志或测试产物] → fixture 使用无生产价值的占位数据,日志与 CI artifact 不输出完整配置中的密钥。
77+
- [husi 当前分支可能面向不同 sing-box 提交] → husi 只用于架构参照;实施前以 `SINGBOX_VERSION=v1.13.16` 对应官方源码/JSON schema 校验字段和 registry API。
78+
- [保留 legacy options 会造成“仍支持 outbound”的误解] → 生成器无引用、测试断言 outbounds 不含 WireGuard,注释注明仅为旧输入诊断;后续可独立清理。
79+
80+
## Migration Plan
81+
82+
1. 固化 v1.13.16 官方 endpoint 契约与测试 fixture,加入 options/纯 builder/解析单元测试;GitHub Actions 编译并运行目标单测,通过后才继续。
83+
2. 接入根 endpoints 与构建后分区,先覆盖单节点输出;CI 执行配置序列化和 libcore 配置检查,确认不触发 legacy stub。
84+
3. 接入 selector/urltest 与支持的链式拓扑;对每类拓扑执行 CI 配置检查,失败则回滚该批配置构建改动而不继续累积。
85+
4. 收敛 wg-quick 与 endpoint JSON 导入并验证旧 Bean 读取;运行导入/数据库测试。
86+
5. 在测试 WireGuard 服务上进行真机连接:启动 VPN、完成握手、访问 IPv4/IPv6(服务具备时)、解析 DNS、切换网络并重连;保存脱敏日志和成功流量证据。
87+
88+
回滚时可恢复 Android 的 builder/分区改动;这会回到明确的“WireGuard 已知不可运行”状态,但不会破坏数据库,因为 Bean 的兼容读取布局不回退。若已追加 Bean 字段,旧代码通常可读取既有前缀;发布前仍需以旧版本读取新保存数据的测试确认降级行为。
89+
90+
## Open Questions
91+
92+
- T4A 现有 CI 中最适合执行最终 sing-box 配置检查的 workflow/job 名称需在实施时从 `.github/workflows` 选择;这不改变测试门禁内容。
93+
- 真机 WireGuard 测试端是否具备 IPv6 决定 IPv6 流量是强制证据还是记录为环境性跳过;endpoint 始终生成双栈 `allowed_ips`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## Why
2+
3+
T4A 仍把 WireGuard 节点生成成 sing-box 已在 1.13 移除的 legacy outbound;当前 libcore 只能返回弃用错误,因此已有的 WireGuard 配置无法实际启动。husi 已完成向 WireGuard endpoint 的迁移,可作为目标版本 sing-box v1.13.16 下恢复该能力的实现参照。
4+
5+
## What Changes
6+
7+
- 将 WireGuard 节点的 sing-box 配置输出从 `outbounds` 中的 legacy WireGuard outbound 迁移为顶层 `endpoints` 中的 WireGuard endpoint。
8+
- 保留 T4A 现有 WireGuard 数据模型、编辑界面与 wg-quick 配置导入能力,并补齐 endpoint 字段映射:本地地址、私钥、MTU、监听端口、对端地址/端口、公钥、预共享密钥、保活间隔及 reserved。
9+
- 支持导入 sing-box WireGuard endpoint JSON,并继续兼容已有数据库中的 WireGuardBean 数据。
10+
- 调整配置构建流程,使 WireGuard endpoint 可以参与单节点、链式代理和 selector/urltest 等既有引用关系,同时不再触发 legacy outbound stub。
11+
- 补充格式转换、导入与最终配置结构的自动化测试,以及 GitHub Actions 和真机场景的滚动验证门禁。
12+
- **BREAKING**:T4A 新生成的 WireGuard sing-box JSON 不再包含 legacy `type: wireguard` outbound,而使用 sing-box 1.13 endpoint 结构;这是对已被上游删除格式的有意替换。
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
18+
- `wireguard-endpoint`: 定义 T4A 对 WireGuard 节点的持久化兼容、配置导入、sing-box endpoint 生成、链路引用及可运行性要求。
19+
20+
### Modified Capabilities
21+
22+
- `libcore-integration`: 要求 Android 配置生成与 libcore 注册的 sing-box v1.13 WireGuard endpoint API 对齐,并禁止依赖已移除的 WireGuard outbound。
23+
24+
## Impact
25+
26+
- Android 格式与配置层:`app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/``ConfigBuilder.kt``SingBoxOptions.java`、sing-box JSON 导入器及 WireGuard 文本配置导入器。
27+
- Android 数据与 UI:复用现有 `WireGuardBean``WireGuardSettingsActivity` 与 preference 资源;如需补齐监听端口/保活字段,需保持 Kryo 版本向后读取兼容。
28+
- libcore:复核 endpoint registry 与 `with_wireguard` 构建标签;目标是使用已经注册的官方 endpoint,不新增自定义 WireGuard 实现。legacy outbound stub 可保留用于明确诊断旧配置。
29+
- 构建链:不改变 `nb4a.properties` 的 sing-box 单一版本来源;以 v1.13.16 官方 endpoint API 为目标,由 GitHub Actions 完成 Android/Go 构建验证。
30+
- 调研依据:husi 的 `WireGuardFmt.kt``ConfigBuilder.partitionEndpoints()``SingBoxOptions.Endpoint_WireGuardOptions`、WireGuard 格式测试和 `libcore/distro/registry.go`;外部 API 的最终契约以 sing-box v1.13.16 官方源码/文档为准,而不是把本地 husi 仓库作为依赖。
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: 已知官方内核降级必须显式保留
4+
5+
SSR/Snell 以及 Clash API selector 回调等尚未完成的兼容项 MUST 在主规范中保持可见,且 MUST NOT 被描述为已支持。WireGuard MUST 使用与 `SINGBOX_VERSION` 对应的官方 endpoint registry/API;Android 生成器 MUST NOT 依赖 sing-box 1.13 已移除的 WireGuard outbound。用于诊断用户自定义旧 JSON 的 legacy outbound stub MAY 保留,但 MUST NOT 被 T4A 自身生成的配置触发。
6+
7+
#### Scenario: 评估 WireGuard 节点
8+
9+
- **GIVEN** 当前官方内核支持 WireGuard endpoint 且不支持 WireGuard outbound
10+
- **WHEN** Android 为 WireGuard 节点生成配置
11+
- **THEN** 配置使用已注册的官方 WireGuard endpoint
12+
- **AND** T4A 自身生成的配置不会触发 legacy outbound stub
13+
14+
#### Scenario: 用户提供 legacy WireGuard JSON
15+
16+
- **GIVEN** 用户自定义配置仍包含 WireGuard outbound
17+
- **WHEN** libcore 加载该配置
18+
- **THEN** 系统返回明确的已移除格式诊断
19+
- **AND** 不将该旧格式描述为可运行能力
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
## Purpose
2+
3+
定义 T4A 在 sing-box 1.13 上存储、导入、生成并运行 WireGuard endpoint 的用户可观察行为,同时保护既有节点数据与链式路由语义。
4+
5+
## ADDED Requirements
6+
7+
### Requirement: WireGuard 节点生成 endpoint 配置
8+
9+
系统 SHALL 将内部 WireGuard 节点生成为顶层 `endpoints``type = "wireguard"` 的 endpoint,并 MUST NOT 为该节点生成 legacy WireGuard outbound。
10+
11+
#### Scenario: 生成单节点配置
12+
13+
- **GIVEN** 用户选择一个字段有效的 WireGuard 节点
14+
- **WHEN** T4A 生成 sing-box 运行配置
15+
- **THEN** 配置的 `endpoints` 包含带稳定 tag 的 WireGuard endpoint
16+
- **AND** `outbounds` 不包含同一节点的 `type = "wireguard"` legacy outbound
17+
18+
### Requirement: WireGuard 字段完整映射
19+
20+
系统 MUST 将节点本地地址列表、私钥、MTU 和正数监听端口映射到 endpoint,并 MUST 将服务器地址、服务器端口、公钥、非空预共享密钥、正数保活间隔和非空 reserved 映射到首个 peer。系统 SHALL 为该 peer 生成 IPv4 与 IPv6 全流量 `allowed_ips`;可选字段为空或非正数时 MUST 省略相应 JSON 字段。
21+
22+
#### Scenario: 映射完整节点
23+
24+
- **GIVEN** WireGuard 节点包含双栈本地地址、监听端口、密钥、MTU、服务器、保活和 reserved
25+
- **WHEN** 节点转换为 endpoint
26+
- **THEN** endpoint 和首个 peer 保留全部对应值
27+
- **AND** peer 的 `allowed_ips` 同时包含 `0.0.0.0/0``::/0`
28+
29+
#### Scenario: 省略可选字段
30+
31+
- **GIVEN** 节点的监听端口和保活间隔为零且预共享密钥与 reserved 为空
32+
- **WHEN** 节点转换为 endpoint
33+
- **THEN** 生成 JSON 不包含这些可选字段
34+
35+
### Requirement: Reserved 表示兼容
36+
37+
系统 SHALL 接受 base64 reserved 字符串,并 SHALL 将由三个十进制字节组成的列表表示(含逗号、换行、空格或方括号)转换为等价的单行 base64 字符串。无法识别为三字节列表的非空值 MUST 原样保留,以避免破坏已有配置。
38+
39+
#### Scenario: 转换三字节列表
40+
41+
- **GIVEN** 节点 reserved 为 `[0, 1, 2]`
42+
- **WHEN** 生成 WireGuard peer
43+
- **THEN** peer 的 reserved 为等价 base64 值 `AAEC`
44+
45+
#### Scenario: 保留已有 base64
46+
47+
- **GIVEN** 节点 reserved 已为 `AAEC`
48+
- **WHEN** 生成 WireGuard peer
49+
- **THEN** 该值保持不变
50+
51+
### Requirement: WireGuard 配置导入
52+
53+
系统 SHALL 支持导入 wg-quick 文本配置和 sing-box WireGuard endpoint JSON。wg-quick 中每个具有有效 Endpoint 与 PublicKey 的 peer MUST 形成独立节点并继承 Interface 地址、私钥、MTU 和监听端口;endpoint JSON 导入 MUST 读取首个 peer 及 endpoint 的 tag、地址、私钥、MTU 和监听端口。缺少 peer 的 endpoint JSON MUST NOT 产生 WireGuard 节点。
54+
55+
#### Scenario: 导入多 peer wg-quick 配置
56+
57+
- **GIVEN** 一个 Interface 下包含两个有效 Peer 且地址值含逗号与空格
58+
- **WHEN** 用户导入配置
59+
- **THEN** 系统生成两个 WireGuard 节点
60+
- **AND** 每个节点继承修剪后的 Interface 地址和对应 peer 字段
61+
62+
#### Scenario: 导入 endpoint JSON
63+
64+
- **GIVEN** sing-box JSON 含一个 WireGuard endpoint 和一个 peer
65+
- **WHEN** 订阅或 JSON 导入器解析配置
66+
- **THEN** 系统生成字段等价的 WireGuard 节点
67+
68+
#### Scenario: 拒绝无 peer endpoint
69+
70+
- **GIVEN** WireGuard endpoint JSON 未提供 peers 或 peers 为空
71+
- **WHEN** 导入器解析配置
72+
- **THEN** 该对象不产生节点
73+
- **AND** 导入流程不会因类型转换异常而崩溃
74+
75+
### Requirement: 既有 WireGuard 数据向后兼容
76+
77+
系统 MUST 继续反序列化升级前保存的 WireGuardBean,并 SHALL 对旧版本不存在的监听端口或保活字段使用零值。迁移 MUST NOT 要求用户删除并重新创建既有 WireGuard 节点。
78+
79+
#### Scenario: 打开旧数据库节点
80+
81+
- **GIVEN** 数据库包含迁移前序列化的 WireGuardBean
82+
- **WHEN** 新版本加载并再次保存节点
83+
- **THEN** 原有地址、密钥、MTU、服务器和 reserved 保持不变
84+
- **AND** 缺失的新字段得到兼容默认值
85+
86+
### Requirement: Endpoint 保持代理图引用语义
87+
88+
系统 MUST 保持 WireGuard endpoint 的 tag 可被主路由、selector、urltest 和链式代理引用。若 WireGuard 位于链中,系统 SHALL 使用 sing-box endpoint 支持的引用/绕行语义,且 MUST NOT 重新把它包装成已移除的 WireGuard outbound。
89+
90+
#### Scenario: WireGuard 作为主节点
91+
92+
- **GIVEN** WireGuard 是当前选中的主节点
93+
- **WHEN** 配置生成器建立最终路由目标
94+
- **THEN** 最终目标引用该 endpoint 的 tag
95+
- **AND** 配置可通过 sing-box 配置检查
96+
97+
#### Scenario: WireGuard 参与代理链
98+
99+
- **GIVEN** 一条支持的代理链包含 WireGuard 节点和另一个内部节点
100+
- **WHEN** 配置生成器建立链路关系
101+
- **THEN** 生成配置保持预期跳序与可解析 tag 引用
102+
- **AND** 不出现 legacy WireGuard outbound
103+

0 commit comments

Comments
 (0)