Pure-stdlib Go implementation of the whole control plane: same nexusgate.json schema (unknown fields preserved verbatim, explicit null/false tri-states modeled), same HTTP API and error strings, embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt auth, X25519 Reality keys, panel-hosted keyStore and heartbeat reconciliation included. Version stays 0.6.8. Verified: Go tests 33/33 (scrypt + Reality vectors generated from the Node implementation, subscription goldens byte-identical), Node suite 52/52 untouched, differential E2E against the Node panel 50/50 including two-way DB swap. See panel-go/PARITY.md and panel-go/INSTALL-GO.md.
77 lines
5.1 KiB
Markdown
77 lines
5.1 KiB
Markdown
# NexusGate 面板 Go 移植 — 行为对齐说明(PARITY)
|
||
|
||
Go 版(本目录)是 Node 版(../server.js + ../lib/)的 1:1 移植:同一
|
||
`nexusgate.json`、同一 HTTP API、同一前端(public/ 零改动,embed 进二进制)。
|
||
版本号保持 `0.6.8`(agent 的 AnyTLS 版本门禁按数字解析面板下发的版本串,
|
||
且 overview/healthz 的 version 字段与 Node 一致)。
|
||
|
||
## 已验证等价(2026-10-20,沙箱内)
|
||
|
||
- **Go 单元测试 33/33**:含 scrypt 测试向量 6 组(由 Node 实现生成,
|
||
`testdata/scrypt_vectors.json`,逐字节一致)、Reality 密钥向量 4 组
|
||
(固定私钥→公钥推导与 Node JWK 导出一致)、资源构建黄金深比较、
|
||
订阅 8 种格式黄金**逐字节**一致、store 事务/原子写/未知字段透传。
|
||
- **Node 测试 52/52**:未改动任何 Node 文件,原样全绿。
|
||
- **差分 E2E 正式模式 50/50**(`difftest/run.mjs`):同一种子 DB(由 Node
|
||
面板自身业务流生成)分别起 Node/Go 面板,跑登录/CSRF、建服务器+注册
|
||
令牌、agent enroll→带 resources 心跳→对账重排→poll→complete、建节点+
|
||
客户+分配→订阅全格式逐字比对、用量上报、节点删除/服务器遗忘、备份
|
||
比对、错误用例;随后**交换 DB 文件**互启对方实现,列表/订阅/写入均
|
||
正常,keyStore/jobs/用量不丢。
|
||
- **三态字段已精确建模**(差分首轮抓到并修复):`nodes.autoRemoving`
|
||
(缺失 vs 显式 false)、`servers.engine`(缺失 vs 显式 null)、
|
||
`customers.suspendReason`(缺失 vs 显式 null)在 Go 版用
|
||
指针/presence 标志区分,加载、响应、写回均与 Node 一致。
|
||
- 未知字段(其他版本写入的键)在记录级与顶层均透传保留,Node 写的 DB
|
||
由 Go 加载再写回不丢字段(store 测试 + 互通步骤双重覆盖)。
|
||
|
||
## 已知微偏差(均为边缘路径,不影响线上业务流)
|
||
|
||
1. **Cookie 解析**:值含畸形 `%` 转义时 JS `decodeURIComponent` 抛错
|
||
(请求 500);Go 跳过该对。Go 的 PathUnescape 也不拒绝非法 UTF-8
|
||
字节序列(JS 会抛错)。
|
||
2. **VerifySecret 的 hex 解码**:JS `Buffer.from(hex,'hex')` 对非法
|
||
hex 是截断容忍,Go 严格解码失败即 false;最终判定结果一致。
|
||
3. **字符串截断口径**:CleanText、错误截断(1800/240/80)、access 的
|
||
128/200 上限,Go 按 rune、JS 按 UTF-16 码元;仅星平面字符(emoji
|
||
等)恰在边界时可能差 1 字符。新密码 10 位长度校验同此口径。
|
||
4. **settings 缺失判定**:Go 用值结构体,以「五字段全零且无 Extra」
|
||
近似 JS 的 `!data.settings`;真实写路径不会产生全零 settings,
|
||
不可达。
|
||
5. **订阅信息页日期**:Go 按存储值(UTC)以 `2006/01/02 15:04:05`
|
||
格式化;JS `toLocaleString('zh-CN')` 按服务器本地时区渲染且随
|
||
ICU 版本可能有差异(旧 ICU 带上午/下午标记)。百分比 `%.1f` 与
|
||
JS `toFixed(1)` 在 .x5 边界可能差 0.1。
|
||
6. **NG_DATA_FILE 默认值**:Go 取可执行文件同目录 `data/nexusgate.json`,
|
||
JS 取源码目录 `__dirname/data`;正式部署都显式设置该变量,无影响。
|
||
NG_PORT 不可解析时 Go 回退 8787,JS 为 NaN 监听失败。
|
||
7. **ReadJSON 边缘**:body 为 `null` 时 Go 视为空对象,JS 返回 null
|
||
后在个别 handler 会 500;非对象 JSON 体 Go 返回 400,JS 各 handler
|
||
自行退化。路由参数解码失败时 Go 保留原始段,JS 抛错 500。
|
||
8. **JSString 近似**:对象/数组入参 Go 返回 "",JS 为
|
||
`"[object Object]"`/逗号拼接;现有 handler 不可达。
|
||
OptionalISO 只接受 ParseISO 覆盖的格式,JS `Date` 还接受
|
||
`2026/10/20` 等写法(Go 返回 400)。
|
||
9. **DNS 失败文案**:resolveBindAddress 报错括号内 Go 用解析器简述
|
||
(如 `no such host`),JS 用 getaddrinfo 的 code(如 ENOTFOUND);
|
||
仅 DNS 解析失败路径的文案细节不同。
|
||
10. **keyMaterial 回填遍历顺序**:多 keyId 时 Go map 迭代序与 JS
|
||
对象序不同,audit 先后可能不同;各 key 相互独立,结果等价。
|
||
11. **JSON 键序**:Go 响应/落盘的键序为结构体字段序(合并未知字段
|
||
时为字母序),JS 为插入序;JSON 语义等价,双方互读无碍(互通
|
||
步骤已验证)。
|
||
12. **HTTP 超时**:Go ReadHeaderTimeout 15s + ReadTimeout 30s 近似
|
||
Node 的 headersTimeout/requestTimeout 语义。
|
||
13. **TLS 域名校验**:RE2 不支持 lookahead,总长度 253 改在代码中
|
||
校验,接受/拒绝结果一致。
|
||
14. **畸形请求建记录**:节点创建缺 name 时 Go 存 "",JS 该键缺失;
|
||
服务器 labels 缺省 Go 为 `[]`,JS 为 undefined;仅手工畸形请求
|
||
可见,前端正常请求不触发。
|
||
|
||
## 构建与产物
|
||
|
||
- `build.sh`:从 ../public 刷新 embed_public 后 CGO_ENABLED=0 构建。
|
||
- 交叉编译(发布):`CGO_ENABLED=0 GOOS=linux GOARCH=amd64|arm64 go build -trimpath -ldflags="-s -w"`,
|
||
产物 `nexusgate-panel-linux-{amd64,arm64}`,纯静态、无外部依赖。
|
||
- 运行时可用 `NG_PUBLIC_DIR` 指定外部静态目录覆盖 embed(调试用)。
|