feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库

覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)

验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
This commit is contained in:
admin committed 2026-09-17 17:03:27 +08:00
1 parent 640813e84e
commit 146c3d25ef
53 files changed
+16187 -63

No files matched your search

+18 -2
View File
@@ -168,6 +168,9 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
hosts: hosts.map((h) => ({
id: h.id,
endpoint: h.endpoint,
// 覆盖网络 S2:`via` 必须**可见** —— 否则"经谁可达"这件事运维查不出来,
// 加列就等于白加(迁移出问题时第一眼就要能看见)。
via: h.via,
capacityMb: h.capacityMb,
usedMb: h.usedMb,
status: h.status,
@@ -182,6 +185,8 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
const body = request.body as {
id?: string
endpoint?: string
/** 覆盖网络 S2:**经谁可达**(`local` / `manager-ssh` / 未来的 `relay:<id>`)。省略 = 列默认。 */
via?: string
token?: string
capacityMb?: number
}
@@ -191,13 +196,24 @@ export const adminRoutes: FastifyPluginAsync = async (app) => {
const host = await app.db.upsertDshHost({
id: body.id,
endpoint: body.endpoint,
via: body.via,
agentToken: body.token,
capacityMb: Number(body.capacityMb ?? 0),
})
await app.db.audit(request.user?.id ?? null, 'host.upsert', JSON.stringify({ id: host.id, endpoint: host.endpoint }))
await app.db.audit(
request.user?.id ?? null,
'host.upsert',
JSON.stringify({ id: host.id, endpoint: host.endpoint, via: host.via }),
)
return {
ok: true,
host: { id: host.id, endpoint: host.endpoint, capacityMb: host.capacityMb, status: host.status },
host: {
id: host.id,
endpoint: host.endpoint,
via: host.via,
capacityMb: host.capacityMb,
status: host.status,
},
}
})
+83
View File
@@ -0,0 +1,83 @@
/**
* 覆盖网络 P0-2:**引导目录端点** `GET /dshs-overlay/bootstrap`(**只读 + 无鉴权**)。
*
* ## 为什么必须无鉴权
*
* 它服务的对象正是「**还没有任何凭据**的新节点」(首次入网)⇒ 不能要求登录。
* 因此它的暴露面必须**小到可以不设防**,这份目录里只允许出现四类东西:
*
* | 字段 | 说明 |
* |---|---|
* | `version` / `issuedAt` / `refreshAfterSeconds` | 结构与缓存策略(**被签名覆盖**) |
* | `network` | 该目录所属的网(`ops`) |
* | `relays[]` | 中继端点(**只放公网 / 域名形态**,过滤回环与私网) |
* | `bootstrap[]` | **可轮换的引导地址清单** |
*
* ⛔ 目录里**不出现**:hostId、任何密钥、用户数据、内网地址、实例端口、版本号之外的内部标识。
* 连上之后**能拨谁**由 relay 侧的「网维度 + 白名单」(P0-1)决定 —— 目录只管"去哪儿"。
*
* ## 失败关闭
*
* 没有配签名私钥 / 私钥读不出来 / 签名抛错 ⇒ **`503 directory-unavailable`**,
* **绝不返回未签名目录**(否则"签名不对就拒绝"这条客户端判据会被一个半成品端点绕过)。
*
* 缓存策略 = `no-store`:目录**故意允许被轮换**,让 CDN / 浏览器留一份旧副本会把轮换能力拖死
* (客户端自己按 `refreshAfterSeconds` 缓存,不需要中间层再插一手)。
* @module dshs/web/routes/overlay
*/
import type { FastifyPluginAsync } from 'fastify'
import { readFileSync } from 'node:fs'
import type { ServerConfig } from '../../config.js'
import {
DIRECTORY_PATH,
buildDirectoryDocument,
publicRelayEntries,
signDirectory,
type OverlayDirectory,
} from '../../net/relay/directory.js'
/** 组装并签发目录;任一步不可用 ⇒ `undefined`(调用方回 503,**不发未签名目录**)。 */
function buildSignedDirectory(config: ServerConfig): { doc: OverlayDirectory; sig: string } | undefined {
const log = (line: string): void => {
process.stdout.write(`${line}\n`)
}
if (config.overlayDirKeyFile === '') {
log('[overlay-dir] ⛔ 未配 DSHS_OVERLAY_DIR_KEY ⇒ 目录端点返回 503(**不发未签名目录**)')
return undefined
}
let pem: string
try {
pem = readFileSync(config.overlayDirKeyFile, 'utf8')
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
log(`[overlay-dir] ⛔ 读不到签名私钥 ${config.overlayDirKeyFile}(${msg})⇒ 503`)
return undefined
}
// `relays[]` 优先用**显式配置的中继入口**,其次用种子(种子本身就是中继入口 ⇒ 语义自洽)。
const candidates = config.relayUrl !== '' ? [config.relayUrl, ...config.overlayBootstrapSeeds] : [...config.overlayBootstrapSeeds]
const relays = publicRelayEntries(candidates)
const bootstrap = publicRelayEntries(config.overlayBootstrapSeeds)
try {
const doc = buildDirectoryDocument({
relays,
bootstrap,
network: config.overlayNetworkId,
now: Date.now(),
})
return { doc, sig: signDirectory(doc, pem) }
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
log(`[overlay-dir] ⛔ 目录组装/签发失败(${msg})⇒ 503`)
return undefined
}
}
export const overlayRoutes: FastifyPluginAsync = async (app) => {
app.get(DIRECTORY_PATH, async (_request, reply) => {
reply.header('cache-control', 'no-store')
const built = buildSignedDirectory(app.config)
if (built === undefined) return reply.code(503).send({ error: 'directory-unavailable' })
return { ...built.doc, sig: built.sig }
})
}