Moshpit

Guide

Reach your machine from anywhere

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.

Put the server on a mesh

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.

Put the phone on the same mesh

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.

Add the connection in Moshpit

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

Do not forward port 22 with a password on it.

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.

If you genuinely have no alternative

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:

  • Two authentication methods, no third. Password and public key. There is no keyboard-interactive, so an sshd configured for a TOTP prompt will not authenticate from Moshpit.
  • No ProxyJump, no bastion, no agent forwarding. "SSH to a jump box first" is not something the app can do. The jumpHost and forwardAgent fields exist on the model and nothing reads them.
  • Passphrase-encrypted private keys do not work. The key is loaded without a decryption key, so an encrypted PEM fails to authenticate. Generate a key inside Moshpit (Settings → SSH Keys → +) and put its public half on the server instead — those can be held in the Secure Enclave and gated behind Face ID.

Why UDP matters

UDP shows up twice, for different reasons.

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.

The mesh, underneath

wireguard · udp

  • Tailscale tries to build a direct UDP path between the two machines — its default listening port is 41641, outbound from both ends
  • When it cannot punch through, it relays traffic instead. Relayed still works; it costs latency
  • tailscale status on either end tells you which one you got — direct or a relay
  • Nothing here needs an inbound port on your router

mosh, on top

inside the tunnel

  • Mosh is UDP end to end, and over a mesh it is UDP inside the tunnel — your router never sees it
  • mosh-server binds one port per session in the configured range, default 60000–61000
  • Moshpit sends datagrams to the same host string you typed in the form, on the port the server reported
  • Mosh cannot bootstrap without a working SSH login first — TCP has to work before UDP matters
The bootstrap, exactly

SSH starts it, then gets out of the way

Moshpit 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.

A Mosh session in Moshpit showing git log output, with the MOSH pill in the title bar
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 has to be open, and where

WhatPortDirectionWhy
SSHTCP 22 (or your Port) Inbound on the server Mosh has no standalone mode — SSH starts mosh-server and carries the key back
mosh sessionUDP 60000–61000 Inbound on the server, outbound from the phone One port per session, picked by mosh-server inside the range
Mesh direct pathUDP 41641 Outbound from both ends Tailscale's default. Blocked means relayed, not broken
Your routerNothing 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

Pick one address and use it everywhere.

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:

  • A session you started at home, connecting to 192.168.1.30, has a mosh-server bound to 192.168.1.30. From the mesh, that server is unreachable.
  • A session started over the mesh binds the mesh address, and is unreachable from the LAN.
  • Neither of those is Moshpit doing something wrong. It is what --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.

Orphan mosh-servers pile up

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

The Local Network permission, and what denying it looks like

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.

Prompt

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."

Denied

SSH connects, mosh black-screens

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.

Fix

Settings → Moshpit → Local Network

The prompt does not come back on its own. Turn it on there, then reconnect the session.

Scope, stated plainly: this gates LAN addresses192.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

Six checks, in this order

Each one rules out the layer below it. Do not skip ahead — the answer is almost always at step 1 or 2.

1 · Is the peer up, and is it direct?

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.

2 · Does plain SSH work from a laptop on the same mesh?

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.

3 · Does UDP actually round-trip?

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.

4 · Is the range open on the interface the traffic arrives on?

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.

5 · Ask the app what it is seeing

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.

6 · Take the escape hatch

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

What a bad mesh link actually feels like

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

What still does not work once you can reach the box.

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

Reaching the machine, specifically.

Do I have to use Tailscale?

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.

Can Moshpit connect through a jump host?

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.

Why does it work on 5G but not on office Wi-Fi?

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.

Can I run my company VPN and a mesh at the same time?

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.

Does Moshpit see my mesh, or my other machines?

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.

My mosh session died when I got home. Why?

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.