SSLTool V4.0:从一个 SSH 客户端到一个多协议终端工作台
本文记录 SSLTool 从 V3.1 到 V4.0 的安全加固与体验升级:多协议、代理、连接复用、录制、SSH 配置互操作、钩子与效率工具,以及一路上踩过的坑。
起点
V3.1 的 SSLTool 已经是一个具备 SSH 终端、SFTP、跳板机、TOFU 主机密钥验证、会话与工程管理的桌面客户端。V4.0 的目标是把它从「SSH 客户端」推近「终端工作台」:同一套界面能连更多东西,并在真实运维场景里少几次点击。
三个梯队一并落地:
- T1(连接能力):SSH 认证扩展(Agent / 证书)、网络出口代理(HTTP CONNECT / SOCKS4 / SOCKS5)、Telnet、串口、SSH 连接复用
- T2(运维效率):终端录制、SSH 配置导入导出、全局日志检索、端口转发管理面板、连接钩子
- T3(工程化):中英文界面、系统托盘、心跳提示、SSH 密钥生成器
多协议:抓住「会话线程」这层抽象
现有代码里,每种连接都是一个 QThread,向上提供相同的信号(connected / dataReceived / error / disconnected)。V4.0 没有重写这套约定,而是让新协议去实现它:
TelnetWorker:原始字节流 + 最少 IAC 处理(应答 WILL/WONT/DO/DONT),窗口尺寸用 NAWS 子协商上报SerialWorker:pyserial 后端,字节流与 SSH 终端完全同构SSLWorker/SSHWorker:既有实现
于是标签页、日志、录制、搜索、快捷键全都自动继承——加协议的成本被压到了「写一个 run 循环」。
代理与跳板机是两层正交的能力
代理回答「本机如何到达目标网络」,跳板机回答「哪台机器替我去连目标」。两者在代码里通过同一个缝对接:
sock_factory = None
if self._jump is not None:
sock_factory = lambda: self._jump.open_channel(self.host, self.port)
elif self.proxy is not None:
sock_factory = lambda: self._make_proxy_sock(self.host, self.port)
paramiko 的 SSHClient.connect(sock=...) 接受任何「已完成握手、可直接当作目标套接字」的 socket,所以 SOCKS5 / HTTP CONNECT 握手逻辑独立在 app/core/proxy.py 里,SSH、SSL、Telnet 与跳板机都能复用同一份实现。
代理最容易被忽略的细节是先认证、后建隧道:
sock.sendall(b"\x05" + bytes([len(methods)]) + methods)
ver, method = _recv_exact(sock, 2)
if method == 0x02: # 用户名密码
...
测试里,我们写了一个迷你 SOCKS5 服务器(同时回放 seen 列表),让 SSHWorker 带 proxy dict 去连内嵌 sshd——如果代码忘了走代理,seen 会是空的,测试立刻失败。
连接复用池:省的是重复握手,防的是重复转发
同一目标、同一跳板机、同一认证方式的多个标签,其实只需要一条已认证连接。app/core/pool.py 做的是典型的引用计数池:
entry = pool.acquire(key, connect_fn) # 命中 refcount+1,否则建连
... # 每标签独立 channel / shell
pool.release(entry) # 归零后闲置,超时由后台线程回收
但有一条硬约束:端口转发只由建立该连接的标签启动。如果复用连接的新标签也去启动 -L 规则,同一个端口会被监听两次。所以每个标签记录「我是不是这条连接的首建者」,转发结果只打给首建标签。
闲置回收用后台线程(IDLE_TIMEOUT = 600 秒),进程退出时 pool.close_all()。
录制:最小可用的 asciinema cast v2
录制不追求完整回放,只做「事后能翻」:每个输出块追加一条 o 事件,输入可选记录 i 事件。
rec.write(b"hello cast") # → [0.123, "o", "hello cast"]
rec.note_input("ls\n") # → [0.456, "i", "ls\n"]
文件头是一行 JSON({"version": 2, "width": 80, "height": 24, ...}),之后的每行是一条事件。这个格式的价值在于:现有工具链都能读——asciinema play、各种 cast 查看器,甚至简单的 jq。
SSH 配置互操作:解析比导出难
导入 ~/.ssh/config 只需要处理五个字段:HostName / User / Port / IdentityFile / ProxyJump。真正麻烦的是边界:
Host *.internal这类通配块要跳过(否则你会得到一堆连不通的会话)ProxyJump admin@bastion:2223要拆成跳板机用户名 / 主机 / 端口- 引号路径、
~展开
导出走相反方向:每个会话生成一个 Host 块,跳板机写成 ProxyJump user@host:port。互操作的意义是「用户已有的资产不必重录」。
钩子:能在真实机器上跑命令的便利与风险
偏好设置里配置连接前 / 后命令,执行时注入 SSHTOOL_HOST / SSHTOOL_PORT / SSHTOOL_USER 环境变量:
run_hook("ssh-add ~/.ssh/id_ed25519", "pre", {"host": ..., "port": ...})
钩子输出会显示在终端里,而不是悄悄吞掉——出问题时用户能看到原因。同时文档明确写出:钩子是本机命令执行,只应配置你自己信任的命令。
i18n:不做框架,只做字典
V4.0 的界面语言切换是最小实现:一个 t() 函数 + 两张字典,运行时按语言取值,菜单标题在切换时原地重命名。
i18n.set_language("en")
i18n.t("menu.file") # "File"
i18n.t("status.reconnecting", n=2, max=5) # "Reconnecting (2/5)"
这不是完整的本地化方案,但对「中英两语」的需求,它比任何框架都轻。
诚实的限制:证书认证
paramiko 没有消费 OpenSSH 用户证书的能力。V4.0 的做法是把证书路径当私钥文件传给 key_filename,并在 README 里明确写:证书认证为实验性,OpenSSH 证书文件通常不会被解析。
同样诚实处理的还有 SOCKS4(仅无认证,用户名作 userid)与代理密码(不保存,每次询问)。把限制写进文档,比假装支持更省用户的时间。
测试:两套件各司其职
- 冒烟(113 项):纯逻辑层——代理握手(迷你 SOCKS5 / HTTP 服务器)、cast 解析、ssh_config 导入导出、i18n、钩子、对话框序列化
- e2e(13 项):真实链路——内嵌 sshd 回显、TOFU、跳板机、续传、经 SOCKS5 代理连 SSH、Telnet 回显
新增的 test_proxy_socks 与 test_telnet 是本版最有价值的两个测试:它们第一次证明了「代理隧道 + paramiko」与「Telnet 裸字节流」在真实连接下的正确性。
结尾
V4.0 的功能清单很长,但真正让这个版本成立的,是三处「不显眼」的设计:
- 会话线程同构——新协议几乎零成本接入界面层
- sock_factory 这个缝——代理与跳板机正交组合
- 诚实写限制——证书、SOCKS4、代理密码
工具的价值不在于支持多少协议,而在于每个协议都走得通、可解释、出问题能定位。
本项目更新到此为止,不再维护
https://github.com/Urtaome0211/SSLTool/releases/tag/v4.0
SSLTool v4.0 — Copyright (c) 2026 Urtaome0211