Skip to main content

Module tcp

Module tcp 

Source
Expand description

TCP CONNECT tunnels (RFC 9114 §4.4).

§Close semantics

A CONNECT tunnel is two independent byte streams, and getting their termination right is what makes protocols that half-close (notably older HTTP and some database clients) work through the proxy:

eventreaction
client finishes its sending side (FIN)shut down only the write side of the TCP socket, keep reading from the target
target reaches EOFfinish our sending side, keep reading from the client
target resets or errorsreset the request stream with H3_CONNECT_ERROR, whichever direction noticed
client resets the request stream, or stops reading itclose the TCP connection with a reset, and cancel the direction the client left alone with H3_REQUEST_CANCELLED

The two directions therefore run as independent pumps that are joined, plus a sticky teardown signal for the abnormal cases where one direction failing must stop the other. The signal carries why the tunnel is being torn down — see Teardown below — because the code the pump that did not see the failure has to put on its own half follows from the reason: H3_CONNECT_ERROR when the target failed, H3_REQUEST_CANCELLED when the client cancelled.

Only the last row aborts the TCP connection; see abort_target for why the other three keep their FIN semantics.

§What bounds the waits

While both directions are live nothing here is on a timer, and nothing needs to be: each pump is the other’s watchdog. A client that abandons the request stream is met by the read pump, or – when that pump is parked in a write – by the Reader::reset_by_peer arm beside it; a target that fails is met by whichever pump touches the socket next. Every one of those raises a teardown, and a teardown is what ends the other pump’s wait.

Those two arms cover a client that resets its request stream. A client that merely stops reading the response – STOP_SENDING, which is half of what dropping a request stream sends – is a signal on the other half of the stream, and Writer::stopped in target_to_client is what watches for it. It has to be watched rather than met by a write, because the two things that would otherwise notice can both be absent at once: the client’s own FIN ends the opposite pump without raising a teardown, and a target that has yet to say anything leaves this pump nothing to write (D87).

The first two rows of the table above take that watchdog away, because they are the two endings that end a direction without a teardown: the pump returns, and the surviving direction is left with nobody watching it. A client that finishes its sending side and then stops granting flow-control credit, or a target that reaches EOF and then stops reading, would hold the target socket, its file descriptor and the tunnel slot for as long as the QUIC connection lasts – which keep-alives can make indefinite.

So from the moment one direction has ended cleanly, each write in the surviving direction is bounded by [limits] udp_session_timeout, the same knob a CONNECT-UDP session’s idle bound comes from. Each write gets its own budget, so a client that keeps reading may take hours to drain a download after its FIN. What the budget bounds is the completion of one write rather than progress on it, though: a peer that takes a few bytes every so often, and never enough to finish the write it woke, is cut once that one write has been outstanding for a whole budget. So there is a floor under how slowly a half-closed tunnel may be drained – one relay chunk, up to RELAY_BLOCK_SIZE, per budget on the target -> client side and one of quinn’s ~1.4 KB pieces per budget on the other – which at the default 180 s is orders of magnitude under any real client. A tunnel whose two directions are both still open is untouched however long a write parks, because there the other pump is still the watchdog.

What nothing bounds by time is the surviving direction parked in a read. A target that has taken the client’s FIN may legitimately take minutes to answer, and a bound there would cut the very half-closes this proxy exists to carry, so a half-closed tunnel with no traffic at all on it is deliberately left to run. What limits that is capacity rather than time: max_connections x max_targets_per_conn sockets, the product the fd budget is sized from.

Left to run is not left unwatched, though, and the distinction is the whole of D87: such a tunnel ends the moment the client says it wants no more of it, however quiet the target stays. Only a client that is still waiting for an answer keeps one alive.

Constants§

RELAY_BLOCK_SIZE
The block reads are cut from once the initial 16 KiB is used up.
RELAY_BUF_SIZE
Smallest window read_buf is ever offered on the target → client relay.

Functions§

run
Establishes a TCP tunnel to authority and relays until both directions end.
split_authority
Splits a CONNECT :authority into host and port.