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

+70 -15
View File
@@ -28,7 +28,11 @@ import { userRoot } from '../fs/workspace.js'
import { isUserFsErrorCode, UserFsError } from '../fs/user-fs.js'
import { hashUid } from '../isolation.js'
import { LocalSpawner } from '../supervisor/orchestrator.js'
import { normalizeTunnelTarget, SshTunnel } from './tunnel.js'
import { normalizeTunnelTarget, SshTunnel, type WorkerTunnel } from './tunnel.js'
import { loadClientIdentity } from '../net/relay/identity.js'
import { listOverlayRelayCandidates } from '../net/relay/directory.js'
import { overlayEnvSeeds, overlayEnvTrustedKeys } from '../net/relay/directory.js'
import { RelayTunnel, isRelayUrl } from './relay-tunnel.js'
import type { Instance } from '../supervisor/spawner.js'
/** 绑定的头部名(Manager/agent 双方约定)。 */
@@ -153,10 +157,12 @@ export function buildWorkerAgent(
*
* S1:会合地址**优先取 config**(`DSHS_RENDEZVOUS_URL` → 兜底 `DSHS_TUNNEL_TARGET`,在
* `config.ts` 里单点解析);显式 `options.tunnelTarget` 仍是最优先(测试/嵌入用)。
*
* R4:**按 scheme 选传输实现** —— 裸 `user@host:port` / `ssh://` ⇒ `SshTunnel`(旧路,保留可秒回滚);
* `ws://` / `wss://` ⇒ `RelayTunnel`(自研 relay,去掉对 sshd 的长期依赖)。两者同实现
* `WorkerTunnel` 面 ⇒ 本段以下所有代码(自愈 / 对账 / `/healthz`)**与传输无关**。
*/
const tunnelTarget = normalizeTunnelTarget(
options.tunnelTarget ?? config.clusterRendezvousUrl ?? '',
)
const rendezvousRaw = (options.tunnelTarget ?? config.clusterRendezvousUrl ?? '').trim()
const staticPorts = [
options.port,
...(process.env.DSHS_TUNNEL_STATIC_PORTS ?? '')
@@ -164,18 +170,67 @@ export function buildWorkerAgent(
.map((v) => Number(v.trim()))
.filter((v) => Number.isInteger(v) && v > 0),
]
const tunnel =
tunnelTarget === ''
const tunnel: WorkerTunnel | undefined =
rendezvousRaw === ''
? undefined
: new SshTunnel({
target: tunnelTarget,
identity:
options.tunnelIdentity ??
process.env.DSHS_TUNNEL_IDENTITY ??
`${process.env.HOME ?? '/root'}/.ssh/tunnel_ed25519`,
controlPath: options.tunnelControlPath ?? `/tmp/dshs-tunnel-${options.hostId}.sock`,
staticPorts,
})
: isRelayUrl(rendezvousRaw)
? new RelayTunnel({
url: rendezvousRaw,
hostId: options.hostId,
// 密钥必须显式给:**缺了就抛** —— 静默降级到别的传输会让"以为切过去了"变成假象。
secret: (() => {
const secret = (process.env.DSHS_RELAY_SECRET ?? '').trim()
if (!/^[0-9a-fA-F]{64}$/.test(secret)) {
throw new Error('DSHS_RENDEZVOUS_URL 用了 relay(ws/wss)⇒ 必须同时给 DSHS_RELAY_SECRET(64 hex)')
}
return secret
})(),
staticPorts,
/**
* 序⑦ · **C2 装配点:中继失败切流**。
*
* ⛔ **不改会合面**:起始地址仍然是上面那个 `rendezvousRaw`(= `DSHS_RENDEZVOUS_URL`
* 的既有取值顺序,一行没动)。这里只补一件事 —— 起始那台**不健康**时,
* 从**同一份引导链**(`listOverlayRelayCandidates`)里换到下一条。
*
* 为什么候选链不走 `rendezvousRaw`:那是"当前连哪台"的答案,不是"还有哪些台"的清单;
* 而链是**已签名目录 / 内置种子**的产物 ⇒ ⛔ 不可能指向任意 url(引不出任意重定向)。
* 链里取不到候选 ⇒ 什么都不做(D6),**行为与改造前逐字一致**。
*/
failover: {
candidates: async () =>
(
await listOverlayRelayCandidates({
envUrl: '',
seeds: overlayEnvSeeds(),
trustedKeys: overlayEnvTrustedKeys(),
cacheFile: process.env.DSHS_OVERLAY_DIR_CACHE ?? '',
log: (line: string) => console.error(line),
})
).urls,
},
/**
* 序③:**本机节点身份**。配了 `DSHS_OVERLAY_NODE_KEY_FILE` + `..._NODE_GRANT_FILE`
* 才会带上(缺省 = 只做 HMAC,过渡期形态不变)。
* ⚠️ **配了却读不出来 ⇒ 起动即抛**(见 `identity.ts#loadClientIdentity`)——
* 静默退化成"没身份"会让"凭据坏了"表现成"一切正常",等 relay 一开强制就整台失联。
*/
identity: loadClientIdentity({
keyFile: config.overlayNodeKeyFile,
grantFile: config.overlayNodeGrantFile,
log: (line: string) => console.error(line),
}),
log: (line: string) => console.error(line),
})
: new SshTunnel({
target: normalizeTunnelTarget(rendezvousRaw),
identity:
options.tunnelIdentity ??
process.env.DSHS_TUNNEL_IDENTITY ??
`${process.env.HOME ?? '/root'}/.ssh/tunnel_ed25519`,
controlPath: options.tunnelControlPath ?? `/tmp/dshs-tunnel-${options.hostId}.sock`,
staticPorts,
})
let tunnelReady = tunnel === undefined
/**