Setting Up Tunnel Run for Persistent Network Connections
Tunnel Run is a lightweight tunneling utility that sits between your local machine and a remote endpoint, keeping connections alive when everything else drops. It's not a household name, but I've used it on and off for years when standard SSH keepalives weren't cutting it. The basic idea is simple: you define a tunnel source and destination, give it a heartbeat interval, and it maintains a persistent connection. Unlike SSH with ServerAliveInterval, Tunnel Run handles multi-hop scenarios better and doesn't drop the ball on NAT traversal issues that trip up most config files. I first ran into it while managing a cluster of servers behind a flaky carrier-grade NAT. Standard reverse SSH tunnels kept dying every 40 minutes or so. Tunnel Run stayed up for weeks. Not perfect, but good enough.
Installation and Setup
You can grab the latest release from the official GitHub repository at github.com/tunnelrun/tunnel-run. Build from source if you're on Linux. The precompiled binaries work fine on x86_64. ARM builds exist but I haven't tested them thoroughly on Raspberry Pi setups. Configuration lives in a YAML file. Here's a minimal example: tunnel: name: default listen: 0.0.0.0:8080 remote: user@remote.host:22 heartbeat: 30 max_retries: 5
The heartbeat is measured in seconds. 30 is a reasonable default. I usually set it to 15 on production links where latency matters. Max retries controls how many times it attempts reconnection before giving up and exiting. Set that to -1 for infinite retry, which is what I typically do on headless boxes.
Get the Full Details

Running It
Start it with: tunnel-run --config /path/to/config.yaml Add it to your systemd service file or cron if you want it persistent. On my machines, a basic unit file with Restart=always and RestartSec=10 keeps it sane through kernel updates and network blips.
The Problem I Hit and the Workaround
Last year I had a Tunnel Run instance die repeatedly on a connection that was actually fine. The issue turned out to be DNS resolution happening only at startup. If the upstream resolver was down when the tunnel initialized, it cached a bad result and never retried. I spent two hours troubleshooting before noticing the timestamps. The fix was running it through a local DNS forwarder like dnsmasq and enabling the --resolve-on-each-reconnect flag. That flag isn't documented on the main README page, so you find it buried in the issue tracker. I ended up patching my own build to default that behavior on, which saved me from having to remember it every time.
Things Nobody Tells You
Bandwidth over a Tunnel Run connection is not impressive. It's not designed for heavy data transfer. I've seen throughput plateau around 2-3 Mbps on a 100 Mbps link. The overhead comes from the keepalive mechanism and the way it buffers retransmissions. If you're moving large files, use a dedicated VPN or just accept the slow pipe. Another thing: log volume can get out of hand fast. By default it writes every heartbeat event. On a busy day with unstable links, that's thousands of lines per hour. I pipe it through a log rotation script that compresses anything older than 24 hours and deletes anything older than a week. Saved me from filling three separate disks in one month.
When Tunnel Run Is the Wrong Tool
Don't use it if you need encryption for sensitive traffic. It wraps connections in SSH but doesn't add its own layer. For that, wireguard or a proper VPN makes more sense. Don't use it if your use case is application-level forwarding like HTTP proxies. It's a transport-layer tool, not an application gateway. And don't run it on machines where you can't access the filesystem to check logs. When it fails silently, you're guessing. I still reach for it when I need a simple reliable tunnel and don't want to configure a full OpenVPN stack. It does one thing and does it well enough. Just read the issues before you deploy it.