只弹一次
第一次连局域网地址时弹。Moshpit 的说明文案是:「Moshpit connects to servers on your local network over SSH and mosh (UDP). Without this permission, connections to LAN hosts (e.g. 192.168.x.x) will fail.」
指南
跑 agent 的那台机器多半在 NAT 后面——家里的路由器、公司的内网、桌上一台合着盖的 笔记本。这篇讲怎么从手机连回去,而不用把 SSH 挂到公网上。
路只有两条。一条是在路由器上做端口转发,然后祈祷。另一条是把两台设备拉进同一张 私有 mesh 网络:手机拿到这台机器的一个固定地址,而公网上一个监听端口都没有。这篇讲第二条, 因为第一条是大多数人丢掉服务器的方式。
Tailscale、NetBird、headscale,或者自己搭的 WireGuard——底下都是同一套东西。服务器上一条 命令,它就有了一个只有你自己的设备能路由到的固定地址。
Tailscale 发的地址在 100.64.0.0/10 里,另外给一个
m1-pro.your-tailnet.ts.net 这样的名字。
装官方 iOS App 登录即可,它会装一个 VPN profile。这一步跟 Moshpit 没有任何关系。
iOS 同时只能起一条 VPN 隧道,所以公司 VPN 和 mesh 基本上没法同时开着。
Host 填 mesh 的名字或地址,Port 没改过就是 22,再填 Username, AUTHENTICATION 那一栏选 SSH Key。
Moshpit 连的就是你填的那个地址,一个字都不多解释。整个「集成」就这么多。
Moshpit 自己不带任何 VPN,也没有 Tailscale 集成。它不拨号、不做路由、不做发现。 它只是往表单里那个 host 开一条 TCP 连接;走 mosh 的话,UDP 也发到同一个字符串。 手机能连到那个地址,Moshpit 就能连到。
连接的数据模型里确实有一个 useTailscale 字段,但 App 里没有任何代码读它,
表单里也没有这个开关。它是历史遗留——与其让你满设置里翻,不如在这写清楚。
先说不该做的
一个刚暴露出去的 SSH 端口,几分钟内就会被扫到。开着密码认证,等于把它变成一场 没有次数限制的猜密码游戏,而对面是全职干这个的。mesh 的做法不是「加固目标」,是让目标根本不存在。
有些网络环境就是搭不起 mesh。真要暴露 SSH,最低限度是只留公钥认证——注意这是 下限,不是推荐做法:
# /etc/ssh/sshd_config.d/moshpit.conf PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no AuthenticationMethods publickey # 改完先验证再重载:sudo sshd -t && sudo systemctl reload ssh
在你按这套方案设计之前,Moshpit 有三件事得先知道:
jumpHost 和 forwardAgent 字段在模型里存着,
但没有任何代码读它们。为什么 UDP 是关键
底下一次,因为 WireGuard 本身就是 UDP 协议;上面一次,因为 mosh 也是。 两者坏起来的样子完全不同,所以最好在脑子里分开放。
wireguard · udp
tailscale status,就能看到走的是
direct 还是中继跑在隧道里
mosh-server 每个会话在配置的范围里绑一个端口,默认
60000–61000Moshpit 在已认证的 SSH 通道上跑一条命令,读走它打印的那一行,然后把 SSH 关掉。 之后全程都是 UDP。
所以连上之后再把 SSH 掐了,mosh 会话不会死;而把 UDP 掐了, 它就死了。也正因如此,一张 TCP 走得挺好、UDP 却只能中继的 mesh,用 mosh 反而不如直接用 SSH 舒服。
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 MOSH CONNECT 60001 <22 个 base64 字符 = 一把 16 字节的 AES-128 密钥>
| 用途 | 端口 | 方向 | 为什么 |
|---|---|---|---|
| SSH | TCP 22 (或你自己改的 Port) | 服务器入站 | mosh 没有独立模式——是 SSH 拉起 mosh-server 并把密钥带回来的 |
| mosh 会话 | UDP 60000–61000 | 服务器入站 + 手机出站 | 每个会话一个端口,由 mosh-server 在范围内自己挑 |
| mesh 直连 | UDP 41641 | 两端出站 | Tailscale 的默认端口。被挡了只是退化成中继,不是不通 |
| 你的路由器 | 什么都不用开 | — | 不做任何端口转发。这正是选这条路的全部意义 |
走 mesh 的时候,「服务器入站」指的是在 mesh 这块网卡上入站。主机防火墙照样管得着:
ufw、firewalld、云上的安全组,都夹在隧道和 mosh-server 中间。
在 tailscale0 上放开这段范围,跟对公网放开完全是两回事——而这一步恰恰是最常被跳过的。
范围在 Settings → MOSH · ROAMING → UDP port range 里改(From / To,
校验 1–65535 且 From ≤ To)。连接表单里显示的是同一份,只读。起始端口不合法时会整个丢掉
-p,让 mosh-server 用它自己的默认范围;结束端口不合法则退化成只钉在起始那一个端口上。
最像 bug 的那个坑
这个问题最像 App 的 bug,但它不是。它值一个晚上的排查时间,所以单开一节。
Moshpit 起的是 mosh-server new -s。这个参数就是 mosh 的
--bind-server=ssh,也是 mosh 1.4 起的上游默认行为:server 只绑 SSH 连接进来的
那一个地址。这是有意的设计——数据报因此会走你刚刚登录时走过的同一条路。
于是就有了:
192.168.1.30 建起来的会话,它的 mosh-server 绑在
192.168.1.30 上。从 mesh 那一侧,这个 server 根本够不着。--bind-server=ssh 的字面含义。所以:连接里就存 mesh 的名字,在家也用它。反正 mesh 到自己局域网内的流量本来 就会走本地直连。一个地址、一条连接、走出办公室的时候不会突然出事。
每次连接都会起一个新的 mosh-server。链路断掉时,旧的那个不会退出——
它就杵在那占着自己的 UDP 端口。反复重连几周下来就攒了一堆。Moshpit 目前不回收它们,
这是实打实的缺口,不是什么设计取舍。
pgrep -fl mosh-server # 还活着几个 lsof -nP -iUDP | grep mosh-server # 以及每个分别绑在哪个地址上 pkill -f mosh-server # 粗暴解决;活着的会话也会一起被杀
第二条命令里的绑定地址本身就是诊断信息:它告诉你每个会话当初是从哪张网建的, 通常这一条就足够解释「为什么其中一个不响应了」。
iOS
iOS 用一个一次性弹窗管着对局域网地址的访问。如果你几个月前在别的界面顺手点了「不允许」, 症状就是下面这个。
第一次连局域网地址时弹。Moshpit 的说明文案是:「Moshpit connects to servers on your local network over SSH and mosh (UDP). Without this permission, connections to LAN hosts (e.g. 192.168.x.x) will fail.」
iOS 会把发往局域网地址的 UDP 直接丢掉,而且不告诉任何人。TCP 不受影响,所以 SSH 一切正常, 只有 mosh 黑着屏、光标还在闪。
弹窗不会自己再来一次。在那里打开,然后重连会话。
说清楚它管的范围:这个权限管的是局域网地址——192.168.x.x、
10.x.x.x 这些。发往 mesh 地址的流量走的是 VPN 隧道那块虚拟网卡。
但只要你有可能用局域网地址连这台机器,就顺手把它打开:它造成的故障和防火墙问题长得一模一样,
而你会在错误的那一头耗掉一整晚。
怎么验
每一步都在排除它下面那一层。别跳步——答案基本都在第 1 步或第 2 步。
tailscale status # 对端那一行末尾是 direct 还是某个中继名 tailscale ip -4 # 这个地址就是要填进 Host 的
走中继也能用,只是慢;运气差的时候,这就是「能用」和「没法用」的区别。
ssh -v user@m1-pro.your-tailnet.ts.net
这一步不通就别往下走了。上面所有东西都建立在它之上——mosh 没有独立模式, Moshpit 首先是个 SSH 客户端。
这一步几乎没人做,而它恰恰是最容易揪出问题的一步。在范围里挑一个端口, 服务端监听,用一台和手机在同一张网上的电脑发:
# 服务器上 echo REPLY | nc -u -l 60005 > /tmp/udp-in # 和手机同一个 Wi-Fi 的电脑上 echo PING | nc -u -w3 m1-pro.your-tailnet.ts.net 60005
两边都得看到对方那个词。服务端收到了 PING、客户端却始终等不到
REPLY,就是回程死了——这正是 mosh 黑屏的成因。这是网络的答案,不是 App 的答案。
sudo ufw status verbose # Debian / Ubuntu sudo firewall-cmd --list-all # Fedora / RHEL sudo ufw allow in on tailscale0 to any port 60000:61000 proto udp
然后云上的安全组要单独再看一遍。另外确认放开的是 Moshpit 配置的同一段范围—— 默认是 60000–61000,但它是可改的。
在 mosh 会话里长按终端顶栏的传输标记,会弹出 MOSH DIAGNOSTICS:
datagrams、applied、parse fails、gaps。
最有价值的那一行反而不是数字:
MOSH DIAGNOSTICS No datagrams yet.
它的意思是:一个包都没回来过——这个计数器在任何解密和解析之前就已经加过了。 回程是死的。别再查 Moshpit 了,回到第 3 步。
如果这张网就是在吃你的 UDP,而你现在就要一个能用的终端:点 banner 上的 Switch to SSH,或者点顶栏的 MOSH 标记确认 "Switch to SSH?"。 SSH 走 TCP,能穿过那条把数据报吃掉的路径。banner 可以直接叉掉,它从不挡住终端—— 你的击键其实一直都送到了服务器,被饿死的只是画面。
⚠ Mosh isn't receiving data — your network may be blocking UDP (VPN, proxy, or firewall). Switch to SSH ×
一个实测数字,不是承诺
还没有用户,所以没有任何统计数字。只有一台我们天天拿来测的开发机——它的实测数据 比任何形容词都有用。
一台 Mac,走 Tailscale 直连,出口是国内移动网络:RTT 433–640 ms,丢包约 13%。 这条链路上 SSH 握手要 5.8–9 秒。mosh 在上面能用:裸 mosh 会话大约 10 秒出第一屏; mosh + tmux 要做两次 SSH 握手,原生面包屑填上要 20–25 秒。
这是一条链路、测了一次、机器是我们自己的。它不是 benchmark,你的网络一定不一样。 写在这里,是因为「扛得住丢包」这种话,理应带个数字。
这条链路还教了我们两件事,你也该有心理准备:笔记本一睡下去就整个从 mesh 上掉了 (direct → 中继 → 离线);插件配置很重的 tmux server 首次启动时可能直接卡死。 这两件都不是网络问题,但它们看起来跟网络问题一模一样。
依赖它之前,先知道这些边界
App 切到后台就被挂起 iOS 会挂起 Moshpit、它 12 秒一次的 keepalive、以及所有轮询。agent 状态会停在那里不再更新, 等你切回 App 才补上。没有推送服务器——这是「中间什么都没有」的代价。后台超过 20 秒, Moshpit 回来时会直接重连,而不去信任那个可能对半开 socket 误判为「活着」的探活。
走 mosh 时,tmux 和 herdr 跑的是自己的 TUI
mosh 传的是渲染完的屏幕差分,不是原始的按行分帧字节流,所以 tmux -CC 和 herdr
的 frame 协议都承载不了,两者都退回到 mosh shell 里的全屏界面。原生 sheet 是由旁边另一条
轻量 SSH 连接喂的——所以如果你的 mesh 能过 UDP 但 SSH 被挡了,你会得到一个没有原生列表的 tmux。
herdr 的直连 attach 是每个 pane 独占的
Moshpit 用 --takeover 接入。两个 Moshpit 客户端指向同一个 herdr pane 会互相抢,
大约每两秒互相夺一次。这是 herdr 独占式直连的固有行为,客户端糊不过去。桌面端的 herdr TUI
不持有 attach owner,所以那个组合我们认为是安全的——但还没验证过。
herdr 0.7.3 不上报 pane 的命令
当前 Homebrew stable 版本的快照里没有 pane 的命令字段,面包屑因此退回显示
pane N。只是显示问题,但面包屑是进 Select Pane sheet 的唯一入口,值得先认得它。
已信任的 host key 既列不出来也删不掉
首次连接会把指纹和用来核对的 ssh-keygen 命令一起显示给你。但之后没有任何界面能
列出你信任过哪些主机,没有「忘记这台主机」,删掉连接也不会清掉它的指纹。服务器正常换了密钥,
只能在警告弹窗里接受变更——那个按钮是刻意做成红色危险样式的,默认按钮是 Disconnect。
常见问题
不用。Moshpit 连的是一个地址,这个地址怎么变得可达完全由你决定。NetBird、headscale、 自己搭的 WireGuard、ZeroTier、公司 VPN,或者一台有公网 IP 的服务器,从 App 这边看没有任何区别。 这里点名 Tailscale,只是因为它是从零到能用最短的一条路。
不能。没有 ProxyJump,不支持堡垒机,也没有 agent forwarding。jumpHost 和
forwardAgent 字段存在模型里,但没有代码读,表单里也没有。mesh 恰好让你不再需要跳板机,
这也是这篇推荐它的原因之一。
基本都是那种放行出站 UDP、却把回来的包丢掉的网络——分流的 VPN profile、代理、
或者公司防火墙。SSH 照样能用,因为它走 TCP 穿过同一条路。在那张网上跑一遍上面的
nc -u 测试;如果那里等不到回包、切到蜂窝就能收到,答案就出来了。
在 iOS 上基本不行,系统同时只跑一条 VPN 隧道。公司 VPN 必须开着,mesh 就开不了, 你就只能用公司网络给你的那条路。这是 iOS 的限制,不是 App 能绕过去的。
不会。它没有网络发现、没有账号、没有中继,也没有自己的服务器。它只知道你手动填进去的那几条连接, 也只会跟它们通信。本地网络权限存在的唯一目的是别让 iOS 把发往局域网的 UDP 丢掉,不是用来扫描什么的。
多半是地址的问题。mosh-server 只绑 SSH 登录进来的那个地址,
所以从 mesh 建的会话在局域网里够不着,反过来也一样。连接里就存 mesh 的名字,在家也用它。
见上面「选定一个地址」那一节。