Skip to content

feat: SSH 隧道 — 主机密钥校验 + 跳板机(ProxyJump) + 逐跳连通性检测 + 健壮性#14

Merged
rrbe merged 13 commits into
masterfrom
feat/ssh-agent-auth
Jun 18, 2026
Merged

feat: SSH 隧道 — 主机密钥校验 + 跳板机(ProxyJump) + 逐跳连通性检测 + 健壮性#14
rrbe merged 13 commits into
masterfrom
feat/ssh-agent-auth

Conversation

@rrbe

@rrbe rrbe commented Jun 17, 2026

Copy link
Copy Markdown
Owner

背景

MongoDB 连接的 SSH 隧道之前停在「能连单节点」的雏形,存在一个安全 blocker(盲信任 host key)与若干缺口。本 PR 补齐到「安全可用」,并加上跳板机(ProxyJump)与可视化的逐跳连通性检测。

改了什么

主机密钥校验 / TOFU(修复安全 blocker)

  • 此前 ssh2 无 hostVerifier,盲信任意 host key,可被中间人。
  • evaluateHostKey:首连学习并固定指纹(TOFU),之后仅接受完全匹配,变更即拒。
  • 静默执行、无 UI;指纹(非密钥明文)持久化;每一跳独立校验。变更后只能删除并重建连接以重新信任。

跳板机 / ProxyJump

  • ssh2 嵌套连接支持「经跳板机连目标」(等价 ProxyJump / ssh -W):连堡垒机 → forwardOut 到目标 → 在该通道上再起一条 SSH 到目标 → 从目标 forwardOut 出 mongo。
  • v1 范围:单跳;跳板机仅私钥认证(类型层锁定,不存密码 —— 堡垒机应 key-secured);两跳指纹各自 TOFU 持久化。

逐跳连通性检测(Check connectivity)

  • 「SSH 主机」「跳板机」各一个按钮,只测自己那一跳(SSH 主机在配了跳板机时经跳板机测目标),不碰 MongoDB 端口,避免与 Test connection 重合。
  • 结果默认折叠,只显示总结 ✓/✗ + 展开图标;失败自动展开逐步日志。
  • 连接前对入口跳做 TCP 预检(tcpProbe,8s 快失败),把「网络不通」与「认证 / host-key 失败」清楚区分。

健壮性

  • 两个真 bug 修复:~ 路径展开(expandHome,否则 ENOENT)、并发隧道句柄竞态(删共享 pendingTunnel、改局部变量贯穿)。
  • SSH 表单前置校验 / 副本集+隧道告警 / 私钥读取失败友好报错 / 私钥文件选择器(dialog:openFile +「浏览…」)。
  • 隧道核心拆为 main/ssh/tunnelCore.ts(纯函数,有单测)+ tunnel.ts(ssh2 副作用)。

SSH 认证:主机支持密码 + 私钥;跳板机仅私钥。(ssh-agent 在迭代中评估后移除。)

测试

  • pnpm typecheck ✅(node + web)
  • pnpm test:unit369 passed
  • pnpm test:integration116 passed

SSH 握手 / host-key / 跳板机嵌套无法用 mongodb-memory-server 模拟,建议真机冒烟:

  • 直连隧道:首连成功 → 重连(直接过)→ 改指纹(拒连)。
  • 跳板机:配 bastion → target,确认两跳都连上、两跳指纹都被记下。

未包含(后续,见 TODO.md §3)

运行期 SSH 掉线上报、多跳跳板链、跳板机密码认证、私钥粘贴内容(textarea)、连接总超时 + 取消、隧道/驱动自动重连。

rrbe added 5 commits June 17, 2026 22:17
- SshAuthMethod 增 'agent',读取 SSH_AUTH_SOCK(win32 回退 OpenSSH 命名管道)
- 抽出 tunnelCore 纯函数(buildTunnelOptions/resolveSshAgentSock),
  收敛 connect()/test() 两处重复的隧道选项装配
- ConnectionForm 认证方式新增 SSH Agent(无需私钥/密码)+ 三套 i18n
- 新增 11 个 tunnelCore 单测
- expandHome 展开私钥路径的前导 ~ / ~/(Node fs 不展开,照表单
  占位符 ~/.ssh/id_ed25519 填会 ENOENT)
- openTunnel 改为返回隧道句柄、connect() 用局部变量贯穿,删除共享
  实例字段 pendingTunnel —— 并发连接不同 id 不再互相覆盖致隧道泄漏
- 新增 expandHome 单测
修复审计标记的安全 blocker:此前 ssh2 无 hostVerifier,盲信任意
host key,可被中间人。

- evaluateHostKey 纯函数:首连无 pin 则信任并学习指纹(TOFU),
  之后仅接受完全匹配,变更即拒连
- tunnel.ts 设 hostHash:'sha256' + hostVerifier,捕获不匹配并给出
  清晰错误(区分服务器重建 vs MITM);暴露 learnedHostKey
- SshConfig.pinnedHostKey(非密钥,明文存);connectionStore
  .recordSshHostKey 在首连成功后回写;test() 不回写
- ConnectionForm 保留 pin 不被编辑覆盖 + 提供重置入口(合法换机)
  + 三套 i18n
- 新增 evaluateHostKey / pin 透传单测
- ConnectionForm 启用 SSH 时前置校验 host/user 必填、端口 1–65535、
  私钥模式必填路径,未过则禁用保存并在底栏给出原因(不再把空值
  推迟到连接时才报错)
- 启用 SSH 且填了副本集名时给出告警:隧道下按单节点 directConnection
  连接、replicaSet 被忽略(对齐"缺失应报错绝不静默")
- tunnelCore.readPrivateKey:私钥读取失败给出指明路径的可读错误
- 三套 i18n + readPrivateKey 单测
复用现有 ssh2 实现支持"经跳板机连目标"(等价 ProxyJump /
ProxyCommand -W):连堡垒机 → forwardOut 到目标 SSH 端口 → 在该
通道上再起一条 SSH 到目标 → 从目标 forwardOut 出 mongo。每一跳
独立做 host-key TOFU 校验,复用 agent 认证。

- tunnelCore:TunnelOptions 重构为 { target, jump?, dest },buildHop
  统一装配;jump 仅 agent/私钥文件认证、不存任何密钥(带口令私钥
  走 ssh-agent)
- tunnel.ts:SshTunnel 支持可选 jump,connectHop 复用于两跳,
  learnedHostKey / learnedJumpHostKey 分别上报
- SshConfig.jump(SshHopConfig);connectionStore.recordSshJumpHostKey
  回写跳板机指纹;sessionManager 首连后两跳指纹都持久化
- ConnectionForm 新增跳板机区块(开关/host/port/user/认证 agent|key/
  host-key 重置)+ 校验 + 三套 i18n
- tunnelCore 单测覆盖 jump 装配(target+jump、jump 不带存储密钥)
@rrbe rrbe changed the title feat: SSH 隧道 — ssh-agent 认证 + 主机密钥校验 + 健壮性修复 feat: SSH 隧道 — ssh-agent + 主机密钥校验 + 跳板机(ProxyJump) + 健壮性 Jun 17, 2026
rrbe added 8 commits June 17, 2026 23:58
PR #14 之后尚未做的:运行期掉线上报(需新 IPC 推送通道,价值最高)、
多跳跳板链、跳板机密码认证、私钥粘贴/文件选择器、连接超时+取消、
自动重连。
- 连接前对入口跳板/SSH 主机做 TCP 可达性预检(tcpProbe,8s 快失败),
  把"网络不通"与"认证/host-key 失败"清楚区分开,且不必等 ssh2 的
  20s readyTimeout
- classifyConnError 纯函数:按 socket code / ssh2 level 把错误分为
  network / timeout / dns / auth / hostkey / other 并给可读提示
- 每跳报错标注是 jump host 还是 target(host:port),多跳失败一眼
  看出是哪一跳;跳板→目标 forwardOut 失败也单独标注
- 新增 diagnose.test(tcpProbe)+ classifyConnError 单测
面向终端用户:不再需要开终端 ssh-add,私钥+口令在界面内即可完成。

- 新增通用 dialog:openFile IPC(Electron 原生文件选择器,main 侧展开
  ~,接 ipc.ts/preload/registerIpc/useAppStore);私钥路径旁加“浏览…”
- 跳板机那一跳支持带口令私钥:新增第 4 个加密密钥 encJumpSshPassphrase
  (safeStorage),沿 StoredSecrets/sanitize/getDecrypted/saveConnection/
  ConnectionInput/DecryptedConnection/inputToDecrypted 对称扩展;表单加
  “跳板机私钥密码短语”框
- tunnelCore 给 jump 传 dec.jumpSshPassphrase(此前硬编码空对象)
- 软化 auth 报错:从“去终端 ssh-add”改为“在设置里改用私钥文件”
- 三套 i18n(browse/pickKey/jumpPassphrase,改写 jumpHint)+ 单测
- agent 降为可选快路(保留),私钥文件成为不依赖终端的稳路
把之前隐形的网络预检做成 GUI 可见功能:SSH 标签页加「检测连通性」
按钮,点击逐段显示 TCP→跳板机 / 登录跳板机 / 经跳板机到目标SSH端口 /
登录目标 / 到达MongoDB端口 的 ✓/✗ + 耗时 + 失败原因,哪一段断了一眼
可见(非技术用户也能自查)。

- tunnel.ts:diagnoseConnection 逐段执行、首个失败即停(其余 skip)、
  收尾销毁所有 client;connectHop 抽成模块级可复用(SshTunnel 与
  诊断共用)
- tunnelCore.planDiagnoseStages:纯函数规划阶段(可测,有无跳板两种)
- IPC connections:diagnose 全链路(ipc/preload/registerIpc/useAppStore)
- ConnectionForm:SSH 区「检测连通性」按钮 + 结果面板;三套 i18n
- 新增 planDiagnoseStages 单测
- 移除 ssh-agent 认证方式(主连接下拉 + 跳板机),跳板机仅私钥
- 私钥密码短语提示改为输入框 placeholder(已存密钥仍优先显示掩码)
- 移除主机密钥指纹展示与重置勾选;TOFU 校验机制保留(静默校验,
  首连学习、变更即拒),变更拒连报错改为提示删除并重建连接
- check connectivity 拆成「SSH 主机」「跳板机」两个按钮,各测自己那一跳
  (去掉到 MongoDB 端口那步,避免与 Test connection 重合);SSH 主机检测
  在配了跳板机时经跳板机测目标,否则直连
- 结果默认只显示总结 ✓/✗ + chevron,click 展开逐步日志(失败自动展开)
- 移除跳板机区块冗余说明文字(与上方 SSH 段不对称)
- diagnose IPC 增加 scope 参数(ssh|jump),贯穿 ipc/preload/registerIpc/store
- SshHopConfig.authMethod 收紧为 'privateKey',类型层杜绝跳板机密码登录
- TODO: 文件选择器已实现(误列为待办);补上逐跳连通性检测到已交付清单
@rrbe rrbe changed the title feat: SSH 隧道 — ssh-agent + 主机密钥校验 + 跳板机(ProxyJump) + 健壮性 feat: SSH 隧道 — 主机密钥校验 + 跳板机(ProxyJump) + 逐跳连通性检测 + 健壮性 Jun 18, 2026
@rrbe rrbe merged commit 6a08533 into master Jun 18, 2026
2 checks passed
@rrbe rrbe deleted the feat/ssh-agent-auth branch June 18, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant