ProxyJump timeouts when a VPN drops your IP address
When you hop through a bastion with ssh -J, the jump host keeps a TCP tunnel open to the target.
If you’re on a VPN that can change your public IP or even tear the tunnel down, that tunnel can stall or die.
The result? A hanging SSH that eventually spits out “Connection timed out” or “Connection reset by peer”.
For homelab users who rely on VPNs for remote access, this is the kind of annoyance that keeps you awake at night.
Below is a practical, coffee‑sized walk‑through that shows how to spot the problem, patch it up, and build a more resilient workflow that survives VPN hiccups.
1. Reproduce the problem
# From your laptop
ssh -J bastion.example.com [email protected]
If the VPN disconnects in the middle, you’ll see something like:
ssh_exchange_identification: Connection closed by remote host
or a long pause followed by:
ssh: connect to host target.internal port 22: Connection timed out
What’s happening is that the bastion’s outbound link to the target stays open, but the VPN no longer forwards packets back to the laptop. The SSH client on the laptop eventually gives up.
2. Visualise the path
Laptop (VPN) → Bastion (jump host) → Target (internal)
- VPN on laptop: encrypts traffic to the VPN server, then the server routes it to the bastion.
- Bastion: usually has a static IP, but its outbound interface may be on a private network that the VPN server routes to.
- Target: often behind NAT or a firewall that only accepts connections from the bastion’s IP.
When the VPN drops, the laptop stops sending packets to the VPN server. The bastion’s connection to the target stays open, but the packets never reach the laptop. The SSH client eventually times out.
3. Quick sanity checks
| Check | Command | What to look for |
|---|---|---|
| VPN status | ip a show tun0 |
Interface exists and has an IP. |
| Routing table | ip route show |
Default route via VPN (tun0). |
| Connectivity to bastion | ping -c 3 bastion.example.com |
No packet loss. |
| Connectivity to target from bastion | ssh -q -o BatchMode=yes [email protected] exit |
Immediate exit. |
If any of these fail, the VPN is already broken before SSH starts.
4. Why ssh -J stalls
ssh -J opens a single TCP connection from the laptop to the bastion, then uses that same connection to open a second tunnel to the target.
The SSH client keeps the first leg alive until the session ends.
If the VPN drops, the first leg is silently broken, but the SSH client doesn’t notice until it tries to send a packet and gets a timeout.
The default SSH timeout is 120 s for keepalive packets. That’s why you often see a 2‑minute pause before the error.
5. Solutions
5.1. Use ControlMaster and ControlPersist
Instead of a single long‑running connection, open a short‑lived control socket that can be reused.
Add to ~/.ssh/config:
Host bastion
HostName bastion.example.com
User bastionuser
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 5m
Host target.internal
User targetuser
ProxyJump bastion
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 5m
Now, when you run:
ssh target.internal
SSH will reuse the existing control socket if it’s still alive, otherwise it will create a new one.
The ControlPersist 5m keeps the socket open for five minutes after the session ends, so a brief VPN drop won’t immediately kill the connection.
Trade‑off: If the VPN drops for longer than five minutes, the socket will be closed and a new connection will be attempted, which may fail if the VPN is still down.
5.2. Enable TCP keepalives
Tell SSH to send keepalive packets more frequently. This helps the client detect a broken tunnel sooner.
Host *
ServerAliveInterval 30
ServerAliveCountMax 5
With ServerAliveInterval 30, SSH sends a keepalive every 30 seconds.
If five consecutive keepalives fail, the client aborts the session.
This reduces the timeout from 2 minutes to roughly 2.5 minutes.
5.3. Use ProxyCommand with ssh -W and -o ExitOnForwardFailure=yes
ssh -J is just syntactic sugar for ProxyCommand ssh -W %h:%p.
Adding ExitOnForwardFailure=yes makes the client exit immediately if the forward fails, rather than waiting for the timeout.
Host target.internal
ProxyCommand ssh -W %h:%p -o ExitOnForwardFailure=yes bastion
This is a more explicit configuration and works on older OpenSSH versions that lack -J.
5.4. Switch to a VPN‑independent bastion
If you control the bastion, set it up with a public IP or a static IP on a network that the VPN server always routes to.
That way, even if your laptop’s VPN drops, the bastion can still reach the target.
You can then use a simple ssh target.internal without -J.
This is the most robust solution but requires network changes.
5.5. Use autossh to maintain the jump
autossh monitors an SSH connection and restarts it if it dies.
Wrap the jump host in autossh:
autossh -M 0 -N -f -o "ServerAliveInterval 30" -o "ServerAliveCountMax 5" \
-o "ExitOnForwardFailure=yes" -J bastion target.internal
-M 0 disables the monitoring port, but autossh still restarts the session if it exits.
This adds resilience but also adds a bit of overhead.
6. Practical example: a homelab setup
Assume:
- VPN client on laptop uses
tun0. - Bastion has static IP
203.0.113.10. - Target is
10.0.0.5.
6.1. ~/.ssh/config
Host bastion
HostName 203.0.113.10
User bastionuser
IdentityFile ~/.ssh/id_rsa_bastion
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
ServerAliveInterval 30
ServerAliveCountMax 5
Host target.internal
HostName 10.0.0.5
User targetuser
ProxyJump bastion
IdentityFile ~/.ssh/id_rsa_target
ControlMaster auto
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
ServerAliveInterval 30
ServerAliveCountMax 5
6.2. Test the connection
ssh target.internal
If the VPN drops mid‑session, you should see:
ssh: connect to host target.internal port 22: Connection timed out
but only after ~2.5 minutes, not 2 minutes.
The ControlPersist will keep the socket alive for 10 minutes, so a brief drop won’t kill the session.
6.3. Verify keepalive behavior
ssh -vv target.internal
Look for lines like:
debug1: Sending packet: type 18
debug1: Sending packet: type 18
These are keepalive packets. If you see them every 30 seconds, the configuration works.
7. Troubleshooting tips
| Symptom | Likely cause | Fix |
|---|---|---|
ssh_exchange_identification: Connection closed by remote host |
VPN dropped, bastion closed connection | Increase ServerAliveInterval, use ControlPersist |
ssh: connect to host target.internal port 22: Connection timed out |
VPN down, no route | Check ip route, restart VPN |
ssh: connect to host bastion.example.com port 22: Connection refused |
Bastion firewall blocked | Verify bastion’s firewall, ensure port 22 open |
ssh: connect to host target.internal port 22: No route to host |
VPN routing issue | Re‑establish VPN, check default route |
ssh: connect to host target.internal port 22: Connection reset by peer |
VPN reconnects with a different IP | Use ProxyCommand with -o ExitOnForwardFailure=yes |
8. Security considerations
- Key management: Keep separate SSH keys for bastion and target. Store them in
~/.sshwithchmod 600. - SSH agent forwarding: Avoid
-Aunless you trust the bastion. Forwarding can expose your keys if the bastion is compromised. - Firewall rules: On the bastion, limit inbound SSH to known IPs (e.g., your VPN’s subnet). On the target, allow only the bastion’s IP.
- Audit logs: Enable
LogLevel VERBOSEon the bastion to capture jump attempts.
9. When to choose each method
| Method | When to use |
|---|---|
ControlMaster + ControlPersist |
Most users; minimal overhead. |
ServerAliveInterval |
When VPN drops are frequent; quick detection. |
ProxyCommand with ExitOnForwardFailure |
Older OpenSSH or when you need explicit control. |
autossh |
Long‑running background services that must stay alive. |
| Public IP bastion | When you can modify network topology; highest resilience. |
ProxyJump timeouts caused by VPN IP changes are a common pain point for homelabs and remote‑access setups.
By combining SSH’s built‑in keepalive options, control sockets, and optional tools like autossh, you can keep your sessions alive with minimal manual intervention.
The key is to understand the path, configure keepalives, and pick the right persistence strategy for your workflow.
Feel free to tweak the intervals and persistence times to match your VPN’s stability. The examples above work well on Ubuntu 24.04, Debian 12, and Arch Linux 2026 releases.
See also
- Rootless Podman volumes: how to keep data safe across upgrades without root
- How to Keep a Linux Laptop Connected to a VPN When the Router Switches Between DHCP and Static IPs
- Testing Restic Incremental Backups on an Active MySQL Server Without Downtime
- Fixing the 2‑second DNS lookup lag on my Pi by tweaking resolvectl and systemd‑resolved cache
- Diagnosing a 30‑second Nginx start‑up lag on an 8‑core ARM home‑server