Skip to content

Add client reconnect, Happy Eyeballs, and clean disconnect handling - #32

Merged
guyte149 merged 1 commit into
masterfrom
feat/client-reconnect-and-clean-disconnect
Apr 30, 2026
Merged

Add client reconnect, Happy Eyeballs, and clean disconnect handling#32
guyte149 merged 1 commit into
masterfrom
feat/client-reconnect-and-clean-disconnect

Conversation

@guyte149

Copy link
Copy Markdown
Owner

Reliability and UX overhaul of the client/server lifecycle. Previously the client exited on the first disconnect, blocked v6-first DNS against v4-only servers for the full QUIC handshake timeout, and the server silently leaked reverse-tunnel listeners when clients went away. Now:

  • Client reconnects automatically with exponential backoff (200 ms → 300 s by default, configurable via --max-retry-count and --max-retry-interval, mirroring chisel). The same loop covers initial connect failures so clients started before the server is up keep retrying until it appears.
  • Client races every resolved address with RFC 8305 Happy Eyeballs v2: parse_server_addr collects all candidates and reorders them by alternating address families, the client maintains one quinn endpoint per family lazily, and connect attempts are staggered by the spec-recommended 250 ms Connection Attempt Delay. Fixes the common "macOS localhost resolves to ::1 first, server is on 0.0.0.0" failure mode that previously blocked for ~30 s.
  • Server installs a ^C handler that gracefully closes the QUIC endpoint with reason "server received ^C", so connected clients log the close immediately and proceed into the reconnect loop instead of waiting out the idle timeout.
  • Server's per-connection accept loop decodes every ConnectionError variant into a human-readable INFO log; previously even a clean client shutdown produced zero server-side output (the message was at debug!).
  • Server scopes per-tunnel work to a tokio::task::JoinSet whose shutdown().await fires the moment the connection ends, releasing reverse-tunnel listener ports immediately. New regression test in tests/disconnect_cleanup.rs asserts the listener is rebindable within ~600 ms of a client connection.close().
  • Client briefly waits for quinn to flush the CONNECTION_CLOSE frame before tearing down the endpoint on shutdown, so the server reliably sees the close instead of racing wait_idle.

Public API changes:

  • ClientConfig gains a reconnect: ReconnectConfig field.
  • ServerEndpoint.addr (SocketAddr) is now addrs (Vec) plus a primary() accessor.
  • create_client_endpoint takes the resolved server SocketAddr so it can pick the matching bind family.

Tests:

  • tests/reconnect.rs covers initial-connect-before-server-up and server-restart-mid-session scenarios.
  • tests/disconnect_cleanup.rs is the JoinSet-abort regression.
  • Three unit tests in main.rs for the address-family interleaving.

Reliability and UX overhaul of the client/server lifecycle. Previously
the client exited on the first disconnect, blocked v6-first DNS against
v4-only servers for the full QUIC handshake timeout, and the server
silently leaked reverse-tunnel listeners when clients went away. Now:

* Client reconnects automatically with exponential backoff (200 ms →
  300 s by default, configurable via --max-retry-count and
  --max-retry-interval, mirroring chisel). The same loop covers initial
  connect failures so clients started before the server is up keep
  retrying until it appears.
* Client races every resolved address with RFC 8305 Happy Eyeballs v2:
  parse_server_addr collects all candidates and reorders them by
  alternating address families, the client maintains one quinn endpoint
  per family lazily, and connect attempts are staggered by the
  spec-recommended 250 ms Connection Attempt Delay. Fixes the common
  "macOS localhost resolves to ::1 first, server is on 0.0.0.0" failure
  mode that previously blocked for ~30 s.
* Server installs a ^C handler that gracefully closes the QUIC endpoint
  with reason "server received ^C", so connected clients log the close
  immediately and proceed into the reconnect loop instead of waiting
  out the idle timeout.
* Server's per-connection accept loop decodes every ConnectionError
  variant into a human-readable INFO log; previously even a clean
  client shutdown produced zero server-side output (the message was at
  debug!).
* Server scopes per-tunnel work to a tokio::task::JoinSet whose
  shutdown().await fires the moment the connection ends, releasing
  reverse-tunnel listener ports immediately. New regression test in
  tests/disconnect_cleanup.rs asserts the listener is rebindable
  within ~600 ms of a client connection.close().
* Client briefly waits for quinn to flush the CONNECTION_CLOSE frame
  before tearing down the endpoint on shutdown, so the server reliably
  sees the close instead of racing wait_idle.

Public API changes:
* ClientConfig gains a `reconnect: ReconnectConfig` field.
* ServerEndpoint.addr (SocketAddr) is now addrs (Vec<SocketAddr>) plus
  a primary() accessor.
* create_client_endpoint takes the resolved server SocketAddr so it can
  pick the matching bind family.

Tests:
* tests/reconnect.rs covers initial-connect-before-server-up and
  server-restart-mid-session scenarios.
* tests/disconnect_cleanup.rs is the JoinSet-abort regression.
* Three unit tests in main.rs for the address-family interleaving.

Made-with: Cursor
@guyte149
guyte149 merged commit 2e9bcb7 into master Apr 30, 2026
1 check passed
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