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:
| event | reaction |
|---|---|
| client finishes its sending side (FIN) | shut down only the write side of the TCP socket, keep reading from the target |
| target reaches EOF | finish our sending side, keep reading from the client |
| target resets or errors | reset the request stream with H3_CONNECT_ERROR, whichever direction noticed |
| client resets the request stream, or stops reading it | close 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_bufis ever offered on the target → client relay.
Functions§
- run
- Establishes a TCP tunnel to
authorityand relays until both directions end. - split_
authority - Splits a CONNECT
:authorityinto host and port.