Mosh 与漫游
mosh 是可选的。不开它,Moshpit 就是一个正常的 SSH 客户端,除了漫游之外一切照旧。 mosh 换来的是「手机切网络时会话不死」——代价也先说清楚: 走 mosh 时,tmux 和 herdr 画的是它们自己的全屏 TUI,Moshpit 不再原生渲染它们。 原生 sheet 还在,由旁边另一条 SSH 连接喂着;变的是终端画面本身,那是多路复用器自己画的。 这不是待修的 bug,原因写在下面那一节。
当 iOS 报告出现了更优的网络路径——Wi-Fi 起来了、或者切到蜂窝——Moshpit 会把会话标为 roaming,并立刻从新路径推一个包出去,让服务端把地址迁到新的这个上。
整个过程不重新协商。mosh 的 State Synchronization Protocol 是无连接的:客户端自己维护状态号, 没有握手要重做。收到第一个通过认证的回包,roaming 标记立刻清掉。
它扛得住休眠
Section titled “它扛得住休眠”回到前台时,如果 UDP 连接在 iOS 收走 socket 期间进了 failed 或 waiting 状态, Moshpit 会把它重启,并马上 flush 一个包让服务端重新定位到你的地址。
SSH 会话在长时间挂起后走的是彻底重连;mosh 会话走自己的 resume 路径, 因为 SSP 一般能自愈。但它做不到的是「你不在的时候继续跑」——见 最后一节。
只有当一个屏幕 diff 的 old_num 正好等于 Moshpit 已经应用的状态时才会被应用, 这样 ANSI 才拼得对。出现空洞时,Moshpit 会 ack 自己真实的状态、让服务端重新算 diff, 而不是硬套一个错的。
1 秒一次的心跳负责保住 NAT 映射,也让 ack 一直在流动。
Moshpit 不去调用 mosh 这个可执行文件,也没有打包 mosh 的 C++。 它是同一套协议的一次从零实现。
实现
净室实现,拿 RFC 的向量对过
Section titled “净室实现,拿 RFC 的向量对过”传输层是 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 的项目,这里用这个名字只是为了指明在说哪个协议

一处故意跟上游 mosh 不一样的地方
Section titled “一处故意跟上游 mosh 不一样的地方”没被 ack 的击键,10 秒之后就不再重传了。上游 mosh 是永远重传—— 链路只是抖一下时这么做是对的,链路真的死透了时就是错的: 你对着冻住的屏幕敲的那些键,会在几分钟后突然回放进 shell; 而你重连后已经重新敲过一遍的命令,会被执行第二次。
resize 事件是例外——它是幂等的状态,不是动作。 过期的状态以空 diff 的形式排掉,好让状态编号继续连贯,因为服务端是按编号追踪输入流的。
Moshpit 在花掉一个来回之前会先探测主机。没装 mosh-server 时,你永远不会看到一条裸的 command not found——它会降级。
没装也照样给你一个终端
Section titled “没装也照样给你一个终端”Moshpit 会复用刚刚认证过的那条 SSH 会话,把你接成一个普通 shell, 并顶一条可关闭的横幅:
⚠ mosh-server not found — connected over SSH instead. Install mosh ×首页那张连接卡片上的 MOSH 标签同时会变灰并挂一个警告三角, 免得卡片继续声称自己有漫游。你选的多路复用器不会跟着一起丢:选 tmux 的话, 这条降级后的 SSH 会话照样拉起 tmux -CC;选 herdr 的话, 走的是原生帧通道,和你本来就用 SSH 连过去时一样,是整屏宽的单个 pane。 掉的是漫游,不是多路复用。
Install Assist
Section titled “Install Assist”点 Install mosh 会弹出一个 sheet,里面是按探测到的包管理器生成的命令:
sudo apt-get install -y mosh # Debian、Ubuntusudo dnf install -y mosh # Fedorasudo yum install -y mosh # 老一点的 RHEL / CentOSsudo pacman -S --noconfirm mosh # Archsudo apk add mosh # Alpinebrew install mosh # macOS只有三个动作,没有第四个。Run in terminal 把命令粘进你看得见的那个 shell 并回车, 所以 sudo 提示和全部输出都在你眼皮底下走完;Copy command 只是复制; Re-check 重跑一次探测。Moshpit 绝不静默安装。 如果这条连接同时用 tmux,它会把 tmux mosh 合成一条命令给你。 要是探测压根没找到包管理器,那就没有命令可给:sheet 会直说,只留一段通用说明和同一个 Re-check 按钮。
重连会重跑能力探测,所以你后来才装上的依赖,下次连接就自动被认出来, 不需要去改任何设置。
Homebrew 的坑
Section titled “Homebrew 的坑”ssh host command 起的是非登录、非交互 shell, 它不会 source .zprofile 或 .bash_profile, 所以 brew shellenv 往 PATH 里加的东西在这条通道上是看不见的—— 同一个二进制,你手动登录时跑得好好的,探测时却「不存在」。
Moshpit 在探测和启动这两处都会把同一批目录前置进去:
/opt/homebrew/bin /opt/homebrew/sbin /usr/local/bin /usr/local/sbin $HOME/.local/bin如果这还不够,就去连接表单 ROAMING · MOSH 里的 mosh-server path 填绝对路径——不是设置页里那个叫 Server binary 的行, 连接流程根本不读它(见设置那一节)。 填了绝对路径就意味着你为它背书:Moshpit 原样使用、不再前置 PATH, 并且对这条连接完全跳过「二进制是否存在」的检查。
三件事——第三件绊住的是绝大多数连自己局域网机器的人。
22 端口,或者你自己的 SSH 端口
Section titled “22 端口,或者你自己的 SSH 端口”TCP. 没有一次成功的 SSH 登录,mosh 根本引导不起来。不存在「只跑 mosh」这种模式。
60000–61000,双向
Section titled “60000–61000,双向”UDP. 服务端入站要开,手机出站也要开。mosh-server 每个会话在这个区间里绑一个端口。
本地网络权限
Section titled “本地网络权限”iOS. 只跟局域网主机有关。拒绝之后 iOS 会把发往 192.168.x.x 的 UDP 直接丢掉—— 表现就是 SSH 连得上、mosh 一片黑。公网主机不受影响。
Moshpit 实际执行的那条命令
Section titled “Moshpit 实际执行的那条命令”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-8new -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
四行,按你会用到的顺序排
Section titled “四行,按你会用到的顺序排”这个分组自己的脚注就说得很直白:「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 - 连接表单里显示的端口区间是设置里的镜像,在那儿是只读的——它在你保存表单时被抄进这条连接, 所以改了全局区间,之前保存过的连接不会自动跟上,得重新打开保存一次

| 设置项 | 位置 | 默认值 | 作用 |
|---|---|---|---|
| Mosh by default | 设置 | 开 | 新建的 SSH 主机连接时自动用 mosh-server 包一层 |
| Server binary | 设置 | /opt/homebrew/bin/mosh-server |
只存不读,连接时根本不看它——见下 |
| UDP port range | 设置 | 60000 – 61000 | From / To,保存连接表单时抄进那条连接,再作为 -p 传下去。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 | 连接表单 | 开 | 只存不用——开不开都会漫游 |
设置里的 Server binary 没接上
Section titled “设置里的 Server binary 没接上”这一行在、编辑页也能存,但连接流程从头到尾不看它。 启动 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 —— 帧协议:整屏宽的单个 pane,原生的 workspace 和 tab 列表
- 面包屑、滑动切换、所有 sheet 都来自同一个控制面
走 mosh
Section titled “走 mosh”它们自己的全屏界面
- tmux —— mosh shell 里那个普通的 attach TUI,用 Ctrl-b 操作,跟着 mosh 一起漫游
- herdr —— herdr 自带的 TUI,Moshpit 只是渲染器
- 另开的一条轻量 SSH 连接喂原生 sheet:渲染走 UDP,控制走 TCP
这件事 Moshpit 是在你连接之前告诉你的,不是连上才说。 连接表单的脚注最后一句就是它:「With Mosh, herdr runs its own terminal UI; native rendering needs SSH.」另有一点要从那个 TUI 里带走:herdr 的前缀键同样是 Ctrl-b, 但前缀之下的绑定跟 tmux 不是一套——Ctrl-b q 在 tmux 里是列 pane,在 herdr 里是 detach。 App 里的 sheet 会按各自的多路复用器打印真实按键;你的肌肉记忆没有这层保护。
双传输在实际使用中意味着什么
Section titled “双传输在实际使用中意味着什么”- mosh + tmux 是同时两条连接。如果 SSH 被封而 UDP 通, 控制面 sidecar 起不来——Moshpit 会退回到往 mosh shell 里直接敲
tmux attach。你拿到的是 tmux,但拿不到原生 sheet。 - 两个控制面都骑在 SSH 上,而 iOS 会在后台把它杀掉。回到前台时 mosh 渲染通道自愈,
-CCsidecar 或 herdr 轮询器则被重建。 - tmux 窗口是通过 sidecar 钉到手机的字符网格上的, 因为 mosh 渲染器自己没法给窗口设尺寸。没有这个钉住, 它那个 80×24 的 PTY 会把 TUI 卡在屏幕上半部分的 80×24 里。
- 滚动回看是这条规则的例外。sidecar 的 copy-mode 不会去重绘另一个 mosh 客户端, 所以一次滑动只能从 mosh 这条通道自己走。窗格里如果是抢了鼠标的程序——Claude Code、vim、less ——收到的是转发过去的滚轮事件,由它自己滚;如果是普通 shell,才发 copy-mode 按键,粒度是整页。
- mosh + tmux 的引导最长可能要 15 秒左右(第二次 SSH 握手 + 轮询), attach 循环最多等 6 秒,等控制面落到某个 session 上。
- Moshpit 绝不替你建 tmux session。如果 sidecar 活着但服务端一个 session 都没有, 它就让 mosh shell 保持原样,等你自己显式创建第一个。
socket 到了 ready,你敲的键也送到了服务端,但回程数据报一个都没到。 黑屏、光标还在闪、没有任何报错。这几乎总是你此刻所在网络上的 VPN、代理或防火墙 ——放行了出站 UDP,丢掉了入站的。
Moshpit 会主动发现它
Section titled “Moshpit 会主动发现它”连接进入 ready 并开始 flush 之后,会挂上一个8 秒的看门狗。 这个时长是故意给得宽的:第一个回包在第一次 flush 之后一个 RTT 就该到, 所以哪怕延迟好几秒的链路也能远远赶在它之前应答。只有什么都不回的链路才会把它耗完。
只要落地任何一个数据报,看门狗立刻解除—— 哪怕那个包解密失败或者解析失败也算——所以「能用但很慢」的链路永远不会误触发。 而且它只在第一个数据报之前武装:会话中途卡住是另一回事, 那种情况通常靠漫游能自己恢复,所以它不管。
一旦触发,终端顶部会出现一条琥珀色横幅:
⚠ Mosh isn't receiving data — your network may be blocking UDP (VPN, proxy, or firewall).
Switch to SSH ×这条横幅从不挡住终端。击键照样送得到服务端,只是渲染被饿住了。 Switch to SSH 会翻转这条连接的协议、存下来、然后用 TCP 重连—— TCP 走的正是那条把你 UDP 吃掉的路径,而它能通。 点 × 只是关掉横幅,mosh 会话原样保留。
你也可以随时自己切:点顶栏的 MOSH 标签,确认 「Switch to SSH?」。重连会重跑能力探测,所以不会有什么状态一直卡在降级里。
怎么读那几个计数器
Section titled “怎么读那几个计数器”在 mosh 会话上长按传输标签,会弹出 MOSH DIAGNOSTICS—— 四个数字,是给你截图发出来用的,不是给你欣赏的:
datagrams 收到的 UDP 数据报总数,在解密和解析之前就计数。标签显示「已连接」而这个数很低, 说明问题在网络或 NAT,不在 App。
applied 实际应用到的最高显示状态号。datagrams 在涨而它一直是 0, 说明到目前为止每个 diff 都掉进了「空洞」或「解析失败」分支—— 服务端有内容排着队,但一条都没被接受。
parse fails 到了但解不开的 diff。数字高,说明这台主机的输出里有什么东西解不出来。
gaps 乱序到达的 diff。丢包链路上偶尔几个是正常的;但如果它一直涨而 applied 一直不动,说明客户端卡在反复重新求差分上了。
「No datagrams yet.」 这句话就是判据。一个包都没到过——那是回程路径死了,不是渲染 bug。 别再查 Moshpit 了,去查网络。
这个浮层之所以存在,是因为这类故障要真实的丢包、乱序, 或者某台远端特定的 shell 配置才复现得出来——这些环回测试全都造不出来。 对一个谁也没法从外部检查的会话来说,把这个浮层截图发出来是唯一实用的诊断手段。
提 issue 之前先按这个顺序过一遍
Section titled “提 issue 之前先按这个顺序过一遍”- 换蜂窝试试。5G 上正常、Wi-Fi 上不正常,那答案是这个 Wi-Fi 网络,不是 App。
- 把手机上的 VPN 关掉再连。分流模式的 VPN 配置是最常见的元凶。
- 连的是局域网主机?去手机的 设置 → 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?
那是 sidecar 没起来。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, 原生的 workspace 和 tab sheet 由另一条 SSH 连接轮询 herdr api snapshot 来喂——有变化时 2 秒一轮,树不动了之后退到 8 秒一轮。 herdr 的 CLI 没有开放事件订阅,所以这个轮询就是 sheet 和 agent 状态能有多新的天花板。 另外 herdr 0.7.3 的 pane 对象不带命令名,所以窗格面包屑显示的是 pane 1 这种兜底文案,0.8.0 才补上这个字段。
想要 herdr 被原生渲染,那就走 SSH。走之前先知道一件事:herdr 的直连附着是按窗格独占的, 第二个客户端连上同一个窗格会把第一个踢掉。两台 Moshpit 盯同一个窗格就会来回抢, 直到其中一边退让——Moshpit 的退让规则是 30 秒内被踢三次就先停一会儿, 并显示「Another client is using this pane — retrying shortly.」 herdr 自己的 TUI 不占这个附着位,所以你笔记本上开着的普通 herdr 不算竞争者。
为什么我敲的东西过了几分钟才出现在一个旧会话里?
现在不该再有了。没被 ack 的击键 10 秒后就过期,不再永远重传, 为的正是不让一条死了很久又回来的链路,把一堆陈旧输入回放进你的 shell。 如果你还遇到,那是值得报上来的 bug。