从零打造一款可运维的图形化 SSH 客户端 —— SSH Studio(Electron + ssh2 + xterm.js)

一款支持密钥/密码认证、多标签终端、SFTP、跳板机、端口转发、资源监控看板、实时日志、批量广播、断线自动重连的桌面 SSH 客户端。技术栈 Electron + ssh2 + xterm.js,纯前端也能写出"运维级"工具。

在这里插入图片描述

https://github.com/yinBlog/ssh

目录


一、为什么又造一个 SSH 客户端?

市面上 SSH 工具不少:PuTTY 轻但简陋、Xshell/SecureCRT 功能全但收费、各类 Web Terminal 又依赖服务端。我想要的是这样一款工具:

  • 开箱即用的桌面应用,不用部署服务端;
  • 真终端体验,彩色输出、光标、窗口自适应一个不少;
  • 不仅仅是终端,而是把日常运维要用的能力(监控、日志、文件、隧道、批量命令)都揉进一个界面;
  • 代码可读、可魔改,纯 JavaScript 技术栈,前端工程师也能轻松二次开发。

于是有了 SSH Studio。它把"连服务器"这件事,做成了一个小型运维工作台。


二、技术栈与整体架构

选型 作用
桌面容器 Electron 31 跨平台窗口、系统能力(通知、剪贴板、文件对话框)
SSH 协议 ssh2 1.16 纯 JS 的 SSH2 客户端,支持 shell / exec / sftp / forwardOut
终端渲染 @xterm/xterm 5.5 + addon-fit 浏览器端终端模拟器,负责 ANSI 解析与渲染
构建打包 electron-builder 输出 Windows NSIS 安装包与 Portable 免安装版

整体是标准的 Electron 三进程模型:

┌──────────────────────────────────────────────────────────┐
│  Renderer(渲染进程 · 浏览器环境)                          │
│  index.html / styles.css / renderer.js                    │
│  - xterm 终端渲染、标签管理、各种面板 UI                    │
│  - 关闭 nodeIntegration,拿不到 Node 能力                   │
└───────────────▲───────────────────────┬───────────────────┘
                │ window.sshBridge       │ IPC (invoke/send/on)
┌───────────────┴───────────────────────▼───────────────────┐
│  Preload(预加载 · contextBridge 白名单桥接)               │
│  只暴露受限、明确的 API,不泄露 ipcRenderer 原始对象         │
└───────────────▲───────────────────────┬───────────────────┘
                │                        │
┌───────────────┴───────────────────────▼───────────────────┐
│  Main(主进程 · Node 环境)                                 │
│  main.js:窗口管理、ssh2 连接编排、监控采集、SFTP、隧道       │
│  lib/:logger(结构化日志)· error-handler · validators       │
│  数据落盘:hosts.json / settings.json                      │
└────────────────────────────────────────────────────────────┘

关键点:所有真正的 SSH 连接、文件读写都跑在主进程(Node 环境),渲染进程通过 contextBridge 暴露的 window.sshBridge 白名单 API 通信,彻底关闭 nodeIntegration。这样即使页面里执行了不可信内容,也拿不到文件系统与网络能力。


三、核心功能全景

连接与会话

  • 🗂️ 多标签终端 —— 每个标签一个独立会话,颜色指示状态(绿=已连、红=出错);
  • 🔑 双认证 —— 密码 / SSH 私钥(可带 passphrase),兼容 keyboard-interactive 交互式认证;
  • 🛡️ 主机指纹校验(TOFU) —— 首次连接记录指纹,变更时弹窗警告,防中间人;
  • 🚇 跳板机 / ProxyJump —— 先连 bastion,再经其打通内网目标;
  • 🔁 断线自动重连 —— 心跳检测死连接,掉线后指数退避重连并带倒计时浮层;
  • 🛰️ 连接前 TCP 预检 —— 先探测 host:port 可达性,端口不通给出明确提示。

运维监控

  • 📊 资源监控看板 —— CPU / 内存 / Swap / 网络吞吐(含 sparkline 折线)、磁盘分区、Top 进程,按需轮询;
  • 📉 底部状态栏 —— 实时响应延迟、CPU 负载、内存、磁盘、运行时长,带迷你占比条与阈值变色告警;
  • 📜 实时远程日志 —— tail -F 流式跟踪(支持日志轮转),关键字过滤高亮、暂停/继续、error/warn 着色;
  • 🔔 阈值桌面通知 —— 指标越过严重阈值时弹系统通知,边沿触发(进入危险才提醒一次)。

操作工具

  • 📁 SFTP 文件管理 —— 浏览、上传、下载、新建文件夹、重命名、删除,复用当前 SSH 连接;
  • 📡 批量命令广播 —— 向多台已连接服务器同时下发命令(支持多行脚本);
  • 🔀 端口转发 / 隧道 —— 本地转发(Local)与动态 SOCKS5 代理。

四、关键实现拆解

4.1 安全模型:三进程隔离

Electron 安全的第一原则:渲染进程不该拥有 Node 能力。做法是三点:

  1. 创建窗口时 nodeIntegration: falsecontextIsolation: true;
  2. preload.js 通过 contextBridge.exposeInMainWorld 只暴露业务需要的方法;
  3. 主进程用 ipcMain.handle / ipcMain.on 承接,永不把原始 ipcRenderer 交给页面。
// preload.js —— 只暴露白名单 API,而不是整个 ipcRenderer
contextBridge.exposeInMainWorld('sshBridge', {
  connect: (cfg) => ipcRenderer.invoke('ssh:connect', cfg),
  sendData: (sessionId, data) => ipcRenderer.send('ssh:data', { sessionId, data }),
  onOutput: (cb) => ipcRenderer.on('ssh:output', (_e, payload) => cb(payload)),
  // ...其余通道
});

4.2 连接编排与跳板机 ProxyJump

主进程用一个 buildAuth(ep, sessionId) 统一构造 ssh2 连接参数(注入心跳与指纹校验),然后按有无跳板机走两条路径:

  • 无跳板:TCP 预检目标 → 直连;
  • 有跳板:TCP 预检跳板机 → 连 bastion → conn.forwardOut() 打到目标 → 用得到的 sock 作为目标连接的底层通道。
// 伪代码:经跳板机连内网目标
bastion.on('ready', () => {
  bastion.forwardOut('127.0.0.1', 0, target.host, target.port, (err, sock) => {
    if (err) return fail(err);
    // 复用 bastion 打通的 socket 作为目标连接底层通道
    targetConn.connect({ sock, ...buildAuth(target, sessionId) });
  });
});

每个会话对象(sessions Map)持有 conn / jumpConn / stream / statsTimer / tunnels / logs,断开时统一清理,避免句柄泄漏。

4.3 主机指纹校验(TOFU)

TOFU(Trust On First Use):首次连接记录服务器公钥指纹,之后每次比对,不一致就弹窗警告。ssh2 提供了 hostVerifier 钩子:

hostVerifier: (keyHash) => {
  const known = knownHosts[endpoint];
  if (!known) { save(endpoint, keyHash); return true; }   // 首次信任
  if (known === keyHash) return true;                     // 指纹一致
  // 指纹变更 —— 通过 IPC 让渲染进程弹窗,由用户决定是否接受
  return askUserViaIPC(endpoint, keyHash);
}

4.4 断线自动重连与心跳

两条防线:

  1. 心跳:keepaliveInterval 定期发包,keepaliveCountMax 次无响应即判死连接;
  2. 指数退避重连:掉线后按 2s → 5s → 10s → 15s → 30s 递增重试,最多 10 次,带倒计时浮层。

关键细节:只在"曾连接成功后掉线"或"已处于重连周期"时才自动重连,避免初次因认证错/地址错导致死循环。cancelReconnectTimer 只取消挂起定时器并保留退避计数,clearReconnect 才彻底结束周期(成功 / 用户停止 / 关闭标签 / 用尽)。

4.5 监控采集:一条命令拿全部指标

为了少开连接、少往返,监控用两条精心拼装的 shell 命令:

  • STAT_CMD:轻量单行指标(延迟、CPU 负载、内存、磁盘),驱动底部状态栏,随会话定时采集;
  • DASH_CMD:分段富指标(CPU / 内存 / 磁盘 / 网络 / 进程),供资源看板按需轮询。

其中 CPU 使用率与网络速率无法从单次快照得出,做法是在渲染进程保留"相邻两次快照",用差值计算:

// 网络速率 = (本次累计字节 - 上次累计字节) / 采样间隔
const rxRate = (snap.rxBytes - prev.rxBytes) / intervalSec;

采集间隔可在 UI 切换(2s / 5s / 10s / 30s / 关闭)并持久化到 settings.json

4.6 实时远程日志 tail -F

exec 起一个 tail -F <path> 通道(-F 而非 -f,以跟随日志轮转),数据流经 IPC 推到渲染进程:

  • 前端做关键字过滤 + 高亮、暂停/继续、自动换行、清空;
  • 按行匹配 error / warn 着色,快速定位问题;
  • 关闭面板时 log:stop 结束对应通道,避免僵尸进程。

4.7 SFTP 文件管理

复用当前 SSH 连接的 conn.sftp() 拿到 SFTP 会话,封装成一组 IPC:list / download / upload / mkdir / delete / rename。上传下载走系统文件对话框选路径,列表按目录/文件排序展示。

4.8 端口转发与 SOCKS5 隧道

  • 本地转发(Local):本机监听端口 → 经 SSH 通道 forwardOut 到远端目标;
  • 动态代理(SOCKS5):本机起一个 SOCKS5 服务,按客户端请求动态 forwardOut,相当于一条轻量"翻墙"隧道。

隧道也挂在会话对象上,断开会话时一并关闭监听。


五、IPC 通道设计

所有能力都以 IPC 通道形式暴露,命名遵循 域:动作 约定,一目了然:

通道 方向 说明
hosts:list/save/delete invoke 主机配置增删查(含跳板机)
settings:get/set invoke 应用设置(采集间隔、通知开关)
ssh:connect invoke 建立连接(支持 ProxyJump)
ssh:data/resize/disconnect send 终端输入 / 尺寸 / 断开
ssh:output/status/stats on 终端输出 / 连接状态 / 状态栏指标
ssh:hostkeyssh:hostkey-reply on/send 主机指纹确认(TOFU)
monitor:snapshot invoke 资源看板一次性富指标采集
log:start/stop + log:data/end invoke/on 实时日志起止与数据流
tunnel:list/add/remove invoke 端口转发 / SOCKS5 隧道
sftp:list/download/upload/mkdir/delete/rename invoke SFTP 文件操作
app:notify send 触发桌面通知(阈值告警)
win:minimize/maximize/close send 自绘标题栏窗口控制

新增一个功能只需三步:主进程 ipcMain.handle → preload 加一行白名单 → 渲染进程调用 window.sshBridge.xxx()

// 1) 主进程
ipcMain.handle('feature:action', async (_e, arg) => {
  try { return { ok: true, data: await doSomething(arg) }; }
  catch (err) { return { ok: false, error: err.message }; }
});

// 2) preload
contextBridge.exposeInMainWorld('sshBridge', {
  featureAction: (arg) => ipcRenderer.invoke('feature:action', arg),
});

// 3) 渲染进程
const r = await window.sshBridge.featureAction(arg);
if (r.ok) console.log(r.data); else console.error(r.error);

六、安装、运行与打包

环境要求

  • Node.js 18+(已在 Node 24 上验证)
  • npm 8+

开发运行

git clone <your-repo-url>
cd ssh
npm install      # 首次会下载 Electron,可能较慢
npm start        # 启动应用

打包(Windows)

npm run pack     # 仅生成免安装目录,验证构建
npm run dist     # 生成 NSIS 安装包 + Portable 免安装版

产物输出到 dist/package.jsonbuild 段配置了 NSIS(可选安装目录、创建桌面快捷方式)与 Portable 两种目标。

使用步骤

  1. 左侧栏右上角 新增主机,填地址 / 端口 / 用户名;
  2. 选认证方式:密码(留空则连接时弹窗询问)或密钥(填私钥绝对路径,加密私钥再填口令);
  3. 点击主机条目连接,终端在右侧打开;标签的 关闭并断开会话。

数据存储

主机配置与设置落盘在 Electron 用户数据目录:

  • Windows:%APPDATA%\ssh-connect-tool\(hosts.json / settings.json)

⚠️ 安全提示:为方便起见,凭据以本地文件存储。请仅在个人可信机器上保存敏感信息;生产环境建议使用密钥认证。


七、踩坑记录

1. Cannot find module 'ssh2' / 原生模块报错
ssh2 含原生依赖,Electron 与系统 Node 的 ABI 不同。执行 npm rebuildnpm run rebuild(electron-rebuild)重建即可。

2. Windows 打包失败
需要 Visual Studio Build Tools 支持原生模块编译;缺失时装一套 C++ 生成工具即可。

3. F12 打不开开发者工具
xterm 终端会捕获按键,需在非终端区域按 F12。启动时(未打包或 DEBUG=1)也会自动打开。

4. 终端尺寸与远端不同步
窗口 resize 时要把 xterm 的 cols/rows 通过 ssh:resize 同步到远端 stream.setWindow(),否则会出现换行错乱、top 显示不全。

5. 初次连接失败陷入重连死循环
务必区分"首次失败"与"曾成功后掉线"——只有后者才该自动重连,否则认证错会无限重试。


八、总结与展望

SSH Studio 证明了一件事:用纯前端技术栈(Electron + ssh2 + xterm.js),也能做出接近专业运维工具的桌面客户端。它的价值不在于某个单点功能,而在于把终端、监控、日志、文件、隧道、批量命令这些"日常运维散件"整合进了一个清爽的界面,并且代码结构清晰、易于二次开发。

后续可以继续打磨的方向:

  • ✅ 凭据用操作系统级加密(Windows DPAPI / macOS Keychain)落盘;
  • ✅ 会话录制与回放;
  • ✅ 命令片段库 / 快捷命令;
  • ✅ 主题与终端配色自定义;
  • ✅ 引入自动化测试(单元 + E2E)。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注。有问题或建议欢迎在评论区交流 👋


技术栈:Electron · ssh2 · xterm.js
许可证:Apache License 2.0

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐