feat(overlay): 覆盖网络线 序㉔–㊸ 补提交 —— 源码与已部署产物对齐

把这批「已 scp 到 47 / 106 生产并在跑、但一直未入 git」的实现补进版本库
(其中 P2 的全部新源码此前一直是 untracked)。

分棒内容:
- 序㉔ 内容分发块级寻址:src/net/relay/content/{runtime,source}.ts
- 序㉖ 骨干稳定选路(jitter):src/net/relay/{index,network}.ts、src/web/server.ts
- 序㉘ 组密钥加密(GCM 确定性):src/net/relay/content/{runtime,source}.ts
- 序㉛ P1 一键加入 + 分组准入:src/net/relay/{join,registry}.ts、
  src/web/routes/overlay-nodes.ts、scripts/overlay-node-{join,admit}.cjs、
  test/overlay-join.test.mjs
- 序㊵/㊶ 直连打洞 + peer 档:src/net/relay/direct/{candidate,index,punch}.ts、
  scripts/overlay-direct-probe.cjs、test/overlay-direct.test.mjs
- 序㊷/㊸ 观测面:scripts/overlay-probe.cjs、scripts/overlay-failover-drill.cjs

零回归三件套(2026-09-18 16:2x 提交前复跑,全绿):
- npm test                                  201 tests / pass 200 / fail 0 / skipped 1(Node v22.22.2)
- overlay-probe.cjs --table                 28 PASS / 0 SKIP / 0 FAIL
- overlay-failover-drill.cjs --scene all --table  12 PASS / 0 SKIP / 0 FAIL

⛔ 未纳入:_tmp_seq24/、_tmp_seq40/、_中间产物_待清理/(本机临时产物,仍 untracked)
🔴 未实施:D8 云安全组乙-1(待人工在云控制台落地,见
   交接单_覆盖网络直连与P2P_20260918.md §8.11)
This commit is contained in:
admin committed 2026-09-18 16:37:09 +08:00
1 parent 09ce76f3af
commit c452013129
18 files changed
+6604 -78

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
/**
* 覆盖网络 **管理面 API**(序㊵ · P2/S5 一并做 —— 该文件是 P1 的出口项)。
*
* ## 它回答两个问题
* | 端点 | 作用 | 鉴权 |
* |---|---|---|
* | `GET /api/admin/overlay-nodes` | **有哪些网 / 每个节点什么状态**(只读汇总) | `requireAdmin` |
* | `GET /api/admin/overlay-nodes/direct` | 本机**直连开关**现状 + **三段提示** | `requireAdmin` |
* | `POST /api/admin/overlay-nodes/direct` | **用户可设置**(口径①)—— 只改本机配置的 `direct` 字段 | `requireAdmin` |
*
* ## 🔴 三条纪律
* ① **全部走 `requireAdmin`** —— ⛔ 不做第二个无鉴权端点:既有的 `GET /dshs-overlay/bootstrap`
* 之所以能无鉴权,是因为它**只服务还没有凭据的新节点**且内容被收窄到"去哪儿";
* "**有哪些节点**"是**拓扑信息**,放公网 = 扩大暴露面(命中 **R5**)。
* ② **只写"跟着这台机器走"的那一项** —— 直连开关放**本机配置**(`<NODE_CONFIG_FILE>.direct`),
* ⛔ 不写共享 drop-in / env:那属于控制面或运维的通道(见 `direct/index.ts#resolveDirectSwitch`
* 的优先级口径:env > 本机配置 > 缺省)。
* ③ **失败具名** —— 文件不存在 / 形状坏 / 写不进去,各自一个原因码,⛔ 不吞、⛔ 不静默创建。
*
* @module dshs/web/routes/overlay-nodes
*/
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import type { FastifyPluginAsync } from 'fastify'
import {
DIRECT_ENV_KEY,
DEFAULT_DIRECT_ENABLED,
NODE_CONFIG_FILE_DEFAULT,
directFromNodeConfig,
directHintLines,
resolveDirectSwitch,
writeNodeConfigDirect,
} from '../../net/relay/direct/index.js'
import { loadRegistry, summarizeNetworks, listNodes } from '../../net/relay/registry.js'
import { requireAdmin } from '../middleware/authn.js'
/** 本机配置落点(`DSHS_OVERLAY_NODE_CONFIG` 可覆盖;缺省与 `join` 的 `--config` 一致)。 */
function nodeConfigFile(): string {
const v = process.env.DSHS_OVERLAY_NODE_CONFIG
return typeof v === 'string' && v.trim() !== '' ? v.trim() : NODE_CONFIG_FILE_DEFAULT
}
/** 注册表落点(`DSHS_OVERLAY_NODES_FILE` 可覆盖;缺省 = 参数表 `NODES_REGISTRY_FILE`)。 */
function registryFile(): string {
const v = process.env.DSHS_OVERLAY_NODES_FILE
return typeof v === 'string' && v.trim() !== '' ? v.trim() : '/var/lib/dshs/overlay/nodes.json'
}
const fss = {
exists: (p: string) => existsSync(p),
read: (p: string) => readFileSync(p, 'utf8'),
write: (p: string, text: string, mode: number) => writeFileSync(p, text, { mode }),
rename: (from: string, to: string) => renameSync(from, to),
}
/** 读本机配置里的 `direct`(**缺文件 ⇒ `undefined`**,形状坏 ⇒ 抛给调用方具名化)。 */
function readLocalDirect(): { file: string; present: boolean; direct?: boolean } {
const file = nodeConfigFile()
if (!existsSync(file)) return { file, present: false }
const raw: unknown = JSON.parse(readFileSync(file, 'utf8'))
return { file, present: true, direct: directFromNodeConfig(raw) }
}
export const overlayNodeRoutes: FastifyPluginAsync = async (app) => {
/** 只读汇总:有哪些网 / 每张网几个 approved、几个 pending / 节点清单。 */
app.get('/api/admin/overlay-nodes', { preHandler: requireAdmin }, async (_request, reply) => {
const file = registryFile()
if (!existsSync(file)) {
// "这套准入还没开始用"是**合法状态** ⇒ 200 + 明确的 `present:false`(⛔ 不是 500,也不是空 200)
return reply.send({ registry: { file, present: false }, networks: [], nodes: [] })
}
let reg
try {
reg = loadRegistry(file)
} catch (err) {
return reply.code(500).send({ error: 'registry-unreadable', detail: err instanceof Error ? err.message : String(err) })
}
return reply.send({
registry: { file, present: true, version: reg.version },
networks: summarizeNetworks(reg),
nodes: listNodes(reg).map((n) => ({
network: n.network,
hostId: n.hostId,
status: n.status,
group: n.group,
appliedAt: n.appliedAt,
approvedAt: n.approvedAt,
})),
})
})
/** 直连开关现状(含**三段提示** —— 用户口径③的"设置面显示"落点)。 */
app.get('/api/admin/overlay-nodes/direct', { preHandler: requireAdmin }, async (_request, reply) => {
let local: { file: string; present: boolean; direct?: boolean }
try {
local = readLocalDirect()
} catch (err) {
return reply.code(500).send({ error: 'node-config-bad', detail: err instanceof Error ? err.message : String(err) })
}
const state = resolveDirectSwitch(process.env, { localDirect: local.direct })
return reply.send({
envKey: DIRECT_ENV_KEY,
defaultEnabled: DEFAULT_DIRECT_ENABLED,
effective: state.enabled,
source: state.source,
raw: state.raw,
invalid: state.invalid,
nodeConfig: { file: local.file, present: local.present, direct: local.direct ?? null },
hint: directHintLines(),
})
})
/**
* 设置直连开关(**用户可设置**)。
*
* ⛔ 只动本机配置的 `direct`;⚠️ 若 env 里显式设了 `DSHS_OVERLAY_DIRECT`,它会**压过**本项
* ⇒ 回执里把这件事**明说**(否则用户会以为"改了没生效 = 平台坏了")。
*/
app.post('/api/admin/overlay-nodes/direct', { preHandler: requireAdmin }, async (request, reply) => {
const body = request.body as { enabled?: unknown } | undefined
if (body === undefined || typeof body.enabled !== 'boolean') {
return reply.code(400).send({ error: 'bad-body', detail: 'body 须为 {enabled: boolean}' })
}
const outcome = writeNodeConfigDirect(nodeConfigFile(), body.enabled, fss)
if (!outcome.ok) return reply.code(400).send({ error: outcome.reason, detail: outcome.detail })
const state = resolveDirectSwitch(process.env, { localDirect: outcome.direct })
return reply.send({
ok: true,
file: outcome.file,
direct: outcome.direct,
effective: state.enabled,
source: state.source,
envOverrides: state.source === 'env',
note:
state.source === 'env'
? `⚠️ 本机 env 里已显式设了 ${DIRECT_ENV_KEY}=${state.raw} ⇒ 它**压过**本次设置(env > 本机配置 > 缺省)`
: '已生效(重启节点服务后按本文件生效)',
})
})
}