Skip to content

Propagate TCP tunnel hard errors instead of waiting on QUIC idle timeout - #40

Merged
guyte149 merged 2 commits into
masterfrom
fix/tcp-error-propagation
May 5, 2026
Merged

Propagate TCP tunnel hard errors instead of waiting on QUIC idle timeout#40
guyte149 merged 2 commits into
masterfrom
fix/tcp-error-propagation

Conversation

@guyte149

@guyte149 guyte149 commented May 5, 2026

Copy link
Copy Markdown
Owner

tunnel_tcp_stream ran the two copy directions inside tokio::join! to preserve buffered bytes during graceful half-close, but had no error-path bridge between them: on a hard error in one direction (peer RST, kernel-level disconnect), the failing future returned Err while its SendStream lived on in the outer scope, and the other direction kept blocking on a QUIC read that would never produce data. The function couldn't return until both halves finished, so the local tcp_send write half wasn't dropped either — no FIN to the local socket. Everything sat wedged until QUIC's ~30 s idle timeout reaped the connection.

Fix: keep join! for the graceful path (EOF → shutdown().await, unchanged), but on a hard error in either direction explicitly (1) reset that direction's SendStream so the peer's RecvStream errors immediately, (2) fire a shared Notify so the other local direction's select! exits its blocking read, and (3) the unblocked direction calls tcp_send.shutdown() so the local TCP peer sees a FIN. Both ends run the same code, so the reset chain propagates across the QUIC connection without either side needing originator knowledge. biased ordering on the select! guarantees the Notify waiter is registered before the copy is polled.

Also adds five new integration test files covering proxy semantics that were previously uncovered:

  • tests/half_close.rs — target-initiated half-close (forward), reverse half-close in both directions, SOCKS5 forward half-close.
  • tests/abrupt_close.rs — RST mid-stream from target/originator on forward and reverse paths, plus reverse dead-target. The three RST tests are the regression targets the fix above unblocks.
  • tests/multi_client.rs — two clients sharing one server, plus isolation when one client disconnects.
  • tests/udp_semantics.rs — per-sender response routing, oversized datagram framing, silent target doesn't wedge the tunnel.
  • tests/concurrent.rs — per-connection failure isolation under multiplexing (one stream RST'd, siblings continue cleanly).
  • tests/disconnect_cleanup.rs — reverse UDP socket and reverse SOCKS listener cleanup on client disconnect, mirroring the existing reverse-TCP listener cleanup test.

Suite goes from 109 to 124 tests; all green.

guyte149 and others added 2 commits May 5, 2026 11:55
`tunnel_tcp_stream` ran the two copy directions inside `tokio::join!`
to preserve buffered bytes during graceful half-close, but had no
error-path bridge between them: on a hard error in one direction
(peer RST, kernel-level disconnect), the failing future returned Err
while its `SendStream` lived on in the outer scope, and the other
direction kept blocking on a QUIC read that would never produce
data. The function couldn't return until both halves finished, so
the local `tcp_send` write half wasn't dropped either — no FIN to
the local socket. Everything sat wedged until QUIC's ~30 s idle
timeout reaped the connection.

Fix: keep `join!` for the graceful path (EOF → `shutdown().await`,
unchanged), but on a hard error in either direction explicitly
(1) reset that direction's `SendStream` so the peer's `RecvStream`
errors immediately, (2) fire a shared `Notify` so the other local
direction's `select!` exits its blocking read, and (3) the unblocked
direction calls `tcp_send.shutdown()` so the local TCP peer sees a
FIN. Both ends run the same code, so the reset chain propagates
across the QUIC connection without either side needing originator
knowledge. `biased` ordering on the `select!` guarantees the Notify
waiter is registered before the copy is polled.

Also adds five new integration test files covering proxy semantics
that were previously uncovered:

- tests/half_close.rs — target-initiated half-close (forward),
  reverse half-close in both directions, SOCKS5 forward half-close.
- tests/abrupt_close.rs — RST mid-stream from target/originator on
  forward and reverse paths, plus reverse dead-target. The three
  RST tests are the regression targets the fix above unblocks.
- tests/multi_client.rs — two clients sharing one server, plus
  isolation when one client disconnects.
- tests/udp_semantics.rs — per-sender response routing, oversized
  datagram framing, silent target doesn't wedge the tunnel.
- tests/concurrent.rs — per-connection failure isolation under
  multiplexing (one stream RST'd, siblings continue cleanly).
- tests/disconnect_cleanup.rs — reverse UDP socket and reverse SOCKS
  listener cleanup on client disconnect, mirroring the existing
  reverse-TCP listener cleanup test.

Suite goes from 109 to 124 tests; all green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@guyte149
guyte149 merged commit bd11723 into master May 5, 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