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.

What you have been typing
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.7
mirrorball
mirb 10.0.0.7 3000 3010 8080

No -L, no bind addresses, no backslashes. Add a port by typing a number.

curl -fsSL https://mirb.dev/install.sh | sh
Read the docs

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

Bare ports, mappings, ranges

The first field is always the local port. Everything else is optional.

You writemirrorball forwards
3000localhost:3000 → remote localhost:3000
8080:80localhost:8080 → remote localhost:80
8080:db.internal:5432localhost:8080 → db.internal:5432, reached from the ssh host
3000-3005six forwards, same port on both ends
8000-8002:9000-9002paired ranges, zipped in order
5432:[::1]:5432IPv6 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 --all

There 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

mirrorballssh -Lautosshsshuttle
What it doeswraps ssh -Lport forwardingkeeps an ssh alivetransparent subnet routing
Many ports in one argument list3000 3010 8080one -L eachvia ssh argssubnets, not ports
Ranges and port mappings3000-3005, 8080:80by hand, one eachvia ssh argsn/a
Manage running tunnelsls / stop / logsnonenonepidfile with -D
Reconnectsbackoff + jitternoyes, its whole purposeyes
Tells bound from ready from refusedyesnonono per-port model
Machine-readable outputNDJSON + JSONnonono
Needs rootnononoyes, for firewall rules
Needs anything on the remotenononoyes, 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