Appears once
On the first connection to a LAN address. Moshpit's reason string is: "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."
Guide
The box your agents run on is usually behind NAT — a home router, an office network, a laptop on a desk. This is how to reach it from a phone without putting SSH on the public internet.
There are two ways to solve this. One is to forward a port on the router. The other is to put both machines on a private mesh network, so your phone gets a stable address for that box and nothing is listening on the public internet at all. This guide is the second one, because the first one is how people lose servers.
Tailscale, NetBird, headscale, or hand-rolled WireGuard — they are all a WireGuard mesh underneath. One command on the server and it has a stable address that only your own devices can route to.
Tailscale hands out addresses in 100.64.0.0/10 and a name like
m1-pro.your-tailnet.ts.net.
Install the vendor's iOS app and sign in. It installs a VPN profile. Nothing about Moshpit is involved in this step.
iOS runs one VPN tunnel at a time, so a corporate VPN and your mesh generally cannot both be up.
Host = the mesh name or address. Port = 22 unless you moved it. Username, then SSH Key under AUTHENTICATION.
Moshpit connects to exactly the address you typed. That is the entire integration.
Moshpit has no VPN of any kind, and no Tailscale integration. It does not dial, route, or discover anything. It opens a TCP connection to the host string in the form and, for mosh, sends UDP to that same string. If your phone can reach the address, so can Moshpit.
There is a useTailscale field on the saved connection model. Nothing
in the app reads it and it is not in the form. It is a leftover, and we would rather
say so here than have you go looking for the toggle.
First, the thing not to do
A newly exposed SSH port gets found by scanners within minutes. Password authentication turns that into a guessing game with unlimited retries, run by people who do nothing else. A mesh removes the target instead of hardening it.
Some networks make a mesh impossible. If you must expose SSH, the minimum is key-only authentication — and note that this is the minimum, not a recommendation:
# /etc/ssh/sshd_config.d/moshpit.conf PasswordAuthentication no KbdInteractiveAuthentication no PermitRootLogin no AuthenticationMethods publickey # check it parses, then reload: sudo sshd -t && sudo systemctl reload ssh
Three things about Moshpit you should know before you design around it:
jumpHost and
forwardAgent fields exist on the model and nothing reads them.Why UDP matters
Once underneath, because WireGuard is a UDP protocol. Once on top, because mosh is. They fail in different ways, so it is worth keeping them apart in your head.
wireguard · udp
tailscale status on either end tells you which one you got —
direct or a relayinside the tunnel
mosh-server binds one port per session in the configured
range, default 60000–61000Moshpit runs one command over the authenticated SSH session, reads the one line it prints, and then closes SSH. Everything after that is UDP.
That is why blocking SSH after connect does not kill a mosh session, and why blocking UDP does. It is also why a mesh that carries TCP well but relays UDP badly is a mesh where mosh feels worse than plain 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 chars = a 16-byte AES-128 key>
| What | Port | Direction | Why |
|---|---|---|---|
| SSH | TCP 22 (or your Port) | Inbound on the server | Mosh has no standalone mode — SSH starts mosh-server and carries the key back |
| mosh session | UDP 60000–61000 | Inbound on the server, outbound from the phone | One port per session, picked by mosh-server inside the range |
| Mesh direct path | UDP 41641 | Outbound from both ends | Tailscale's default. Blocked means relayed, not broken |
| Your router | Nothing | — | No forwarded ports. That is the entire point of doing it this way |
On a mesh, "inbound on the server" means on the mesh interface.
A host firewall still applies there: ufw, firewalld and a
cloud security group all sit between the tunnel and mosh-server. Opening
the range on tailscale0 is not the same as opening it to the world, and
it is usually the step people skip.
Change the range in Settings → MOSH · ROAMING → UDP port range
(From / To, validated 1–65535 with From ≤ To). The connection form shows the same
range read-only. A start port outside that range drops -p entirely and
mosh-server falls back to its own default; an unusable end pins the session to the
single start port.
The trap
This is the failure that looks most like a bug and is not one. It costs people an evening, so it gets its own section.
Moshpit launches mosh-server new -s. That flag is
mosh's --bind-server=ssh, the upstream default since mosh 1.4: the
server binds only the address the SSH connection arrived on. It is a deliberate
choice — the datagrams then take the same path your login just took.
The consequence:
192.168.1.30, has a
mosh-server bound to 192.168.1.30. From the mesh, that
server is unreachable.--bind-server=ssh means.So: save the connection with the mesh name, and use it at home too. Traffic to your own LAN over a mesh takes a direct local path anyway. One address, one saved connection, no surprises when you walk out of the building.
Every connect starts a fresh mosh-server. When the link
dies, the old one does not exit — it sits there holding its UDP port. Over weeks of
reconnecting they accumulate. Moshpit does not reap them, and that is a real gap, not
a design decision.
pgrep -fl mosh-server # what's still running lsof -nP -iUDP | grep mosh-server # and which address each one bound pkill -f mosh-server # the blunt fix; kills live sessions too
The bind addresses in that second command are also a diagnostic: they tell you which network each session was created from, which is usually enough to explain why one of them stopped answering.
iOS
iOS gates access to local network addresses behind a one-time prompt. If you dismissed it months ago on a different app screen, this is the symptom you will get.
On the first connection to a LAN address. Moshpit's reason string is: "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 drops UDP to LAN addresses on the floor and tells nobody. TCP is unaffected, so SSH works perfectly and only mosh stays black behind a live cursor.
The prompt does not come back on its own. Turn it on there, then reconnect the session.
Scope, stated plainly: this gates LAN addresses —
192.168.x.x, 10.x.x.x and friends. Traffic to a mesh address
leaves through the VPN tunnel interface instead. Grant it anyway if there is any chance
you will connect to that machine by its LAN address, because the failure it produces is
indistinguishable from a firewall problem and you will spend the evening on the wrong
end of it.
Verify
Each one rules out the layer below it. Do not skip ahead — the answer is almost always at step 1 or 2.
tailscale status # the peer line ends in "direct" or a relay name tailscale ip -4 # the address to put in the Host field
A relayed peer still works. It is slower, and on a bad day it is the difference between a usable and an unusable terminal.
ssh -v user@m1-pro.your-tailnet.ts.net
If this fails, stop. Nothing above it can work — mosh has no standalone mode, and Moshpit is an SSH client first.
This is the check nobody runs, and it is the one that finds the problem. Pick a port inside your range, listen on the server, send from a machine on the same network as the phone:
# on the server echo REPLY | nc -u -l 60005 > /tmp/udp-in # from a laptop on the same Wi-Fi as the phone echo PING | nc -u -w3 m1-pro.your-tailnet.ts.net 60005
Each side must see the other's word. Server sees PING but
the client never sees REPLY = the return path is dead, which is exactly
the mosh black-screen. That is a network answer, not an app answer.
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
Then check the cloud security group separately if there is one. And confirm it is the same range Moshpit is configured for — the default is 60000–61000, but it is editable.
Long-press the transport pill in the terminal's top bar on a mosh
session for MOSH DIAGNOSTICS: datagrams, applied,
parse fails, gaps. The line that matters most is the one that
is not a number:
MOSH DIAGNOSTICS No datagrams yet.
That means literally nothing has come back — the counter increments before any decryption or parsing. Dead return path. Stop debugging Moshpit and go back to step 3.
If a network is eating your UDP and you need a terminal now, tap Switch to SSH on the banner, or the MOSH pill in the top bar and confirm "Switch to SSH?". SSH rides TCP through the same path that dropped the datagrams. The banner is dismissible and never blocks the terminal — your keystrokes were reaching the server the whole time; only the rendering was starved.
⚠ Mosh isn't receiving data — your network may be blocking UDP (VPN, proxy, or firewall). Switch to SSH ×
One data point, not a promise
There are no users yet, so there are no aggregate numbers. There is one development host we test against, and its measurements are more useful than an adjective.
A Mac over Tailscale, direct path through a Chinese mobile egress: RTT 433–640 ms, roughly 13% packet loss. SSH handshake takes 5.8–9 seconds on that link. Mosh works on it. The first screen of a bare mosh session lands in about 10 seconds; mosh plus tmux needs two SSH handshakes and takes 20–25 seconds before the native breadcrumb populates.
That is one link, measured once, on hardware we own. It is not a benchmark and your connection will differ. It is here because "survives packet loss" is the kind of claim that should come with a number attached.
Two things that link taught us and that you should expect: a laptop that goes to sleep drops off the mesh entirely (direct → relayed → offline), and a tmux server with a heavy plugin config can deadlock on first launch. Neither is a network problem, and both look exactly like one.
Limits worth knowing before you rely on this
The app suspends in the background iOS suspends Moshpit, its 12-second keepalive timer, and its polling. Agent state stops updating until you come back to the app, and catches up then. There is no push server — that is the trade for having nothing in the middle. Away for more than 20 seconds, Moshpit forces a fresh reconnect on return rather than trusting a liveness probe that can false-positive on a half-open socket.
Over mosh, tmux and herdr run their own TUI
Mosh transmits rendered screen diffs, not a raw line-framed byte pipe, so
tmux -CC and herdr's frame protocol cannot ride it. Both fall back to
their own full-screen interface inside the mosh shell. The native sheets are fed by a
second, lightweight SSH connection alongside — so if your mesh carries UDP but SSH is
blocked, you get tmux without the native lists.
herdr's direct attach is exclusive per pane
Moshpit attaches with --takeover. Two Moshpit clients pointed at the same
herdr pane will fight over it, each reclaiming roughly every two seconds. That is
inherent to herdr's exclusive attach, not something the client can paper over. A
desktop herdr TUI does not hold an attach owner, so that combination is believed safe
— we have not verified it.
herdr 0.7.3 reports no pane command
The current Homebrew stable does not carry a pane's command in its snapshot, so the
breadcrumb falls back to pane N. Cosmetic, but it is the only way into
the Select Pane sheet, so it is worth recognising.
Trusted host keys cannot be listed or forgotten
First connection shows the fingerprint and the ssh-keygen command to
check it against. After that there is no screen listing what you trusted, no "forget
this host", and deleting a connection leaves its fingerprint behind. A legitimate
re-key is handled by accepting the change in the warning dialog — which is deliberately
the destructive-styled button, with Disconnect as the default.
Questions
No. Moshpit connects to an address; how that address becomes reachable is entirely yours to decide. NetBird, headscale, plain WireGuard, ZeroTier, a corporate VPN, or a server with a real public IP all work identically from the app's side. Tailscale is named here because it is the shortest path from nothing to working.
No. There is no ProxyJump, no bastion support and no agent forwarding. The
jumpHost and forwardAgent fields exist on the stored model
and nothing reads them — they are not in the form either. A mesh sidesteps the need
for one, which is part of why this guide recommends it.
Almost always a network that passes outbound UDP and drops the inbound replies —
a split-tunnel VPN profile, a proxy, or a corporate firewall. SSH keeps working
because it rides TCP through the same path. Run the nc -u check above
from that network; if the reply never arrives there and does on cellular, you have
your answer.
Generally not on iOS, which runs one VPN tunnel at a time. If your company VPN has to be up, the mesh will not be, and you are back to whatever route the corporate network gives you. This is an iOS constraint, not something an app can work around.
No. It has no network discovery, no account, no relay and no server of its own. It knows the connections you typed in, and it only ever talks to those. The Local Network permission exists solely so iOS does not drop UDP to LAN addresses — not to scan anything.
Most likely the address. mosh-server binds only the address your SSH
login arrived on, so a session created over the mesh is not reachable from the LAN and
the reverse. Save the connection with the mesh name and use it everywhere, including
at home. See Pick one address
above.