首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >为什么 ssh 每次按键都会发送 100 个数据包?

为什么 ssh 每次按键都会发送 100 个数据包?

作者头像
萝卜要努力
发布2026-03-04 21:42:56
发布2026-03-04 21:42:56
790
举报
文章被收录于专栏:萝卜要加油萝卜要加油

最近读到一篇很有意思的文章,作者 nolen royalty[1] 在开发一个基于 SSH 的高性能在线游戏时,发现了一个诡异的性能问题—— 一次按键竟然会触发 100 多个数据包。这背后的原因让我也学到了不少东西,记录分享一下。

问题是怎么发现的

作者在用 Go 的 bubbletea[2] + wish[3] 构建一个通过 SSH 运行的 TUI 游戏,性能目标是支持 2000+ 并发玩家。

为了做压力测试,作者写了个测试工具,用几百个 bot 通过 SSH 连接到服务器,模拟玩家每秒操作一次。正常情况下,服务端需要给每个 bot 渲染一个 80×60 的游戏画面,每秒更新 10 次——这是主要的 CPU 和带宽消耗。

有一天,测试工具出了个 bug:服务端没有给 bot 发送正常的游戏画面,而是只回了一条 "your screen is too small" 的错误信息就不再更新了。按理说,既然不用渲染游戏画面了,CPU 和带宽应该接近零才对。但实际上,它们只降了大约一半。

这就很奇怪了——游戏数据完全没发,CPU 用量为什么还有 50%?剩下的开销到底花在哪了?

用 Tcpdump 抓包分析

tcpdump 抓一次按键的数据包,结果非常出乎意料:

代码语言:javascript
复制

$ ./summarize_pcap.sh single-key.pcap
Total packets: 270

  36-byte msgs:   179 packets ( 66.3%)   6444 bytes
  Other data:       1 packet  (  0.4%)    564 bytes
  TCP ACKs:        90 packets ( 33.3%)

  Data packet rate: ~90 packets/second (avg 11.1 ms between data packets)

按一个键,产生了 270 个数据包!其中 179 个是神秘的 36 字节消息,大约每 11ms 发送一个。真正的"有效数据"只有 1 个包(564 字节),其余全是 36 字节的"不知道干什么用的"包。

排查过程

最开始怀疑的方向有几个:

  • SSH 流量控制机制?
  • PTY 轮询?
  • bubbletea/wish 框架的问题?

关键线索是:这些 36 字节的包全部是由客户端发起的,而不是服务端

用普通的 ssh 会话测试了一下,问题依然存在——说明和游戏框架无关,是 SSH 客户端本身的行为。

最终,ssh -vvv 的调试输出给出了答案:

代码语言:javascript
复制

debug3: obfuscate_keystroke_timing: starting: interval ~20ms
debug3: obfuscate_keystroke_timing: stopping: chaff time expired (49 chaff packets sent)
debug3: obfuscate_keystroke_timing: starting: interval ~20ms
debug3: obfuscate_keystroke_timing: stopping: chaff time expired (101 chaff packets sent)

根本原因:按键时序混淆

2023 年,OpenSSH 引入了一个叫做 按键时序混淆(Keystroke Timing Obfuscation) 的安全功能。

原理很简单:攻击者可以通过分析 SSH 数据包的时序模式,推断出你按了哪些键。比如你输入密码的时候,每个字符之间的间隔时间是不同的,这个时序信息就是一种侧信道。

OpenSSH 的解决方案是:在你按键的同时,发送大量干扰包(Chaff Packets),把真实的按键时序淹没在噪声里。这些干扰包其实就是 SSH2_MSG_PING 数据包,每隔约 20ms 发送一次。

当服务端宣告支持 [email protected] 扩展时,客户端就会自动开启这个功能。

对普通 SSH 会话来说,这是非常有意义的安全特性。但是对于一个面向整个互联网、延迟至关重要的在线游戏来说,这些额外的包带来了巨大的性能开销。

解决方案

客户端可以通过配置 ObscureKeystrokeTiming=no 来禁用这个功能。但这不现实——你不可能要求所有玩家都去改 SSH 配置。

那能不能从服务端禁用?

作者先问了 Claude Code,得到的回答是"不可能"。但通过阅读 Go 的 crypto/ssh 库源码,他发现:客户端是否开启这个功能,取决于服务端是否在握手阶段宣告了 [email protected] 扩展。也就是说,只要服务端不宣告这个扩展,客户端就不会发干扰包。

最终方案是 fork Go 的 crypto 库,去掉对 [email protected] 扩展的支持声明,然后用 go.modreplace 指令替换依赖。

效果立竿见影:

代码语言:javascript
复制

Total CPU:  29.90% → 11.64%
Syscalls:   3.10s  → 0.66s
Crypto:     1.6s   → 0.11s
Bandwidth:  ~6.5 Mbit/sec → ~3 Mbit/sec

CPU 直接砍半,syscall 开销降了 80%,带宽也几乎减半。

当然,fork 标准库的 crypto 包需要持续跟进上游更新,维护成本不低,这是一个需要权衡的点。

LLM 在调试中的角色

这篇文章里另一个有意思的点是作者对使用 LLM 辅助调试的反思:

  • ChatGPT 非常自信地把这些流量模式识别为"正常的 SSH 行为"——错了
  • Claude Code 最初认为服务端禁用不可能——也错了
  • 最关键的逻辑跳跃——把损坏的测试工具与 SSH 开销联系起来——是人做出的

LLM 在分析 tcpdump 输出、提供初步方向方面确实很有用,但在关键判断上,人的直觉和对问题的深入理解仍然不可替代

总结

  • OpenSSH 在 2023 年引入了按键时序混淆,通过发送干扰包来隐藏打字模式,这是一个有效的安全特性
  • 但对高性能场景(如在线游戏)来说,这些额外的包会带来显著的 CPU 和带宽开销
  • 通过服务端不宣告 [email protected] 扩展,可以让客户端自动关闭此功能
  • 性能优化没有银弹,很多时候瓶颈并不在你以为的地方——这次就是一个 SSH 协议层面的"隐藏特性"

参考链接

  • 原文: SSH sends 100 packets for every keystroke[4]
  • OpenSSH Keystroke Timing Obfuscation[5]
  • bubbletea - TUI 框架[6]
  • wish - SSH server for Go[7]

引用链接

[1]nolen royalty: https://blog.nolen.dev/

[2]bubbletea: https://github.com/charmbracelet/bubbletea

[3]wish: https://github.com/charmbracelet/wish

[4]原文: SSH sends 100 packets for every keystroke: https://blog.nolen.dev/posts/ssh-sends-100-packets-per-keystroke

[5]OpenSSH Keystroke Timing Obfuscation: https://www.openssh.com/releasenotes.html

[6]bubbletea - TUI 框架: https://github.com/charmbracelet/bubbletea

[7]wish - SSH server for Go: https://github.com/charmbracelet/wish

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜要加油 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题是怎么发现的
  • 用 Tcpdump 抓包分析
  • 排查过程
  • 根本原因:按键时序混淆
  • 解决方案
  • LLM 在调试中的角色
  • 总结
  • 参考链接
    • 引用链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档