ssh port forwarding
Port forwarding you can
type from memory.
One command instead of an ssh -L incantation — over the same ssh you already have configured. However many ports you need, in one argument list.
ssh -N -o ExitOnForwardFailure=yes \
-L 127.0.0.1:3000:localhost:3000 \
-L 127.0.0.1:3010:localhost:3010 \
-L 127.0.0.1:8080:localhost:8080 \
10.0.0.7mirb 10.0.0.7 3000 3010 8080No -L, no bind addresses, no backslashes. Add a port by typing a number.
curl -fsSL https://mirb.dev/install.sh | shWraps the system ssh, so your ~/.ssh/config aliases, ProxyJump, agent forwarding and hardware keys all apply untouched. Nothing to install on the remote host.
What it looks like running
One line per forward, so you can see at a glance that the ports you asked for are the ports you got.
Binding the local ports, then probing the far end of each one…
Bare ports, mappings, ranges
The first field is always the local port. Everything else is optional.
| You write | mirrorball forwards |
|---|---|
3000 | localhost:3000 → remote localhost:3000 |
8080:80 | localhost:8080 → remote localhost:80 |
8080:db.internal:5432 | localhost:8080 → db.internal:5432, reached from the ssh host |
3000-3005 | six forwards, same port on both ends |
8000-8002:9000-9002 | paired ranges, zipped in order |
5432:[::1]:5432 | IPv6 literals, bracketed |
Forwards bind to 127.0.0.1. Binding anywhere else would publish the service to your whole network, so mirrorball refuses unless you also pass --expose. Full grammar in Port syntax.
Leave it running, then stop it by name
--background detaches a supervisor and waits for the tunnel to prove itself before returning, so the ports are listening by the time you get your prompt back.
$ mirb -b --name api 10.0.0.7 5432 6379 8080
an8ioa 10.0.0.7 ready
stop it with: mirb stop an8ioa
$ mirb ls
ID NAME HOST FORWARDS UP STATUS
an8ioa api 10.0.0.7 5432 ← 5432, 6379 ← 6379 8s ● ready
$ mirb logs api -f
$ mirb stop an8ioa # or: mirb stop --allThere is no daemon. Each session is a detached mirb supervising one ssh, its state a JSON file under ~/.local/state/mirb. A record whose supervisor is gone is pruned the next time you run mirb ls.
And it tells you when the far end is down
ssh -L binds your local port whether or not anything is listening on the other side, so a dead service looks exactly like a working one until your first request hangs. mirrorball probes each forward and labels it.
bound
The local socket accepts connections. A probe is on its way to the far end.
Wait a moment
ready
A probe reached the remote service. Traffic you send will arrive.
Nothing to do
refused
The tunnel is healthy. Nothing is listening on the remote port.
Start your app
failed
The forward was never established — a bind conflict, auth, or an unreachable host.
Fix your ssh
Compared to
mirrorball | ssh -L | autossh | sshuttle | |
|---|---|---|---|---|
| What it does | wraps ssh -L | port forwarding | keeps an ssh alive | transparent subnet routing |
| Many ports in one argument list | 3000 3010 8080 | one -L each | via ssh args | subnets, not ports |
| Ranges and port mappings | 3000-3005, 8080:80 | by hand, one each | via ssh args | n/a |
| Manage running tunnels | ls / stop / logs | none | none | pidfile with -D |
| Reconnects | backoff + jitter | no | yes, its whole purpose | yes |
| Tells bound from ready from refused | yes | no | no | no per-port model |
| Machine-readable output | NDJSON + JSON | no | no | no |
| Needs root | no | no | no | yes, for firewall rules |
| Needs anything on the remote | no | no | no | yes, Python 3 |
autossh solves reconnection, and only reconnection — for a permanent tunnel under systemd it is still an excellent answer. sshuttle routes whole subnets like a VPN, which is what you want when you do not know in advance which addresses you will need; it costs local root and a Python interpreter on the remote. mirrorball forwards ports you can name, and needs neither.
Start here
- Quick startYour first tunnel, start to finish
- Port syntaxEvery form mirrorball accepts, and what each expands to
- CLI referenceEvery command and flag
- TroubleshootingWhen it says refused, failed, or nothing at all