Skip to content

fix(tunnel): add timeout to keepAliveLoop to detect dead connections after sleep/wake#581

Open
ncodespot wants to merge 2 commits into
jpillora:masterfrom
ncodespot:fix/keepalive-timeout-on-sleep-wake
Open

fix(tunnel): add timeout to keepAliveLoop to detect dead connections after sleep/wake#581
ncodespot wants to merge 2 commits into
jpillora:masterfrom
ncodespot:fix/keepalive-timeout-on-sleep-wake

Conversation

@ncodespot

Copy link
Copy Markdown

Problem

After OS sleep/wake, port forwarding becomes unresponsive even with --keepalive set. Root cause: SendRequest("ping") in keepAliveLoop blocks indefinitely on a dead TCP connection. When the system wakes from sleep, the TCP connection is stale but no RST/FIN has been received, so the OS has not yet detected failure. Both the keepalive ping and concurrent OpenChannel calls block until TCP retransmit timeout fires (potentially 15+ minutes).

Fix

Run SendRequest in a goroutine and race it against time.After(KeepAlive). If no pong is received within the keepalive interval, treat it as a dead connection and call sshConn.Close(). This also unblocks any goroutines stuck in OpenChannel on the same connection, triggering reconnection.

With --keepalive 15s, worst-case detection is now ~30s (15s sleep + 15s ping timeout) instead of indefinite.

Tests

  • Unit test (share/tunnel/tunnel_keepalive_test.go): mock ssh.Conn whose SendRequest blocks forever; asserts Close() is called within 2×keepalive.
  • E2E test (test/e2e/keepalive_test.go): freezable TCP proxy between client and server simulates sleep/wake (silent packet drop, no RST); verifies tunnel recovers automatically.

Your Name and others added 2 commits March 10, 2026 10:37
…after sleep/wake

SendRequest blocks indefinitely on a dead TCP connection (e.g. after OS
sleep/wake where no RST/FIN is received). Run it in a goroutine and use
select with time.After(KeepAlive) as a deadline. On timeout, close the
SSH connection so that blocked OpenChannel calls are also unblocked and
the client can reconnect.

Also add unit tests for keepAliveLoop and an E2E test with a freezable
TCP proxy that simulates the sleep/wake scenario end-to-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jpillora added a commit that referenced this pull request Jul 16, 2026
- race keepalive SendRequest against a timer (default: keepalive
  interval, override with CHISEL_PING_TIMEOUT) and close the ssh
  conn on timeout so the client reconnect loop kicks in
- replace the time.Sleep ping loop with a stoppable ticker that
  exits when the connection closes
- add unit tests (mock ssh.Conn) and an e2e blackhole-link test,
  adapted from PR #581 with its broken tcpEcho assertion fixed

Closes md task 1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jpillora added a commit that referenced this pull request Jul 16, 2026
- closed 11 superseded dependabot PRs (#448 #449 #450 #451 #467
  #470 #478 #496 #513 #516 #517) — deps updated via #568/#578
- closed duplicate keepalive PRs #488/#442 with thanks (#581's
  approach won, implemented in task 1)
- closed keepalive issues #445/#560 (fixed by task 1, ships next
  release); #585/#541 were already closed
- left #504/#550/#485 for task 22 and #561/#498 for task 23, as
  those tasks deliver the fixes that close them
- filed task 35: decide dependabot grouped updates vs renovate

Closes md task 21

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant