跳转到内容
moshpit

Mosh 与漫游

mosh 是可选的。不用它,Moshpit 就是一个普通的 SSH 客户端,除了漫游,其他功能一样。mosh 带来的是手机换网络时会话不断。代价也先说清楚:走 mosh 时,tmux 和 herdr 画的是它们自己的全屏 TUI,Moshpit 不再原生渲染。原生面板还能用,由旁边另一条 SSH 连接供数据。但终端画面本身是多路复用器画的。这不是等着修的 bug,原因见下文。

iOS 报告有了更好的网络路径,比如 Wi-Fi 连上了,或者切到了蜂窝网络,Moshpit 会把会话标记为漫游中,立刻从新路径发一个包出去,服务端就把地址迁到新的这一个。

整个过程没有重新协商。mosh 的 State Synchronization Protocol 是无连接的,客户端自己维护状态编号,没有握手要重做。收到第一个通过认证的回包,漫游标记就清掉。

回到前台时,如果 UDP 连接在 iOS 接管 socket 期间进入了 failed 或 waiting 状态,Moshpit 会重启它并立刻 flush 一个包,让服务端重新认到你的地址。

SSH 会话长时间挂起后走的是完整重连。mosh 会话走自己的恢复路径,因为 SSP 一般能自愈。它做不到的是在你离开时继续跑,见最后一节。

一个画面 diff 只在它的 old_num 正好等于 Moshpit 当前持有的状态时才应用,这样 ANSI 才拼得对。出现空洞时,Moshpit 回报自己真实的状态,让服务端重新算 diff,而不是硬套一个错的。

每秒一次的心跳保住 NAT 映射,也让确认包一直在流动。

Moshpit 不调用 mosh 可执行文件,也没有打包 mosh 的 C++ 代码。它是同一套线上协议的从零实现。

实现

传输层是 Swift,架在 Apple 的 Network framework 上。加密是 AES-128-OCB3,也就是 RFC 7253 里的 AEAD_AES_128_OCB_TAGLEN128 参数集。测试套件跑的是 RFC 附录 A 自带的测试向量,包括「篡改 tag 必须拒绝」那一条。

  • 按公开的协议描述写成,不是从 mosh 的 GPL-3.0 C++ 翻译过来。完整声明在仓库的 NOTICES.md
  • zlib 分帧在 Apple 的裸 DEFLATE 之上手写,对齐 mosh-server 的预期
  • 数据报按 1300 字节分片
  • 「mosh」是 Keith Winstein 的项目。这里用这个名字只为说明在讲哪个协议

Moshpit 里的一个 Mosh 会话,显示着 git log 输出,顶栏是 MOSH 标签

没有确认的按键,10 秒后停止重传。上游 mosh 会一直重传。链路只是抖了一下,这样做是对的。链路真的断了,这样做就是错的:你对着冻住的画面敲的键,几分钟后会突然回放进 shell。重连后已经重敲过的命令,会再执行一次。

resize 事件例外,它是幂等的状态,不是动作。过期的状态以空 diff 排出,让状态编号保持连续,因为服务端按编号追踪输入流。

Moshpit 在花一个往返之前会先探测主机。没装 mosh-server 时不会出现一句裸的 command not found,而是降级。

Moshpit 复用刚认证过的那条 SSH 会话,把你接成普通 shell,顶部挂一条可以关掉的横幅:

Terminal window
⚠ mosh-server not found — connected over SSH instead.
Install mosh ×

首页这条连接的卡片上,MOSH 标签同时变灰并加一个警告三角,卡片不会声称自己有它没有的漫游。多路复用器不受影响:用 tmux 的话,降级后的 SSH 会话照样启动 tmux -CC。用 herdr 的话,走原生帧通道,和纯 SSH 连过去时一样,是一个整屏宽的窗格。丢的是漫游,不是多路复用。

点 Install mosh,弹出的面板里是按探测到的包管理器给出的命令:

Terminal window
sudo apt-get install -y mosh # Debian、Ubuntu
sudo dnf install -y mosh # Fedora
sudo yum install -y mosh # 老一点的 RHEL / CentOS
sudo pacman -S --noconfirm mosh # Arch
sudo apk add mosh # Alpine
brew install mosh # macOS

只有三个操作。Run in terminal 把命令粘进你看得见的 shell 并按回车,sudo 提示和全部输出都在你眼前。Copy command 只复制。Re-check 重新探测一次。Moshpit 从不静默安装。这条连接同时用 tmux 的话,它会把 tmux mosh 合成一条命令。探测没找到任何包管理器,就没有命令可给。面板会直说,留下一段通用说明和同一个 Re-check 按钮。

重连会重新探测。后来才装上的依赖,下次连接自动认出来,不用改任何设置。

ssh host command 启动的是非登录、非交互的 shell。它不加载 .zprofile 和 .bash_profile,brew shellenv 加进 PATH 的目录在这条通道上看不见。同一个可执行文件,手动登录时好好的,探测时却「不存在」。

Moshpit 在探测和启动两处都把同一批目录加到 PATH 前面:

/opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin $HOME/.local/bin

还不够的话,在连接表单 ROAMING · MOSH 分组的 mosh-server path 里填绝对路径。不是设置页里那行 Server binary,连接流程根本不读它(见设置一节)。填了绝对路径,就等于你为它担保:Moshpit 原样使用,不再加 PATH 前缀,这条连接也完全跳过「可执行文件是否存在」的检查。

三件事。第三件绊倒的多半是连自己局域网机器的人。

TCP。没有一次成功的 SSH 登录,mosh 启动不起来。没有「只用 mosh」这种模式。

UDP。服务端入站,手机出站。mosh-server 每个会话在这个区间里绑一个端口。

iOS。只影响局域网主机。拒绝之后,iOS 会直接丢掉发往 192.168.x.x 的 UDP。表现是 SSH 连得上,mosh 黑屏。公网主机不受影响。

Terminal window
PATH="$PATH:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:$HOME/.local/bin" \
mosh-server new -s -c 256 -p 60000:61000 -l LANG=en_US.UTF-8

new -s 把 UDP socket 绑到这条 SSH 连接的服务端 IP 上,数据报走的就是你刚刚 SSH 登录的那条路。-c 256 要求 256 色。服务端打印一行,然后 fork 到后台:

MOSH CONNECT 60001 <22 个 base64 字符 = 一把 16 字节 AES-128 密钥>

Moshpit 读到这一行,关掉 SSH 连接,之后的画面全走 UDP。所以连上之后再封 SSH,mosh 会话的画面不受影响。封 UDP 才会。有一个例外:连接用了 tmux 或 herdr 时,Moshpit 会另开一条 SSH 连接跑控制面,这条连接在整个会话期间都需要 TCP 通着。

端口区间填错是降级,不是报错

Section titled “端口区间填错是降级,不是报错”
  • 起始端口不可用(非正数,或大于 65535):整个 -p 参数去掉,mosh-server 用自己的默认区间
  • 结束端口不可用:起始端口变成单端口,-p 60000
  • 只有结束端口确实大于起始端口时,才是 -p start:end

设置里的编辑页在保存前会校验:不是数字、超出 1–65535、From 大于 To,各有各的提示。

全局默认在 设置 → MOSH · ROAMING。按连接的设置在 Add/Edit Connection 表单的 ROAMING · MOSH 分组。这两处有四行只存不读。与其让你自己撞上,不如在这里点名。

设置 → Mosh · Roaming

这个分组自己的脚注说得很直白:「Mosh runs over UDP and survives IP changes. If your server is behind a strict firewall, open the port range above outbound from your iPhone.」

  • 关掉 Mosh by default,新主机就是纯 SSH。已有的连接保持保存时的设置
  • Moshpit 真正执行的路径只有一个:连接自己的 mosh-server path。留空就跑 mosh-server
  • 连接表单里显示的端口区间是设置页的镜像,在表单里只读。保存表单时它抄进这条连接。改了全局区间,之前保存的连接不会跟着变,得重新打开保存一次

Moshpit 设置页的 Mosh 与漫游分组:Mosh by default、Predictive Echo、Server binary、UDP port range

设置项 位置 默认值 作用
Mosh by default 设置 开 新建的 SSH 主机连接时自动套一层 mosh-server
Server binary 设置 /opt/homebrew/bin/mosh-server 只存不读,连接时不看它。见下文
UDP port range 设置 60000 – 61000 From / To。保存连接表单时抄进那条连接,再以 -p 传给 mosh-server。1–65535,From ≤ To
Use Mosh 连接表单 跟随默认 按主机选传输方式。也可以在顶栏的传输标签上随时切换
mosh-server path 连接表单 空 Moshpit 真正启动的路径。留空就用 PATH 上的 mosh-server,比绝对路径更通用
Predictive Echo · Predict Mode 两处都有 Adaptive 只存不用。见下文
Trail on predict 设置 → Cursor 开 只存不用。见下文
Roam on Cellular 连接表单 开 只存不用。开不开都漫游

这一行在,也能保存,但连接流程从头到尾不看它。启动 mosh 只读一个路径:连接自己的 mosh-server path,留空就是 mosh-server。所以默认值 /opt/homebrew/bin/mosh-server 不是你的 Linux 主机上在跑的东西。它们跑的是上面那段扩展 PATH 里解析出来的 mosh-server,这正是你想要的。要指定某个可执行文件,填连接表单里的那个字段,不是这一行。

预测回显(predictive echo)没有实现

Section titled “预测回显(predictive echo)没有实现”

选择器是真的,选完也真的存下来了,但当前版本里它什么都不改变。传输层里没有本地预测引擎,Moshpit 只渲染服务端发来的字节。协议里的 echo-ack 字段直接跳过不读,这个模式从没传到 UDP 传输层,远端命令行上也没有它的任何痕迹。Trail on predict 处境相同,它只驱动设置页里的一小块预览,别的什么都不做。漫游横幅上的「Predict ON」是写死的文字,不是状态。

在高延迟链路上,你敲的字符要等服务端回显才会出现,和走 SSH 一样。

Roam on Cellular 同样只存不读。漫游不是可选项:系统告诉 Moshpit 有了更好的路径,漫游就发生,这个开关开着关着都一样。与其等你为它提 bug,不如在这里写明。

mosh 传的是渲染好的画面差分,不是裸字节管道。按行分帧的控制协议在上面活不下来。实测验证过:tmux -CC 在服务端确实启动了,但一条 %begin 或 %output 都回不来。herdr 的帧协议同理。

原生渲染

  • tmux:控制模式。Sessions、Windows、Pane 都是真正的列表
  • herdr:帧协议。一个整屏宽的窗格,原生的工作区和标签页列表
  • 面包屑、滑动切换、所有面板都来自同一个控制面

多路复用器自己的全屏界面

  • tmux:mosh shell 里普通的 attach TUI,用 Ctrl-b 操作,跟着 mosh 一起漫游
  • herdr:herdr 自带的 TUI,Moshpit 只负责渲染
  • 另开一条轻量 SSH 连接给原生面板供数据:渲染走 UDP,控制走 TCP

这件事 Moshpit 在你连接之前就说了,不是连上才说。连接表单脚注的最后一句就是它:「With Mosh, herdr runs its own terminal UI; native rendering needs SSH.」用那个 TUI 时还要记一件事:herdr 的前缀键也是 Ctrl-b,但前缀之后的绑定和 tmux 不是一套。**Ctrl-b q 在 tmux 里列出窗格,在 herdr 里是断开连接。**Moshpit 自己的面板会按各自的多路复用器打印真实按键,你的肌肉记忆没有这层保护。

双传输在实际使用中意味着什么

Section titled “双传输在实际使用中意味着什么”
  • mosh + tmux 是同时两条连接。SSH 被封而 UDP 通的话,控制面的旁路连接起不来。Moshpit 会退回到往 mosh shell 里直接敲 tmux attach。你有 tmux,但没有原生面板。
  • 两个控制面都走 SSH,而 iOS 会在后台把 SSH 断掉。回到前台时,mosh 的渲染通道自己恢复,-CC 旁路连接或 herdr 轮询重新建立。
  • tmux 窗口通过旁路连接固定到手机的字符网格上,因为 mosh 渲染器自己没法设置窗口尺寸。没有这一步,它那个 80×24 的 PTY 会把 TUI 困在屏幕上半部分的 80×24 里。
  • 回滚是这条规则的例外。旁路连接的 copy-mode 不会重绘另一个 mosh 客户端,所以上滑手势得从 mosh 这条通道自己走。窗格里是接管了鼠标的程序(Claude Code、vim、less),它收到转发的滚轮事件,自己滚。普通 shell 收到的是 copy-mode 按键,按页翻。
  • mosh + tmux 的启动最长可能要 15 秒左右:第二次 SSH 握手,然后轮询。attach 循环最多等 6 秒,等控制面落到某个会话上。
  • Moshpit 从不替你新建 tmux 会话。旁路连接活着,但 tmux 服务端一个会话都没有,它就让 mosh shell 保持原样,等你自己建第一个。

socket 进入 ready 状态,你敲的键也到了服务端,但回程的数据报一个都没到。黑屏,光标还在闪,没有任何报错。几乎总是你当前所在网络上的隧道软件、中间设备或防火墙在作怪:放出站 UDP,丢入站的。

连接进入 ready 并开始 flush 之后,会启动一个8 秒的看门狗。这个时长故意给得宽:第一个回包在第一次 flush 之后一个往返就该到,延迟好几秒的链路也能远远赶在它之前回答。只有什么都不回的链路才会耗完它。

只要落地任何一个数据报,看门狗立刻解除。解密失败或解析失败的包也算。所以「能用但很慢」的链路永远不会误触发。它只在第一个数据报到达前生效。会话中途卡住是另一回事,通常靠漫游就能恢复,看门狗不管。

触发后,终端顶部出现一条琥珀色横幅:

⚠ Mosh isn't receiving data — your network may be
blocking UDP (VPN, proxy, or firewall).
Switch to SSH ×

横幅从不挡住终端。按键照样到服务端,只是画面收不到。Switch to SSH 把这条连接的传输方式切成 SSH,保存,然后用 TCP 重连。吃掉你 UDP 的那条路径,TCP 能通。点 × 只关掉横幅,mosh 会话原样保留。

你也可以随时自己切:点顶栏的 MOSH 标签,确认「Switch to SSH?」。重连会重新探测能力,不会有什么状态一直卡在降级里。

在 mosh 会话上长按传输标签,弹出 MOSH DIAGNOSTICS。四个数字,是用来截图发出来的,不是用来欣赏的:

datagrams 收到的 UDP 数据报总数,在解密和解析之前计数。标签显示已连接而这个数很低,问题在网络或 NAT,不在 App。

applied 实际应用到的最高画面状态编号。datagrams 在涨而它一直是 0,说明到现在每个 diff 都掉进了空洞或解析失败的分支。服务端有内容排着队,一条都没有接受。

parse fails 到了但解不出来的 diff。数字高,说明这台主机的输出里有什么东西解析不了。

gaps 乱序到达的 diff。丢包的链路上偶尔几个正常。它一直涨而 applied 不动,说明客户端卡在反复重算 diff 上。

「No datagrams yet.」这句话是判据。一个包都没到过,那是回程路径不通,不是渲染 bug。别再查 Moshpit,去查网络。

这个浮层之所以存在,是因为这类故障要真实的丢包、乱序,或者某种特定的远端 shell 配置才能复现。环回测试造不出这些。对一个外人无法检查的会话来说,这张截图是唯一实用的诊断材料。

  • 换蜂窝网络试试。5G 上正常,Wi-Fi 上不正常,问题在这个 Wi-Fi,不在 App。
  • 关掉手机上的网络隧道类配置再重连。分流模式的隧道配置是最常见的原因。
  • 连的是局域网主机?看一眼手机的 设置 → Moshpit → 本地网络。拒绝了的话,iOS 会直接丢掉发往 192.168.x.x 和 10.x.x.x 的 UDP,而且不告诉任何人。
  • 查云上的防火墙或安全组:UDP 区间入站开了吗?开的是不是 Moshpit 里配置的同一个区间?
  • 在同一个网络下用笔记本跑一次 mosh。它也黑屏,那就不是 Moshpit 的问题。
我一定要用 mosh 吗?

不用。不用它,Moshpit 就是普通的 SSH 客户端。在设置里关掉 Mosh by default,或者在某条连接上不开 Use Mosh。除了漫游,其他表现完全一样。tmux 和 herdr 的原生渲染只有走 SSH 才有,网络本来就稳的话,SSH 更划算。

mosh 是用来替代 SSH 的吗?

不是。没有一次成功的 SSH 登录,mosh 启动不起来。SSH 负责启动 mosh-server,把会话密钥带回来。之后 Moshpit 才关掉 SSH 连接,转到 UDP。TCP 22(或你的 SSH 端口)必须可达。

mosh + tmux 下终端为什么卡在 80×24?

那是旁路连接没起来。mosh 渲染器自己改不了 tmux 窗口尺寸,尺寸是由走 SSH 的 -CC 控制连接固定的。SSH 被封,你拿到的就是 tmux 默认的 PTY 尺寸。这台主机改用 SSH,或者把 SSH 和 UDP 一起放开。

这是真的 mosh,还是仿的?

它和真实的、未经修改的 mosh-server 说的是真协议。但客户端是 State Synchronization Protocol 的独立净室 Swift 实现,按公开的协议描述写成,不是从 mosh 的 GPL-3.0 C++ 派生的。加密部分用 RFC 7253 自带的测试向量验证过。

mosh 能和 herdr 一起用吗?

能,也会漫游。但 herdr 跑的是它自己在 mosh shell 里的 TUI。原生的工作区和标签页面板由另一条 SSH 连接轮询 herdr api snapshot 供数据:有变化时每 2 秒一次,树不动了之后放慢到每 8 秒一次。herdr 的 CLI 没有事件订阅,这个轮询就是面板和智能体状态新鲜程度的上限。另外 herdr 0.7.3 的窗格对象不带命令名,窗格面包屑显示的是 pane 1,0.8.0 才加了这个字段。

想让 herdr 原生渲染,就走 SSH。走之前先知道一件事:herdr 的直连是按窗格独占的,第二个客户端连上同一个窗格会把第一个踢掉。两台 Moshpit 盯着同一个窗格会互相抢,直到一边退让。Moshpit 的规则是 30 秒内被踢三次就先停一会儿,并显示「Another client is using this pane — retrying shortly.」herdr 自己的 TUI 不占这个连接,所以笔记本上开着普通 herdr 的终端不算竞争者。

为什么我敲的东西过了几分钟才出现在一个旧会话里?

现在不该再有了。没有确认的按键 10 秒后过期,不再无限重传,就是为了不让一条死了很久又回来的链路把一堆陈旧输入回放进你的 shell。还遇到的话,是值得报上来的 bug。