指南

从任何地方连回自己的机器

跑智能体的那台机器,多半躲在 NAT 后面:家里的路由器、公司内网,或者桌上的一台笔记本。这一页讲怎么用手机连回去,同时不把 SSH 暴露到公网上。

办法有两个。一个是在路由器上做端口转发。另一个是把手机和那台机器放进同一张私有 mesh 网络:手机拿到一个固定地址,公网上什么都不监听。这一页讲第二种。第一种是很多服务器丢掉的原因。

1 把服务器接进 mesh

Tailscale、NetBird、headscale,或者自己搭的 WireGuard,底下都是同一套 WireGuard mesh。在服务器上跑一条命令,它就有了一个固定地址,只有你自己的设备能路由过去。

Tailscale 分配的地址在 100.64.0.0/10 网段,另外给一个像m1-pro.your-tailnet.ts.net 这样的名字。

2 手机接进同一张 mesh

装上厂商的 iOS App 并登录。它会往系统里装一份隧道配置。这一步和 Moshpit 无关。

iOS 一次只允许一条隧道在线,所以公司的远程接入和这张 mesh 一般不能同时开。

3 在 Moshpit 里添加连接

Host 填 mesh 里的名字或地址。Port 没改过就是 22。填好 Username,再在 AUTHENTICATION 下选 SSH Key。

Moshpit 只连你填的那个地址。所谓「集成」,到此为止。

Moshpit 没有任何隧道功能,也没有 Tailscale 集成。它不拨号、不路由、不做发现。它只做一件事:向表单里那个 host 发起 TCP 连接;用 mosh 时,UDP 也发到同一个 host。手机能连到的地址,Moshpit 就能连到。

保存的连接模型里有一个 useTailscale 字段。App 里没有代码读它,表单里也没有这个开关。它是遗留物。与其让你去找那个开关,不如在这里说清楚。

先说不该做的事

不要把 22 端口转发出去,还留着密码登录。

一个刚暴露到公网的 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 的三个限制:

  • 只有两种认证方式。密码和公钥。不支持 keyboard-interactive,所以配置了 TOTP 验证码提示的 sshd,Moshpit 登不上去。
  • 没有 ProxyJump,没有跳板机,没有 agent forwarding。「先 SSH 到跳板机再跳一次」这件事, App 做不到。jumpHost 和 forwardAgent 两个字段虽然在模型里,但没有代码读它们。
  • 带 passphrase 的私钥用不了。加载私钥时不带解密密钥,加密过的 PEM 会直接认证失败。改成在 Moshpit 里生成密钥(Settings → SSH Keys → +),把公钥放到服务器上。这样生成的密钥可以放进 Secure Enclave,由 Face ID 把关。

为什么 UDP 重要

UDP 出现两次,原因各不相同。

底下一层是 WireGuard,它是 UDP 协议。上面一层是 mosh,也是 UDP。两层坏起来的样子不一样,最好分开来想。

底层:mesh

wireguard · udp

  • Tailscale 会先尝试在两台机器之间建立 UDP 直连。默认监听端口 41641,两端都是出站
  • 打不通时改走中转。中转也能用,只是多了延迟
  • 在任意一端跑 tailscale status,就能看到走的是direct 还是某个中转节点
  • 这一层不需要在路由器上开任何入站端口

上层:mosh

隧道之内

  • mosh 端到端都是 UDP。走 mesh 时,它是隧道里的 UDP,路由器看不见它
  • mosh-server 每个会话绑一个端口,从配置的范围里选,默认60000–61000
  • Moshpit 把数据报发到你在表单里填的那个 host,端口用服务端报回来的那个
  • 没有一次成功的 SSH 登录,mosh 起不来。TCP 先通,才轮到 UDP
启动过程,一步不少

SSH 负责启动,然后退场

Moshpit 在已认证的 SSH 会话上执行一条命令,读取它打出的那一行,然后关闭 SSH。之后的一切都走 UDP。

所以连上之后再封 SSH,mosh 会话不会死;封掉 UDP,它才会死。也因此,一张 TCP 顺畅、UDP 却只能中转的 mesh,用 mosh 反而比直接用 SSH 更难受。

Moshpit 里的一个 Mosh 会话,显示着 git log 输出,顶栏是 MOSH 传输标签
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 密钥>

要开哪些端口,开在哪里

用途端口方向原因
SSHTCP 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 那边,这个服务端够不着。
  • 从 mesh 建的会话绑的是 mesh 地址,在局域网里连不上。
  • 两种情况都不是 Moshpit 出错。--bind-server=ssh 就是这个意思。

所以:连接里保存 mesh 的名字,在家也用它。走 mesh 访问自己局域网里的机器,流量本来就会走本地直连。一个地址,一条保存的连接,走出门也不会出意外。

孤儿 mosh-server 会越积越多

每次连接都会启动一个新的 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.」

被拒

SSH 能连,mosh 黑屏

iOS 会悄悄丢掉发往局域网地址的 UDP,不给任何提示。TCP 不受影响,所以 SSH 一切正常,只有 mosh 黑着屏,光标还在闪。

修复

设置 → Moshpit → 本地网络

提示不会自己再弹。去那里打开,然后重连会话。

范围说清楚:这个权限管的是局域网地址,192.168.x.x、10.x.x.x 这一类。发往 mesh 地址的流量走隧道那块虚拟网卡,不受它管。但只要你有可能用局域网地址连这台机器,就把它打开。它造成的故障和防火墙问题长得一样,你会在错的那一头耗掉一个晚上。

验证

六步检查,按这个顺序

每一步都排除它下面那一层。别跳步,答案几乎总在第 1 步或第 2 步。

1 · 对端在线吗?走的是直连吗?

tailscale status          # 对端那一行末尾是 direct 还是某个中转节点名
tailscale ip -4           # 这个地址就是要填进 Host 的

走中转也能用,只是慢。网络差的时候,这就是终端能用和不能用的差别。

2 · 同一张 mesh 上的电脑,直接 SSH 通吗?

ssh -v user@m1-pro.your-tailnet.ts.net

这一步不通就停下。上面的一切都不可能通:mosh 没有独立模式, Moshpit 首先是个 SSH 客户端。

3 · UDP 真的能来回吗?

这一步没人做,而问题往往就在这里。在范围里挑一个端口,服务器上监听,用一台和手机同一网络的电脑发一个包:

# 服务器上
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 的问题。

4 · 端口范围在流量真正进来的那块网卡上开了吗?

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,但可以改。

5 · 问问 App 看到了什么

在 mosh 会话里,长按终端顶栏的传输标签,会打开 MOSH DIAGNOSTICS:datagrams、applied、parse fails、gaps。最要紧的一行不是数字:

MOSH DIAGNOSTICS
   No datagrams yet.

它的意思是一个包都没回来过。这个计数器在解密和解析之前就会加一,所以这就是回程断了。别再查 Moshpit,回到第 3 步。

6 · 走应急出口

如果某张网络就是在吞你的 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。

常见问题

只关于怎么连回去。

必须用 Tailscale 吗?

不用。Moshpit 只连一个地址,这个地址怎么变得可达由你决定。NetBird、headscale、纯 WireGuard、 ZeroTier、公司的远程接入,或者一台有公网 IP 的服务器,从 App 这边看没有区别。这里点名 Tailscale,只是因为它是从零到能用最短的路。

Moshpit 能经过跳板机连接吗?

不能。没有 ProxyJump,不支持堡垒机,也没有 agent forwarding。jumpHost 和forwardAgent 字段在保存的模型里,但没有代码读它们,表单里也没有。有了 mesh 就不需要跳板机,这也是这一页推荐 mesh 的原因之一。

为什么 5G 下能用,公司 Wi-Fi 下不行?

几乎都是同一种网络:放出站 UDP,却丢掉回来的包。可能是分流的隧道配置、中间的转发节点,或者公司防火墙。SSH 照常能用,因为它走 TCP 穿过同一条路。在那张网上跑一遍上面的nc -u 检查。如果那里收不到回包,切到蜂窝网络就能收到,答案就有了。

公司的远程接入和 mesh 能同时开吗?

在 iOS 上一般不行。系统一次只跑一条隧道。公司的远程接入必须在线,mesh 就上不去,你只能走公司网络给你的那条路。这是 iOS 的限制,App 绕不过去。

Moshpit 能看到我的 mesh 或者别的机器吗?

看不到。它没有网络发现,也没有账号。它只知道你填进去的那几条连接,终端流量也只去那里。 Moshpit 自己运营的只有一样东西:通知推送的中转服务器。它转发的是端到端加密的智能体通知,自己解不开,也永远碰不到你的 mesh 和会话。本地网络权限只有一个用途:让 iOS 别丢掉发往局域网的 UDP。不是用来扫描什么的。

回到家 mosh 会话就断了,为什么?

多半是地址的问题。mosh-server 只绑定 SSH 登录进来的那个地址。从 mesh 建的会话在局域网里连不上,反过来也一样。连接里保存 mesh 的名字,到哪儿都用它,在家也一样。见上面的选一个地址。

接下来读在 iPhone 上跑 Claude Code · 回到指南