把任意 dsh 插件的 loopback 看板,按需、限时、可撤销地暴露到公网——并且只做这一件事。
插件侧持有的只有一份目录(哪些看板存在、叫什么)和一个受限的调用边界。 真正的事实全部在 root 手里:
| 事实 | 谁说了算 |
|---|---|
| 哪些目标可达 | /etc/dsh-tunnel/targets/<name>.conf 是否存在且归 root |
| 最大暴露时长 | 描述文件的 ttl_seconds + systemd RuntimeMaxSec drop-in,双重强制 |
| 源站是哪个端口 | 描述文件的 origin_port,模型不可见 |
| 公网是否真的被 Access 保护 | 每次开启做匿名探测,未被重定向到登录域即立即停止 |
| 运行期是否仍然安全 | 看门狗每 10 秒复核连接器/入口/源站/边缘,漂移即关闭 |
即使插件配置被完全篡改,可达目标集合与最大暴露时长也不会变化——配置里写一个 未部署的目标,只会让开启请求被 helper 拒绝。
- id: tunnel
config:
targets:
- name: console
title: 运行状态看板
description: 实时指标、决策记录与运行日志
- name: metrics
title: 指标面板
description: 只读的聚合视图name 必须与生产机上 /etc/dsh-tunnel/targets/<name>.conf 同名。
看板归谁,登记就由谁做——这样标题和描述不会和实现漂移:
export const inject = ['tunnel']
export function apply(ctx: Context) {
const dispose = ctx.tunnel.register({
name: 'console',
title: '运行状态看板',
description: '实时指标、决策记录与运行日志',
})
ctx.on('dispose', dispose)
}模型拿到的是一个 tunnel_control 工具:
list—— 有哪些看板(本地读目录,不触发任何特权调用)status—— 每条隧道的状态与expires_atopen <target>—— 开一条限时隧道close <target>—— 关掉某一条,其余不受影响close-all—— 全关
典型对话:「打开状态看板」→ open console → 返回公网地址与到期时间;
「看完了,关掉」→ close console。
await ctx.tunnel.open('console')
await ctx.tunnel.status()
await ctx.tunnel.closeAll()# 1. 装特权层(可执行文件、模板单元、tmpfiles、sudoers)。不创建任何目标。
sudo ./deploy/install.sh
# 2. 逐个下发目标。凭据一律文件到文件,绝不经过命令行参数。
sudo ./deploy/provision-target.sh console \
./console.conf ./token ./uuid ./hostname ./aud描述文件的字段说明见 deploy/targets/example.conf。
sudoers 里把调用用户改成实际运行 dsh Host 的那个用户。
sudoers 只钉死 helper 路径与四个动词;目标名是可变参数,由 root 侧用
「字面形状 + 必须存在同名 root 持有描述文件」两道校验收敛。
npm ci
npm run check && npm test # 类型检查 + 28 项测试
npm run build
npm run lint:sh # 特权脚本语法检查测试不触网、不需要真实凭据:特权 helper 全部由注入的假执行器代替。