SSLTool v3.0:嵌套的 SSH、会「续」的下载,和看不见的省心

• 7 分钟阅读 • code

SSLTool 是一个扁平化界面的 SSL / SSH / SFTP 桌面客户端(Python + PyQt5 + Paramiko + pyte)。
v1.2 做了安全加固(TOFU / DPAPI / 证书警告),v2.0 做了终端体验(会话日志 / 搜索 / URL / 括号粘贴 / 备用屏幕),
v3.0 的主题是连接与传输:SSH 跳板机(ProxyJump)、SFTP 断点续传,以及一堆
「不做也行,做了就再也回不去」的省心功能——批量多选、右键菜单、只读模式、会话分组 / 搜索 / 导入导出。

这一版最值得写的不是功能本身,而是两个绕不过去的工程细节:嵌套 SSH 的密钥确认顺序,
和**「续传」两个字的真实成本**。


一、跳板机:在别人的 SSH 里再开一条 SSH

跳板机(bastion / ProxyJump)的语义很简单:先 SSH 到一台中间机器,再从它连到目标服务器。
实现上 OpenSSH 用 ProxyJump 干这件事;Paramiko 没有现成的开关,但给了 sock= 参数——
SSHClient.connect(sock=...) 可以在任意 socket 上跑 SSH 协议,而跳板机开出的 direct-tcpip 通道
恰好就是一个 socket。

# app/core/jump.py(节选)
class JumpConnector:
    def connect(self):
        client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.connect(hostname=self.host, port=self.port, username=self.username, ...)
        self._client = client
        return client

    def open_channel(self, host, port):
        self.connect()
        chan = self._transport.open_channel(
            "direct-tcpip", (host, int(port)), ("127.0.0.1", 0))
        chan.settimeout(12)
        return chan

于是「经跳板机连目标」就变成了:JumpConnector 先登录跳板机 → 对目标开一条 direct-tcpip 通道 →
把通道塞进目标连接的 sock=。核心代码不到 90 行,但安全细节不在这 90 行里。

1.1 两把主机密钥,两轮 TOFU

v1.2 的主机密钥验证是「探测先行」:先用一条只做密钥交换的探测连接取回指纹,用户确认之后,
才把密码发给服务器。加上跳板机之后,密钥从一把变成了两把,顺序必须想清楚:

  1. 先确认跳板机的密钥(此时还没有任何凭据离开本机);
  2. 跳板机密钥确认后,才登录跳板机(此时才向跳板机发送密码);
  3. 对目标的探测走跳板机通道——HostKeyGate 为此加了一个 sock_factory 参数,
    探测连接在 direct-tcpip 通道上建立;
  4. 目标密钥确认后,认证连接同样走通道,建立后再复核一次实际指纹(防两条连接被分别劫持)。

任一步被拒或超时,都不会发出任何密码或私钥签名。错误信息也做了区分:
跳板机本身连不上是「跳板机:无法连接 …」,目标经跳板机不可达是「经跳板机 …」——
生产环境里「到底是哪一跳坏了」经常是排查的第一个问题。

1.2 怎么证明流量真的走了跳板机

单测里不好证明「确实绕路了」——目标机就在 127.0.0.1 上,直连也能通。
e2e 测试在内嵌 SSH 服务器(EchoServer)上加了一个计数器:

direct_requests = 0  # 类属性:direct-tcpip 通道请求次数

def check_channel_direct_tcpip_request(self, chanid, origin, destination):
    EchoServer.direct_requests += 1
    ...

跳板机实例收到的 direct-tcpip 请求 ≥ 2 条(探测 1 + 认证 1),且目标连接成功、
欢迎语收到,才能断定整条链路是「目标 ← 跳板机通道 ← 本机」。
还有一个反向断言:跳板机不可达时,错误信息必须指明是跳板机的问题,而不是含糊的「连接失败」。

二、断点续传:seek 两行,「续」字千斤

Paramiko 的 SFTPFile.seek() 让续传看起来像两行代码的事:

# app/core/sftp_worker.py(节选)
if resume and os.path.exists(local):
    off = os.path.getsize(local)
    if off >= size:
        self.opDone.emit(...)      # 已存在且大小一致:跳过
        return
    mode = "ab"
    rfh.seek(off)                  # 从服务器侧的断点继续读
    ...

但真正的成本在交互上:

e2e 测试覆盖了三条路:续传偏移正确(本地先放半截文件,断言服务器只传了后半截)、
跳过(本地文件大小一致时不触发传输)、覆盖(选覆盖时完整重传)。

三、那些「不做也行」的功能

四、怎么证明:89 项冒烟 + 11 项端到端

五、小结

功能 实现量 有意思的点
跳板机 中 sock= + direct-tcpip 通道;两把密钥两轮 TOFU,顺序即安全
断点续传 小 seek 两行,交互与防重复才是成本
只读模式 小 按钮之外还要禁拖放,否则只读形同虚设
批量多选 / 右键 小 复用已有遍历逻辑,新入口零风险
分组 / 搜索 / 导入导出 中 导出不含密码;导入按 ID 合并,幂等

v3.0 和前面两版一样:功能本身都不惊天动地,但每个功能的边界条件(跳板机的密钥顺序、
续传的询问时机、只读的拖放漏洞)都做成了回归测试。边界条件不是加分项,边界条件就是功能本身。


*SSLTool v3.0 已在 GitHub 发布:https://github.com/Urtaome0211/SSLTool/releases/tag/v3.0,

阅读进度 0%