Files
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

165 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 可达性(Reachability)—— **"怎么到这台 worker"的可序列化描述**(覆盖网络 S0)。
*
* ## 为什么需要它
* 现状里"怎么到一台 worker"被写死成两件事:`dsh_hosts.endpoint` 存一个 URL,
* `clusterInstanceHost` 存一个**全局**主机名。两者都**表达不出"经谁中转"**,而现网
* 恰好有两类语义完全不同的 host,字符串却同形:
*
* | hostId | endpoint | 真实语义 |
* |---|---|---|
* | `<host-a>` | `http://127.0.0.1:<worker-port-a>` | **直连本机**(Manager 与 worker 同机,node 直接监听) |
* | `<host-b>` | `http://127.0.0.1:<worker-port-b>` | **经 47 上 sshd 的反向隧道落点**(隧道的副作用) |
*
* ⇒ 换会合 / 中继组件时,表里**无法表达**「via(经哪个中继)+ 真实可达地址」,
* 只能改表;越晚改代价越大(会合中继拆分方案 §2 C3)。
*
* `Reachability` 把这件事显式化:`via` 指向一个 `Rendezvous` 实现,`address` 是真实地址。
*
* ## P0-3:为什么要带 `networkId`
* 地址是**网内**的:`127.0.0.1:<worker-port-b>` 在 `ops` 里指向 `<host-b>` 的 relay 落点,在 `u:5` 里
* 可能指另一台。只带 `hostId` 的重达性描述**在多网下是有歧义的**,而歧义会以
* "打到另一张网的同名节点"这种最贵的形态暴露(静默串网)。⇒ 本类型**必带**网络维度,
* `parseReachability()` 从**逻辑名**(`<network_id>/<hostId>`)里取它,调用方不许自己拼。
*
* ## 边界(勿破)
* 本模块**只做地址的表征与解析**,不承载任何权威状态 —— 归属 / 租约 / 骨干资格
* 一律仍只由控制面写(与 `集群化改造方案 §1.3` 数据分层一致)。
*
* @module dshs/net/reachability
*/
import { OPS_NETWORK, parseLogicalName } from './relay/network.js'
/** 同机直连:Manager 与 worker 在同一台机器上,不经任何中转。 */
export const VIA_LOCAL = 'local'
/** 今天唯一在跑的中转方式 = **Manager 主机上的 sshd 反向隧道**(S4 之后应被 relay 取代)。 */
export const VIA_MANAGER_SSH = 'manager-ssh'
/**
* 自研中继单元 `dshs-relay`(S4):worker **只拨出**、relay **只绑回环**、
* Manager 连 relay 分配的回环口 ⇒ 去掉对 sshd 的长期依赖(传输方案 §10 定案)。
*/
export const VIA_RELAY = 'relay'
export interface Reachability {
/** 哪台 worker(**裸 hostId**,不含网络段 —— 网络在下一行,两者分开表达)。 */
hostId: string
/**
* **属于哪张网**(P0-3 的结构性维度):`ops` | `u:<租户>` | 其它显式命名。
* 地址是**网内**语义,缺了它就等于"一张巨网 + 靠 ACL 兜"。
*/
networkId: string
/** **经谁可达** —— 一个 `Rendezvous` 实现的 id。 */
via: string
/** agent 的真实地址 `host:port`(**不含 scheme**)。 */
address: string
/** 传输层。 */
scheme: 'http' | 'https'
}
/** 只取址所需的最小形状 —— 避免本模块反向依赖 `supervisor`。 */
export interface HostAddressable {
hostId?: string
agentUrl?: string
reachability?: Reachability
/**
* **表里声明的会合形态**(`dsh_hosts.via` 原文,P0-3 加)。
*
* 为什么取址需要它:`via='relay'` 时 `agentUrl`(= `endpoint`)是 **relay 落点**而非直连地址,
* 解析不出时必须**失败关闭**而不是回落它(见 `agentBaseUrlOf`)。少了这一位,那种回落
* 在代码里**完全看不出来**(`agentUrl` 与真地址字符串同形)。
*/
via?: string
}
/**
* 把可达性拼成 agent 基址 —— **全仓唯一的拼接点**,别在别处再拼 `scheme://address`。
*/
export function agentBaseUrl(reach: Reachability): string {
return `${reach.scheme}://${reach.address}`.replace(/\/+$/, '')
}
/**
* 取一台 host 的 agent 基址:**可达性优先,回退旧 `agentUrl`**。
*
* S0 阶段的等价性:现网每个 host 都只有 `agentUrl`(`reachability` 全为 `undefined`)
* ⇒ 本函数返回的就是原先直接用的那个字符串,**行为零变化**。
*
* ⚠️ 两者皆缺时**抛错**,不返回空串 —— "静默打到空地址"是跨机下最难查的失败。
*
* ## ⛔ `via='relay'` 时**不许**回落到 `agentUrl`(P0-3)
* `agentUrl` 就是 `dsh_hosts.endpoint`。relay 语义下它存的是**落点**(`127.0.0.1:<动态口>`,
* 旧形态则是 Worker 侧口号),与 `reachability.address` **字符串同形、语义完全不同**。
* 解析失败(离线 / 跨网 / 快照陈旧)时回落它 ⇒ 请求被打到**控制面本机的同号端口**上,
* 而那个端口可能正被**另一张网的落点**或**某个实例**占着 —— 与 A1「假 404 / 静默写坏」同族。
* ⇒ 宁可**显式失败**(上层拿得到原因),也不发一个"看起来像地址"的东西出去。
*/
export function agentBaseUrlOf(host: HostAddressable): string {
if (host.reachability !== undefined) return agentBaseUrl(host.reachability)
if (host.via === VIA_RELAY) {
throw new Error(
`host "${host.hostId ?? '?'}" 声明 via=relay 但解析不出落点:拒绝回落到 endpoint ` +
'(relay 语义下 endpoint 是落点,回落会打到本机同号端口)',
)
}
if (host.agentUrl !== undefined && host.agentUrl !== '') return host.agentUrl.replace(/\/+$/, '')
throw new Error(`host "${host.hostId ?? '?'}" 既无 reachability 也无 agentUrl:拒绝静默降级`)
}
/**
* 从**逻辑名** + 旧的 `endpoint` 字符串解析出 `Reachability`(S2 迁移回填用 · P0-3 收口)。
*
* ## 为什么第一个参数是"逻辑名"而不是 `hostId`(P0-3 的唯一入口)
* 地址是**网内**语义。若这里只收裸 `hostId`,网络维度就得由**每个调用方**自己补 ——
* 而本线复盘里"散着拼字符串"最后必出三套不一致的口径。⇒ 收 `place` 的唯一入口改成收
* `<network_id>/<hostId>`:网络段由**本函数**切出来(`parseLogicalName`),调用方不碰。
* ⚠️ **裸 `hostId` 仍兼容**(⇒ 落 `ops`):过渡期不破坏现网调用方与既有单测。
*
* 强制面:`endpoint` 历史上是完整 URL(`http://127.0.0.1:<worker-port-b>`),也容忍裸
* `host:port` —— 没写 scheme 时按 `http` 处理,与 `RemoteSpawner` 原先"直接把它当
* fetch 基址"的行为一致(fetch 会补 `http://`)。
*/
export function parseReachability(
name: string,
endpoint: string,
via: string = VIA_MANAGER_SSH,
): Reachability {
const { network, hostId } = parseLogicalName(name)
const trimmed = endpoint.trim()
const matched = /^(https?):\/\/(.*)$/i.exec(trimmed)
if (matched !== null) {
return {
hostId,
networkId: network,
via,
address: matched[2].replace(/\/+$/, ''),
scheme: matched[1].toLowerCase() === 'https' ? 'https' : 'http',
}
}
return { hostId, networkId: network, via, address: trimmed.replace(/\/+$/, ''), scheme: 'http' }
}
/** `Reachability` → 旧 `endpoint` 字符串(与 `parseReachability` 互逆,回填/回滚用)。 */
export function toEndpoint(reach: Reachability): string {
return agentBaseUrl(reach)
}
/**
* 从 `host:port` 里取端口(覆盖网络 R3)。
*
* 为什么需要:relay 为每个注册端口在**它自己的回环**上开一条监听,回环口号由 `listen(0)`
* 动态分配 ⇒ Manager 拿不到、也推不出,只能拿「要拨的端口号」去 relay 的 `/status` 里查。
* 而那个号码就住在 `dsh_hosts.endpoint` 里(`http://127.0.0.1:<worker-port-b>`)⇒ 统一在这里剥出来,
* 别在调用方各写一遍 `split(':')`(IPv6 字面量会切错)。
*/
export function addressPort(address: string): number | undefined {
const idx = address.lastIndexOf(':')
if (idx < 0) return undefined
const raw = address.slice(idx + 1).trim()
if (!/^\d+$/.test(raw)) return undefined
const port = Number(raw)
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined
}