Repository navigation
fix(sshx): keep idle connections alive with SSH keepalives - #162
Merged
Merged
Conversation
Send keepalive@openssh.com on every session's connection and on each jump host's, independent of the server monitor, every keepAliveSeconds (30 by default, 0 off, in Options > General). Any answer counts; after three unanswered in a row the client is closed, so the session shows "Connection lost" instead of looking connected. The monitor's stats script now gives up after 10 seconds, so a hanging script no longer stops the monitor loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nMqJHsqtndDSqJDPp1qYv
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
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.
Summary
Until now the server monitor sent the app's only keepalive. With the host's exec channel off, through a jump host, or after the stats script hung, a quiet session sent nothing, and nothing noticed when a connection died without a word. This PR adds a keepalive to every SSH connection that does not depend on the monitor.
sshx.KeepAlive(ctx, client, KeepAliveConfig{Interval, Timeout, MaxMissed})sendskeepalive@openssh.comwith want-reply every interval. Any answer counts, success or failure. Each wait has its own timeout, run in a goroutine, becauseSendRequesthas no timeout. While one request is still waiting, no new one is sent: global requests wait for their answers one at a time anyway. AfterMaxMissed(3) misses in a row it closes the client. The loop ends when ctx ends or the client closes (client.Wait).sshx.Options.KeepAlive:Dialstarts the keepalive on the client it returns, anddialJumpgives the same config to each jump host's client. A jump host whose keepalive closes it takes down the host's connection through it. With an HTTP or SOCKS proxy, the host's own keepalive crosses the proxy.OnExitsetsstatusClosed/errConnectionLost, so the session shows "Connection lost" and Reconnect works. The test checks this path. No new status code was needed.keepAliveSeconds(Options › General › Connections, "Send a keepalive every N seconds", 0 to 3600). Each wait times out after min(interval, 15 s).intwith a sentinel value. Missing or0means the default of 30, and the options' 0 ("off") is stored as-1. This follows the repo's existing rule that zero means default (wheelLines,termFontSize,sync.interval). It keepsSettingsa comparable value (tests compare with==, and a*intwould compare addresses). It also keeps the sync rule that a setting at its default has no key. The downside is that a hand-written0in settings.json means 30.configuration.mdsays so.syncedSettings, like the other behavior settings.monitor.Run:runOutputnow gives up after 10 s. It closes that exec session and keeps the connection, so a stuckdfno longer stops the monitor loop.internal/sshtest: counts keepalives (KeepAlives), can stop answering them (StopAnswering), and can accept exec requests that never finish (HangExec).keepAliveSeconds), in both languages.CHANGELOG.md only takes version sections, so here is a ready bullet for the next release:
Closes #161
Testing
New tests:
internal/appTestKeepAliveIdleSession: a session tosshtestwith the monitor off gets keepalives at the interval. AfterStopAnswering, the session becomesstatusClosed/ "Connection lost". Fails on main in both parts:0 keepalives, want 3, and, with that step removed,status "Connected" after the server stopped answering.internal/monitorTestRunScriptHangs: withHangExec, the monitor keeps publishing stats. Fails on main:0 stats in 3 s with the script hanging.TestKeepAliveJumpHost(app) andTestDialKeepAliveJumpHost(sshx): keepalives reach both the jump host and the target. In the sshx test, a jump host that stops answering closes the host's client.TestKeepAliveandTestKeepAliveEnds(sshx): the interval, closing after 3 misses, and stopping when ctx ends or the client closes.TestKeepAliveOff: the setting at 0 sends none.TestKeepAliveOption: the Options control, its 30 default, 0 saved as-1, and back to default (nothing saved).TestKeepAliveSetting(config): the mapping and sync.The tests use 10 to 50 ms intervals and add about 1.5 s in total.
Checks:
go fmt ./...changes nothing, andgo build ./...,go vet ./...(alsoGOOS=windowsandGOOS=darwin) andgo tool mygo vetpass.go test -race ./...passes exceptTestArchiveTreeMenu, which also fails in this sandbox on main because it tars the real /tmp. I looked at screenshots of the General page in English and Simplified Chinese.Not tested: a real NAT, firewall or proxy dropping an idle connection, and a real OpenSSH server. The in-process server stands in for both.
Generated by Claude Code