Files
NexusGate/NEXT-ROUND-REPORT-V3.md

133 lines
17 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.
# amo v3 批次构建报告(NEXT-ROUND-NOTES 第 12/15/16/17/20/21/22/23 条)
- 基线:`aa2ee62`(开工时核对一致);本批 10 个 item-scoped 提交,HEAD 见文末清单。
- 施工口径:第 0 条 Go 单一真源——只改 `panel-go/`、`agent-go/`、共用前端 `public/`、安装脚本 `scripts/`;Node 实现零改动(`git diff --name-only aa2ee62..HEAD` 可核)。数据文件既有字段语义未改,新字段全部 `omitempty` 向后兼容。
- 验收环境:沙箱 linux/amd64(AMD EPYC 9D64)、Go 1.24.6、自编 sing-box v1.13.21 沙箱现成二进制。**未部署、未推 Gitea**;真机(路由器 A53)数字待部署后复测。
## 逐项结果
### 1. 第 12 条 核心版本显空修复 — ✅ 完成(`bb974e2`)
- `agent-go/health.go`:探针 `singBoxVersion()` 在二进制版本为空/`unknown` 时兜底读安装记录;记录文件 mtime 纳入探针缓存键,补写记录能被及时感知。新增 `installedSingBoxVersion()`:去空白与可选 `v` 前缀、正则校验版本号形状,垃圾内容忽略。
- `agent-go/main.go`:config 新增 `SingBoxVersionFile`,默认 `/etc/nexusgate/sing-box/version`,env `NG_SINGBOX_VERSION_FILE` 可覆盖。
- `scripts/agent-singbox.sh`:新增 `write_version_record()`(写去 `v` 前缀的 `1.13.21`),在已安装早退、预编译安装、源码编译安装三条成功路径全部调用。
- agent 版本号保持 `0.6.8`(面板 AnyTLS 版本门禁按数字解析,铁律未动)。
- 测试:新增 `TestSingBoxVersionFallsBackToInstallRecord`(unknown→读记录、带戳二进制优先、垃圾记录忽略、v 前缀剥离)通过;脚本 `bash -n` 通过。现网四台补写记录属部署动作,不在本批。
### 2. 第 15 条 凭据页签删绑定区块 — ✅ 完成(`8189a51`)
- `public/app.js` `renderCredentialsPane`:删除按绑定逐行列表(节点名/状态/协议/Reality 公钥+复制),只留订阅令牌+共用凭据三件套。后端 `customerCredentialsView` 载荷、keyStore、订阅、下发数据未动。
- 第 19 条已被第 21 条取代作废:未按旧口径往共用凭据区加「用户名」行(socks 用户名自此在节点上)。
### 3. 第 16 条 REALITY 目标扫描器 — ✅ 完成(`0ab31ae`)
- 后端 `panel-go/realityscan.go`+接口 `POST /api/reality/scan`(RequireAdmin 之后):入参 `{input}` 或 `{targets:[...]}`。
- 输入解析:留空→内置候选池 14 个(与目标预设同源的 tesla/amd 等+ microsoft/google/nvidia/samsung/intel/cloudflare/bing/github/yahoo/mozilla,均为 443);支持域名/IP/host:port/IPv4 CIDR(展开上限 32 主机);显式输入超 32 目标报 400。
- 探测:真实 TLS 握手,并发上限 8、单探针超时 4 秒;返回 target/host/port/ok/reason/tlsVersion/alpn/keyExchange/keyExchangeOk/certName/latencyMs,顺序与输入一致。密钥交换判定:Go 1.24 的 `tls.ConnectionState` 无 CurveID 字段,故改为限定组握手实测——依次只提供 X25519MLKEM768、只提供 X25519、组合兜底,任一成功即记支持。探测带 `InsecureSkipVerify`(只判可达性与密钥交换能力,证书 SAN 仍回报展示;理由已注释在代码内)。
- 前端:预设旁「查找目标」按钮;扫描器为独立叠加层(DOM 元素,不走 openModal,不冲未保存的节点表单状态),带主控视角说明文案(「从主控所在网络发起探测……节点所在网络可能不同」);每行「使用」回填 Reality SNI/目标端口、预设切 custom、关层。样式 `.scan-table` 在 `public/styles.css`。
- 测试:`realityscan_test.go` 通过(池/输入/CIDR 解析、本地自签 TLS 服务器 loopback 实测 OK/TLS 1.3/密钥交换非空、死端口给 reason);冒烟实测见下。
### 4. 第 17 条 节点页 Reality 公私钥 — ✅ 完成(`a9d9867`+测试修复 `4199a43`)
- 新只读接口 `GET /api/nodes/:id/reality-keys`:取 keyStore 中该节点所属**服务器**的托管密钥对;**绝不调用生成逻辑**,无条目返回 null;非 Reality 节点 400、节点不存在 404。
- 前端:节点编辑弹窗(Reality 协议时)加展示区,公钥/私钥各一行带复制;无密钥对显示「尚未生成(首次部署时由主控生成)」。只显示,无「换一对」。
- 测试:`realitykeys_test.go` 通过;冒烟实测接口返回完整公私钥对、非 Reality 节点 400。
### 5. 第 20+21 条 先建后绑+SOCKS5 固定账号 — ✅ 完成(`2fafd85`)
- 先建后绑:`orchestrator.go` 删除 `ValidateNodeDeploy` 的零客户 400 校验,全部协议一致放开(socks 例外按第 21 条连带修订取消);删除 `RebuildNode` 零客户自动移除分支与对账路径的零客户 skip——绑客户归零只重建、不下架。
- 固定账号:`types.go` Node 新增 `socksUser`/`socksPassword`(omitempty;老文件可读、老二进制读新文件忽略新字段不崩)。`protocols.go` `EnsureSocksCredentials()` 生成:用户名 `ngs_`+10 位 hex——**刻意避开 `ng:` 前缀**,因 agent 侧 sing-box 日志解析用 `^ng:(.+)$` 把 user 计数映射为客户级 usage 样本,固定账号带该前缀会被误记到某客户头上;密码为 base64 32 字节随机(与 RelayPassword 同级)。
- 入站 users 恒为固定账号一组,与绑定数无关(0/1/2 客户均有测试覆盖)。
- 订阅:`subscriptions.go` `NodeClientURI` 对所有绑定客户携带节点固定账密;链接只在客户已绑定时出现,绑定只控制出不出现。
- 编辑表单:socks 协议时显示两字段(可改、带复制,含未保存值复制);表单提交空值=保留旧值(与 exitCredentials 合并语义一致);用户名校验拒绝 `ng:` 开头与空白;协议切离 socks 时清空两字段。新建留空保存后自动生成。
- 存量迁移:`store.go` `MigrateSocksNodeCredentials()` 在 Store.Init 与既有凭据收敛迁移合并落盘,幂等只填空缺。
- 记账:固定账号流量在 agent 侧不匹配任何客户 user 样本,走 inbound 样本记节点级 MeteredTraffic;入口侧按客户 UUID 分账逻辑未动。
- 测试:新增 `panel-go/socksfixed_test.go` 全过;订阅金标准夹具的 socks 节点改存固定账密对(取旧派生值使全部格式产物逐字不变,见金标准节)。
### 6. 第 22 条 性能专项 — ✅ 代码完成,真机复测待部署(`05366d9`+`6d97d4e`+基准扩容 `0f74955`)
后端:
- `types.go` `Clone()` 改反射深拷贝(指针/接口/切片/映射/结构体递归、[]byte 逐字拷贝、未导出字段经浅拷贝保留);原 JSON 往返保留为 `cloneViaJSON()` 供等价对照。`clone_equivalence_test.go` 在真实种子库与合成数据(含 json.Number 嵌套、keyStore、Extra)上证明两种克隆序列化逐字节相等且深独立、nil 安全。
- `handlers.go` `CurrentAdmin` 改走 `store.View` 只读命中单条 User 记录,不再为找用户整库 Snapshot——每个 admin GET 的全量克隆由至少两次降为一次(handler 自身的一次 Snapshot 保留,未逐个改 View,以保同一请求内读一致性)。事务语义、数据文件格式、并发模型不变。
前端(`public/app.js`):
- 基础数据集页间缓存(servers/customers/nodes/protocols,TTL 30 秒);`api()` 内任何非 GET 成功统一失效缓存——全站写路径都过 `api()`,无漏点。
- `load()` 改渐进渲染:先以已有数据立即渲染当前页,各数据集到达后逐个应用并重渲染;overview/system/jobs 恒新鲜拉取;仅当本次全部请求皆失败才弹错误提示(保持旧失败面)。
量化证据(沙箱 amd64,合成种子 **20 服务器 / 40 客户 / 40 节点 / 160 绑定**,改动前后同种子同测法;旧版取 `aa2ee62` worktree 构建,基准文件同源移植):
| 指标 | 改动前 | 改动后 | 倍数 |
|---|---|---|---|
| Clone 一次 | 13.14 ms(JSON 往返) | 0.54 ms(反射) | ≈24× |
| GET /api/overview | 31.74 ms | 0.48 ms | ≈66× |
| GET /api/servers | 33.65 ms | 1.29 ms | ≈26× |
| GET /api/customers | 31.17 ms | 2.29 ms | ≈14× |
| GET /api/nodes | 31.31 ms | 5.08 ms | ≈6× |
| GET /api/jobs | 24.63 ms | 0.64 ms | ≈39× |
注:改动后 nodes 接口剩余耗时主要是 PublicNode/订阅 URI 等业务序列化,不再是克隆。以上是 amd64 沙箱数值;路由器真机(A53+整机 load 约 3)验收待部署后用 devtools 同数据量复测,预期回到几十 ms 级的结论以真机数字为准。
空闲稳态 CPU 对照(用户追加项;规模按最终口径 **20 台 agent**):
- 测法:同一种子(20 服务器/40 客户/40 节点,含 20 个带指纹的 agent 凭据),新旧二进制各起一份、无任何界面请求,驱动脚本按 agent-go 现网节奏模拟 20 台 agent——poll 每 8 秒、heartbeat 每 30 秒、usage 每 60 秒(空 samples),错峰启动;预热 15 秒后取 60.06 秒窗口,用 `/proc/<pid>/stat` 的 utime+stime 差折算 %CPU(100 ticks=1 秒 CPU)。两版驱动错误数均为 0。
- 结果:**改动前 38.41% → 改动后 32.31%**。**未达到个位数目标,如实报告。**
- 剩余成本主项(实测归因):agent 鉴权 `VerifySecret` 的 scrypt 校验。指纹快路径只负责定位 agent 记录,定位后仍须对 KeyHash 做一次 scrypt 验证(不可跳过,否则指纹即凭据);沙箱实测单次 VerifySecret ≈ 87.9 ms。稳态请求率 = poll 2.5/s+heartbeat 0.67/s+usage 0.33/s ≈ 3.5/s,scrypt 一项即 ≈ 3.5×87.9ms ≈ 30.7% CPU,与改动后 32.31% 几乎完全吻合——即克隆成本已被基本消除(旧版多出的约 6 个百分点即 JSON 全量克隆+usage 事务克隆落盘),稳态 CPU 现由**密码学校验**主导,与第 22 条的克隆问题是两个独立瓶颈。若后续要继续压稳态 CPU,方向是 agent 鉴权改造(如指纹命中后改用廉价 HMAC 校验或会话化令牌),属新立项,本批未动鉴权语义。
- 事务路径说明:`Transaction` 仍做一次全量克隆+落盘(写路径语义要求),稳态下仅 usage 每分钟 20 次触发,成本已被 scrypt 淹没,不是主项;60 秒 housekeeping 的事务+Snapshot 在两版 RSS 采样中均表现为分钟级瞬时尖峰。
RSS 对照(同种子、无界面请求):
- 空闲(带现网参数 `GOMEMLIMIT=28MiB GOGC=50`,启动 20 秒后连取 3 次):改动前 10,876 KB 稳定;改动后 10,672–10,840 KB。**无回涨**,满足第 22 条「RSS 不回涨」口径。
- 稳态负载下(未加 GOMEMLIMIT 的沙箱默认,60 秒窗口每 10 秒采样):改动前 47→197 MB 持续爬升(JSON 克隆分配压力),改动后多数样本约 46 MB、housekeeping 分钟边界出现一次约 150 MB 瞬时尖峰后回落。
### 7. 第 23 条 绑定变更合并下发(本批第 8 项)— ✅ 完成(`80140f3`)
- 窗口语义:**固定窗口 10 秒**——从该节点第一次待发变更起算,后续变更不延长(用户补充口径);窗口内多次绑定类变更只在到期时以最终状态下发一次 `apply_resource`。
- 实现:Node 新增落盘字段 `rebuildPending`/`rebuildPendingAt`/`rebuildPendingExit`(omitempty);`Orchestrator.FlushPendingRebuilds(now)` 由主进程 1 秒 ticker 调用。**待发状态落盘**(二选一中取此方案):面板在窗口内重启,首个 tick 即补发,不会卡死。
- 打标路径:assign/unassign、AlignCustomerNodes、Suspend/ResumeCustomerResources、凭据 regenerate(原逐节点立即重建改打标)。
- 保持即时并清标:手动 `POST /api/nodes/:id/deploy`(约束①)、RepairNode、节点 PATCH 编辑、对账路径;RemoveNode 清标(约束②)——flusher 对 PendingDelete/removing/draft 节点的标直接丢弃,绝不复活;flush 失败写 node.Error+审计 `rebuild_flush_failed` 并继续其他节点。
- 测试:`rebuild_coalesce_test.go` 五项全过——连加 5 客户窗口内 0 次 apply、到期只 flush 1 次且资源含齐 5 人;固定窗口不滑动;窗口内删节点 flush 0 次且无 apply;手动部署立即 1 次并消费待发标;Store 重载后标仍在且可 flush。
## 横向验收
- **全量测试**:`panel-go` 与 `agent-go` 各 `go test ./...` 全绿(含第 18 条 `listen_hotfix_test`、本批新增 socksfixed/rebuild_coalesce/realitykeys/realityscan/clone_equivalence/singboxversion 等)。
- **金标准**:`golden/golden.mjs verify` 仅有 1 处 diff——`lists.customers.body.customers[0].subscriptionClientCount 3 → 0`。已判定为**种子时间漂移、非本批回归**:用 `aa2ee62` 旧二进制复跑得到逐字相同的 diff(种子内订阅客户端的活跃时间窗已随墙钟过期,过期后恒为 0)。其余 31 个 section 逐字一致(含订阅全格式、部署任务、用量投影)。因此**未 recapture 基线**:该 diff 与本批行为变更无关,贸然 recapture 会把环境漂移写进基线。
- **真实 sing-box check**:冒烟中用改动后面板实际生成的两份节点资源(SOCKS5 固定账号节点+VLESS Reality 节点),按 agent-go `combinedSingBoxConfig` 同规则组装合并配置、keyMaterial 替换 `${REALITY_PRIVATE:...}` 占位后,跑沙箱自编 sing-box v1.13.21 `check` —— **通过**(入站:socks 听 0.0.0.0、users 恰 1 组;vless 听 0.0.0.0、Reality 私钥就位)。
- **沙箱真启冒烟**(新二进制、空库引导):登录成功;overview/servers/customers/nodes/jobs/system/protocols/session 全部 200;建服务器→令牌注册 agent 成功;零客户 socks 节点建单成功且固定账密自动生成(`ngs_` 前缀)、部署不被拦(202)、agent 侧 job 资源 inbounds users 恰固定一组;零客户 VLESS Reality 部署不被拦、资源完整;绑定客户后订阅 raw 链接为 `socks5://<固定用户名>:<固定密码>@…`(密码 URL 编码);reality-keys 接口对 Reality 节点返回完整公私钥对、非 Reality 节点 400;扫描接口对 www.microsoft.com 返回结构完整的结果行(沙箱直连出网被代理拦截,ok=false 且 reason 为握手失败——见缺口),对本地 TLS 1.3 服务器返回 ok=true、tlsVersion=TLS 1.3、keyExchange=X25519、keyExchangeOk=true、certName 与延迟齐全。冒烟全项 PASS。
## 发布包
`~/workspace/NexusGate-release/amo-v3/`(CGO_ENABLED=0、-trimpath;面板构建前经 build.sh 刷新 embed_public):
| 文件 | SHA256 |
|---|---|
| nexusgate-panel-linux-amd64 | `5698b7f5861078e8244c4d558d5191408b32590fd3b699446f1b564de9d6fcd9` |
| nexusgate-panel-linux-arm64 | `57e7b5cd43fd0582bb0aa917a36734483fac3f46d6e9d61ce141d46473a4ff4b` |
| nexusgate-agent-linux-amd64 | `b0aedfdcafa5cde7051f6cce6eb82eea709628f5e060688828ab8d107ed9df92` |
| nexusgate-agent-linux-arm64 | `e8173e6712769184e87efd6c796129c12f3daf0c0127bfc629d758f412d8afa6` |
同目录附 `SHA256SUMS` 清单。
## 未决缺口(如实)
1. **稳态 CPU 未达个位数**:32.31%(旧 38.41%),主因是 agent 鉴权 scrypt 而非克隆,详见第 22 条节内归因;需另立项改鉴权,本批未动。
2. **扫描器真实域名成功路径未在沙箱验到**:沙箱直连 TCP 出网被运行环境代理拦截(curl 走代理正常、直连 TLS 全部 WRONG_VERSION 类失败,python 直连复测同结果),故 www.microsoft.com 只验到结构完整的失败行;成功路径已由本地 TLS 1.3 服务器经面板接口实测通过,真实域名待部署后在路由器网络复测。
3. **真机数字待复测**:接口耗时、切页体感、RSS 均为沙箱 amd64 数值;路由器(arm64 A53)部署后需按第 22 条验收口径用 devtools 复测。
4. **取舍记录**:`PublicNode` 未剥离 socksUser/socksPassword(管理员接口内,编辑表单回显需要);扫描器探测用 InsecureSkipVerify(只判可达性/密钥交换,理由见第 16 条)。
5. **未部署、未推 Gitea**:`aa2ee62` 热修与本批提交均只在本地;现网四台 sing-box 版本记录补写、节点重新部署等部署动作不在本批范围。
## 提交清单(aa2ee62 之后)
| 提交 | 内容 |
|---|---|
| `bb974e2` | 第 12 条 agent 核心版本兜底 |
| `2fafd85` | 第 20+21 条 先建后绑+SOCKS5 固定账号 |
| `80140f3` | 第 23 条 绑定合并下发(固定窗口) |
| `8189a51` | 第 15 条 凭据页签删绑定区块 |
| `a9d9867`+`4199a43` | 第 17 条 节点页 Reality 公私钥(+测试辅助改名) |
| `0ab31ae` | 第 16 条 Reality 目标扫描器 |
| `05366d9`+`6d97d4e` | 第 22 条 后端克隆消除+前端缓存/渐进渲染 |
| `0f74955` | 第 22 条 基准种子扩容到 20 台规模 |
| (收尾提交) | NEXT-ROUND-NOTES.md 原文(未改一字)+本报告 |