Repository navigation
Keep port 2010 working through reconnects: notice dead sessions, retry the forward, probe it - #69
Merged
Merged
Conversation
…y the forward, probe it Measured with the released 5.0 jar against real tunnel VMs: after a NAT or IP change on the customer side, port 2010 stayed dead until the client was restarted. - Nothing read-side timed out. The 30-second keepalive only writes, so a dead session looked connected until the OS gave up retransmitting (~15 minutes on Linux). JSch's ServerAliveInterval (5 s x 3) now drops it after ~15 s; the connection check runs every 2 s instead of 10, and the first reconnect goes out after 1 s instead of 5. - The tunnel server still holds 2010 for the session being replaced, so the reconnect's -R 2010 is refused. That used to tear the healthy session down and re-dial it every 5 s for 30 rounds, then rebuild. The forward is now requested again on the same session every 2 s, with a rebuild only after ~2 minutes. - After a rebuild (or at startup) a refused forward was never requested again: the self-test retry returned early while not forwarding, and the forwarding monitor only repaired the local forward. The monitor now re-requests a missing reverse forward on every check and tells the app, which re-runs the self-test so readiness follows. - verifyReverseForwardDelivery() only proved the local end. A new probe opens direct-tcpip to the server's own 127.0.0.1:2010, where squid and the devices connect, and sends a request that only this client's proxy answers with the probe's nonce (ForwardProbeHandler, ahead of the HTTP log). Two failures in a row re-register the forward. Servers whose sshd does not permit the probe are detected (it never succeeds on the session) and ignored. - The first self-test retry is after 2 s instead of 30: the usual startup failure is a race that is over within a second. With the tunnel servers ending a customer's stale session at login (ansible tunnel02, one-session.sh), 2010 works again 24 s after the network change, on the first reconnect; it was 136 s at best, and indefinitely in practice.
This was referenced Oct 24, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
A customer reported that port 2010 "stopped working suddenly". Reproduced end to end with the released 5.0 jar against clones of the real tunnel VM images (22.04 production image and the new 26.04 one), with a fake API, the customer's own sites behind the client, and devices going through the VM:
KeepAliveTaskonly writes; nothing read-side times out (noServerAliveInterval), so the session looked connected until the OS gave up retransmitting (~15 min on Linux). Meanwhile every device request through 2010 failed.-R 2010is refused, because the tunnel server still holds 2010 for the session being replaced (sshd'sClientAlivenever fires on it while devices keep hitting its listener).finishReconnect()answered that by tearing down the healthy new session and re-dialling every 5 s for 30 rounds (each also waiting up to 10 s for the tcpip-forward reply), then rebuilding the tunnel.tunnelReady()→ forward refused →scheduleSelfTestRetry(), whose task returns early while!tunnelIsForwarding()— forever, without re-requesting the forward; andPortForwardingMonitorTaskonly repairs the local forward. SSH up, Selenium relay working,/readyz503, 2010 dead until the customer restarted the client. On the production image (no server-side changes) this was the observed end state: still broken after 25 minutes.verifyReverseForwardDelivery()connects to the local jetty port only.What changed
SSHTunnel.connect():setServerAliveInterval(5000)+setServerAliveCountMax(3)— a dead session is dropped after ~15 s. Connection check every 2 s (was 10), forwarding check every 5 s (was 15).CustomConnectionMonitor: first reconnect after 1 s (was 5). When the session is up but the forward failed, it re-requests the forwards on the same session every 2 s (ReconnectableTunnel.restartPortForwarding()), and rebuilds only after ~2 min (MAX_FORWARD_RETRIES). A session that drops meanwhile goes back to a normal reconnect.PortForwardingMonitorTask→checkRemoteForward(): re-requests a reverse forward that is not established (startup / after a rebuild), and probes the server end:probeReverseForward()opens adirect-tcpipchannel to the server's own127.0.0.1:2010and sends a request that only this client's proxy answers with the probe's nonce (newForwardProbeHandler, ahead of the HTTP log so probes are not logged). Two consecutive failures re-register the forward. A server whose sshd does not permit the probe is detected (the probe never succeeds on that session) and ignored, so older tunnel images behave as before. On success after a repair,App.reverseForwardRestored()re-runs the self-test so readiness follows.App: first self-test retry after 2 s, then every 30 s as before.Server side (testingbot/ansible, tunnel02 image): a PAM session hook ends a customer's older sessions at login, so 2010 is free when the reconnect asks for it;
PermitOpenallows the probe;tcp_retries2=8.Tests
mvn test: 1011 tests, 0 failures (main: 1005). New:SSHTunnelEmbeddedServerTest(probe round trip through a real reverse forward on the embedded MINA server; probe fails without a forward; a forward refused because the port is held is re-requested by the monitor and the app is told),CustomConnectionMonitorTest(refused forward retried on the same session without re-dialling; session drop during that goes back to reconnect; quick first reconnect; rebuild afterMAX_FORWARD_RETRIES).tunnel-harness/), NAT change with the old flow blackholed on both ends, measured until 2010 answers again: