Tailscale、NetBird、headscale,或者自己搭的 WireGuard,底下都是同一套 WireGuard mesh。在服务器上跑一条命令,它就有了一个固定地址,只有你自己的设备能路由过去。
Tailscale 分配的地址在 100.64.0.0/10 网段,另外给一个像m1-pro.your-tailnet.ts.net 这样的名字。
跑智能体的那台机器,多半躲在 NAT 后面:家里的路由器、公司内网,或者桌上的一台笔记本。这一页讲怎么用手机连回去,同时不把 SSH 暴露到公网上。
办法有两个。一个是在路由器上做端口转发。另一个是把手机和那台机器放进同一张私有 mesh 网络:手机拿到一个固定地址,公网上什么都不监听。这一页讲第二种。第一种是很多服务器丢掉的原因。
Tailscale、NetBird、headscale,或者自己搭的 WireGuard,底下都是同一套 WireGuard mesh。在服务器上跑一条命令,它就有了一个固定地址,只有你自己的设备能路由过去。
Tailscale 分配的地址在 100.64.0.0/10 网段,另外给一个像m1-pro.your-tailnet.ts.net 这样的名字。
装上厂商的 iOS App 并登录。它会往系统里装一份隧道配置。这一步和 Moshpit 无关。
iOS 一次只允许一条隧道在线,所以公司的远程接入和这张 mesh 一般不能同时开。
Host 填 mesh 里的名字或地址。Port 没改过就是 22。填好 Username,再在 AUTHENTICATION 下选 SSH Key。
Moshpit 只连你填的那个地址。所谓「集成」,到此为止。
Moshpit 没有任何隧道功能,也没有 Tailscale 集成。它不拨号、不路由、不做发现。它只做一件事:向表单里那个 host 发起 TCP 连接;用 mosh 时,UDP 也发到同一个 host。手机能连到的地址,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,也是 UDP。两层坏起来的样子不一样,最好分开来想。
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)。连接表单里显示同一份,只读。起始端口不在合法范围内时,Moshpit 会整个去掉-p,mosh-server 用自己的默认范围;结束端口不可用时,会话只钉在起始那一个端口上。
那个坑
这个问题最像 bug,却不是 bug。它常常吃掉别人一个晚上,所以单独成节。
Moshpit 启动的是 mosh-server new -s。这个参数就是 mosh 的--bind-server=ssh,从 mosh 1.4 起是上游默认:服务端只绑定 SSH 连接进来的那个地址。
这是有意的设计:数据报走的路,和你刚才登录走的路一样。
后果是:
192.168.1.30 建的会话,mosh-server 绑在192.168.1.30 上。从 mesh 那边,这个服务端够不着。--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 用一个只弹一次的提示管着局域网地址的访问。如果几个月前你在别的 App 界面里随手点了不允许,症状就是下面这样。
第一次连局域网地址时弹出。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 地址的流量走隧道那块虚拟网卡,不受它管。但只要你有可能用局域网地址连这台机器,就把它打开。它造成的故障和防火墙问题长得一样,你会在错的那一头耗掉一个晚上。
验证
每一步都排除它下面那一层。别跳步,答案几乎总在第 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,而你现在就需要一个能用的终端:点横幅上的Switch to SSH,或者点顶栏的 MOSH 标签,确认 "Switch to SSH?"。 SSH 走 TCP,能穿过那条丢数据报的路。横幅可以关掉,也从不挡住终端。你敲的键其实一直都到了服务器,只是画面没回来。
⚠ 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 上掉线(直连 → 中转 → 离线);插件很重的 tmux server 首次启动可能卡死。两件都不是网络问题,但看起来和网络问题一模一样。
依赖它之前,先知道这些限制
App 在后台会被系统挂起 iOS 会挂起 Moshpit,连同它 12 秒一次的 keepalive 和所有轮询。实时的智能体状态停止更新,等你回到 App 才补上。智能体通知照常送达,因为那是主机自己发的端到端加密推送。离开超过 20 秒,Moshpit 回来时会直接重连,而不是相信一次可能被半开 socket 骗过的存活探测。
走 mosh 时,tmux 和 herdr 用的是自己的 TUI
mosh 传的是渲染后的屏幕差分,不是按行分帧的原始字节流,所以 tmux -CC 和 herdr
的帧协议都跑不了。两者都退回到 mosh shell 里的全屏界面。原生面板由旁边另一条轻量 SSH 连接供数据。所以如果你的 mesh 能过 UDP 却封了 SSH,你拿到的是没有原生列表的 tmux。
herdr 的直连 attach 按窗格独占
Moshpit 用 --takeover 接入。两个 Moshpit 客户端指向同一个 herdr 窗格会互相抢,大约每两秒夺回一次。这是 herdr 独占式 attach 的固有行为,客户端遮不住。桌面端的 herdr TUI
不持有 attach owner,所以这个组合应该是安全的,但我们没有验证过。
herdr 0.7.3 不上报窗格的命令
当前 Homebrew stable 版本的快照里没有窗格的命令字段,面包屑只能退回显示pane N。只是显示问题,但面包屑是进入 Select Pane 面板的唯一入口,值得认出来。
已信任的 host key 列不出来,也忘不掉
首次连接会显示指纹和用来核对的 ssh-keygen 命令。之后没有任何界面列出你信任过的主机,没有「忘记这台主机」,删掉连接也不会清掉它的指纹。服务器正常换了密钥,只能在警告弹窗里接受变更。那个按钮刻意做成了红色的危险样式,默认按钮是 Disconnect。
常见问题
不用。Moshpit 只连一个地址,这个地址怎么变得可达由你决定。NetBird、headscale、纯 WireGuard、 ZeroTier、公司的远程接入,或者一台有公网 IP 的服务器,从 App 这边看没有区别。这里点名 Tailscale,只是因为它是从零到能用最短的路。
不能。没有 ProxyJump,不支持堡垒机,也没有 agent forwarding。jumpHost 和forwardAgent 字段在保存的模型里,但没有代码读它们,表单里也没有。有了 mesh 就不需要跳板机,这也是这一页推荐 mesh 的原因之一。
几乎都是同一种网络:放出站 UDP,却丢掉回来的包。可能是分流的隧道配置、中间的转发节点,或者公司防火墙。SSH 照常能用,因为它走 TCP 穿过同一条路。在那张网上跑一遍上面的nc -u 检查。如果那里收不到回包,切到蜂窝网络就能收到,答案就有了。
在 iOS 上一般不行。系统一次只跑一条隧道。公司的远程接入必须在线,mesh 就上不去,你只能走公司网络给你的那条路。这是 iOS 的限制,App 绕不过去。
看不到。它没有网络发现,也没有账号。它只知道你填进去的那几条连接,终端流量也只去那里。 Moshpit 自己运营的只有一样东西:通知推送的中转服务器。它转发的是端到端加密的智能体通知,自己解不开,也永远碰不到你的 mesh 和会话。本地网络权限只有一个用途:让 iOS 别丢掉发往局域网的 UDP。不是用来扫描什么的。
多半是地址的问题。mosh-server 只绑定 SSH 登录进来的那个地址。从 mesh 建的会话在局域网里连不上,反过来也一样。连接里保存 mesh 的名字,到哪儿都用它,在家也一样。见上面的选一个地址。
接下来读在 iPhone 上跑 Claude Code · 回到指南