覆盖网络线 S0+S1 落地:可达性单一入口 + 会合地址出 env
S0(本机):新增 src/net/reachability.ts(Reachability 描述 + agentBaseUrlOf 唯一取值入口)、src/net/rendezvous.ts、docs/architecture.md(四层划分 + 依赖方向 + R1-R4 判据)、scripts/check-layering.mjs + layering-baseline.json、test/reachability.test.mjs;src/supervisor/remote-spawner.ts 与 src/web/server.ts 改为统一走 agentBaseUrlOf(),agentUrl 降级为可选旧字段(向后兼容)。 S1(本机 + 106):新增 DSHS_RENDEZVOUS_URL 出 env(src/config.ts 解析 clusterRendezvousUrl,优先级 overrides > 新变量 > DSHS_TUNNEL_TARGET 兜底 > 空),src/worker/tunnel.ts 新增 normalizeTunnelTarget()、src/worker/agent.ts 改读新配置 ⇒ 双路径并存、可零代码回滚(删掉新 env 即走旧路径)。106 已上线,健康检查 tunnel.ready=true。 package.json 增加 check:layering 脚本并接入 verify;README 登记分层文档。 验收:npm run build 通过;npm test 44/44;check:layering 无新增违规(基线 5 条)。
This commit is contained in:
1 parent
08219c99da
commit
640813e84e
13 files changed
+660
-18
No files matched your search
+7
-2
@@ -28,7 +28,7 @@ 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 { SshTunnel } from './tunnel.js'
|
||||
import { normalizeTunnelTarget, SshTunnel } from './tunnel.js'
|
||||
import type { Instance } from '../supervisor/spawner.js'
|
||||
|
||||
/** 绑定的头部名(Manager/agent 双方约定)。 */
|
||||
@@ -150,8 +150,13 @@ export function buildWorkerAgent(
|
||||
/**
|
||||
* 反向隧道(可选)。静态转发 = **agent 自身端口** + `DSHS_TUNNEL_STATIC_PORTS`(如控制面 PG);
|
||||
* 实例端口在 launch/stop 时动态加减,并在 `/healthz`(Manager 的心跳)里**对账自愈**。
|
||||
*
|
||||
* S1:会合地址**优先取 config**(`DSHS_RENDEZVOUS_URL` → 兜底 `DSHS_TUNNEL_TARGET`,在
|
||||
* `config.ts` 里单点解析);显式 `options.tunnelTarget` 仍是最优先(测试/嵌入用)。
|
||||
*/
|
||||
const tunnelTarget = options.tunnelTarget ?? process.env.DSHS_TUNNEL_TARGET ?? ''
|
||||
const tunnelTarget = normalizeTunnelTarget(
|
||||
options.tunnelTarget ?? config.clusterRendezvousUrl ?? '',
|
||||
)
|
||||
const staticPorts = [
|
||||
options.port,
|
||||
...(process.env.DSHS_TUNNEL_STATIC_PORTS ?? '')
|
||||
|
||||
+31
-4
@@ -13,7 +13,8 @@
|
||||
* 在**同一条长连接**上加/减转发,不必为每个端口重开连接。
|
||||
*
|
||||
* ⚠️ 定位:这是**演练级**传输(生产长期方案见设计 §2.3:受控网段白名单或隧道服务)。
|
||||
* ⚠️ 默认**关闭**:只有设了 `DSHS_TUNNEL_TARGET` 才启用 ⇒ 对同机/单机形态零影响。
|
||||
* ⚠️ 默认**关闭**:只有设了 `DSHS_RENDEZVOUS_URL` 才启用 ⇒ 对同机/单机形态零影响。
|
||||
* 旧变量 `DSHS_TUNNEL_TARGET` 作为**兜底**保留(新变量未设时才用它)。
|
||||
*
|
||||
* @module dshs/worker/tunnel
|
||||
*/
|
||||
@@ -23,8 +24,28 @@ import { promisify } from 'node:util'
|
||||
|
||||
const run = promisify(execFile)
|
||||
|
||||
/**
|
||||
* 归一化会合地址(覆盖网络 S1)。
|
||||
*
|
||||
* 为什么要它:会合点从"硬写在 env 里的 `user@host:port`"升格为**带 scheme 的 URL**
|
||||
* (`ssh://[email protected]:32022`)—— 以后换传输协议(中继/隧道服务)只改 scheme。
|
||||
* 而本类其余代码如下按 `user@host:port` 切分 ⇒ 必须在**入口处**剥掉 scheme:
|
||||
* 否则 `'ssh://root@h:32022'.split(':')` 会切成三截,把 `ssh` 当成主机名。
|
||||
*
|
||||
* 两种写法都接受(**单点归一,调用方不必判断**):
|
||||
* · `ssh://[email protected]:32022` → `[email protected]:32022`
|
||||
* · `[email protected]:32022` → 原样(兼容历史 env `DSHS_TUNNEL_TARGET`)
|
||||
*/
|
||||
export function normalizeTunnelTarget(raw: string): string {
|
||||
const trimmed = raw.trim()
|
||||
if (trimmed === '') return ''
|
||||
return trimmed
|
||||
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') // 剥 scheme(ssh:// / dshs+ssh:// …)
|
||||
.replace(/\/+$/, '') // 去掉可能的尾斜杠
|
||||
}
|
||||
|
||||
export interface TunnelOptions {
|
||||
/** 拨入目标,形如 `[email protected]:32022`。 */
|
||||
/** 拨入目标,形如 `[email protected]:32022`(带 `ssh://` 前缀也接受,见 {@link normalizeTunnelTarget})。 */
|
||||
target: string
|
||||
/** 私钥路径(建议专用、且在 Manager 侧用 `restrict,port-forwarding` 限权)。 */
|
||||
identity: string
|
||||
@@ -52,11 +73,17 @@ export class SshTunnel {
|
||||
constructor(options: TunnelOptions) {
|
||||
// `user@host:port` 里的 port 是 **SSH 端口**(不是转发的端口)—— 47 上用 32022,
|
||||
// 必须经 `-p` 传,否则会去连 22 而失败。
|
||||
const [hostPart, portPart] = options.target.split(':')
|
||||
// S1:先归一化(剥 `ssh://` scheme),再按 `user@host:port` 切分。
|
||||
const normalized = normalizeTunnelTarget(options.target)
|
||||
const [hostPart, portPart] = normalized.split(':')
|
||||
if (portPart !== undefined && !/^\d+$/.test(portPart.trim())) {
|
||||
// 宁可起不来也不要"静默连到 22 端口":地址写错必须吵。
|
||||
throw new Error(`非法的会合地址(端口必须是数字):${options.target}`)
|
||||
}
|
||||
this.hostPart = hostPart
|
||||
this.portPart = portPart === undefined ? undefined : Number(portPart)
|
||||
this.opts = {
|
||||
target: options.target,
|
||||
target: normalized,
|
||||
identity: options.identity,
|
||||
controlPath: options.controlPath,
|
||||
staticPorts: options.staticPorts ?? [],
|
||||
|
||||
Reference in new issue
Block a user