Skip to main content

Module control

Module control 

Source
Expand description

The control plane — a live handle onto a running proxy.

Everything else in this crate is configured before it starts: a ListenerConfig is consumed when the endpoint is bound, a ProxySessionConfig is copied once per accepted connection, and a ProxyHook declares its interest once at session start. That is deliberate — it is what makes a run reproducible from the values that started it — but it leaves no way to ask a proxy that is already running anything at all.

ProxyControl is that way. It is obtained from TransparentProxy::control before the accept loop is awaited, because the caller that owns the proxy is usually the caller that is about to give up its thread of control to run(); a handle that could only be taken afterwards could not be taken at all. Everything it reports is therefore defined for a proxy that has not bound yet: ProxyControl::local_addr answers ProxyError::NotBound until the listener exists, and ProxyControl::sessions answers an empty list.

§Two pieces of shared state, both released by a guard

The handle reads two things the proxy used to keep as stack locals of run(): the bound listener, and the set of sessions that are live right now. Both are published into this module when they come into existence and removed when they go, and in both cases the removal is a Drop rather than a call at each exit site.

That choice is the whole correctness argument for ProxyControl::sessions. A session ends for at least five distinct reasons — a forwarding task returned, a forwarding task errored, the peer went away, a hook asked for a close, the proxy was cancelled — and a sixth that no enumeration covers, the session’s whole future being dropped by whoever spawned it. A list of removal sites is only ever as complete as the reader who wrote it, and the failure it produces is silent: sessions() keeps naming an id nothing can act on, and every later call that takes that id fails in a way that looks like a race. Drop is complete by construction, so the registration is handed out as a SessionGuard and lives in the frame of the function that runs the session.

A third piece is held here and released by nothing: the proxy-wide shaping counters behind ProxyControl::stats. They are the one report in this module that is cumulative rather than instantaneous, and that is exactly why they cannot be assembled from the two above. A total summed over the session list would fall every time a client disconnected, because the list is released by a Drop and a session that has ended leaves nothing behind — and a statistic that goes down under normal operation cannot be alerted on. So each session is handed the counters when it attaches, charges them as it forwards, and leaves them behind when it goes.

§Reaching a session that is already running

Registering an id is enough to list a session but not to act on one. Acting needs the session’s cancellation token, its close request slot, its stream registry, its two control-stream inboxes and its egress knobs — and every one of those is built by the session before it dials the upstream relay, which is what lets the whole entry go into the registry at the top of the run function. A session spends its longest single operation connecting, sometimes for as long as its connect timeout allows and sometimes forever; one that only became reachable afterwards would be unreachable for exactly that long.

What is deliberately not in an entry is either connection. The two Transports are owned by the forwarding scope, and a table scanned by every list call has no business holding them alive past the session that owns them.

The channels are created with the session rather than attached later because a task attached after the fact could never cover the sessions that were already running when it was attached — which is precisely the set a control plane exists to reach.

§Three levels of reach, and why they are not one mechanism

ProxyControl::close_session acts on the whole session, so it goes to the session’s own command task: it is the only request that has to wait for something — the bounded egress drain — and a waiting request needs somewhere to wait that is not the caller’s thread.

ProxyControl::reset_stream and ProxyControl::inject_control act on one stream, and neither waits. They resolve in the caller: the registry entry carries the session’s StreamRegistry, so a key that names nothing live is refused synchronously, and a key that names a live stream is reached by dropping a StreamCommand into that stream’s own inbox. Routing them through the session task as well would have added a hop and a second place for a request to be lost, and would have made there is no such stream an answer that arrives asynchronously — which a synchronous method signature cannot deliver.

Nothing here accepts a request and discards it. Every method either delivers to a task that will act on it or answers a ControlError saying why it could not.

§Reconfiguring, and the one verb that cannot reach a live connection

Three of the requests here change what the proxy is rather than what a particular session is doing, and they reach three different distances. The distances are not a matter of how they were written; they are what the thing being changed will admit.

ProxyControl::set_impair reaches traffic already in flight. It acts below QUIC, on the datagrams a leg’s socket is about to pass, and the next one out carries the new profile.

ProxyControl::set_shape and ProxyControl::set_shaper_enabled reach a running session. The switch is read on the next release decision a queue makes, which for a stream held by a bucket that will refill is within one pacing interval and for a stream held by a bucket configured at zero is not until the max_hold clamp — see the method for why nothing here can do better. A new profile is taken up by a running session at its next stream, because a stream’s egress queue holds the scheduler its units were admitted under and a class is an index into that scheduler’s class list.

ProxyControl::set_transport reaches no connection that already exists, ever. A QUIC connection takes its transport configuration once, at setup, and keeps it for life — quinn offers four setters on a live connection and no way to replace the configuration behind it. So a profile set on the relay leg reaches the next connection this proxy opens, which is the next session it accepts, and a profile set on the client leg reaches the next connection it accepts, which the proxy does not initiate and which may never arrive. On a proxy that never dials or accepts again — every client already connected, nothing new coming — the call is a permanent silent no-op that returns Ok(()), and no return value here distinguishes that from a setting that reached everything afterwards. It is written out on the method, in this module, and in the crate documentation, in those words, because a caller who reads it as “set the window” and measures the session in front of them will measure the old window and believe the new one.

SessionGuard, StreamRegistry and StreamCommand are crate-internal, and set_impair and clear_impair exist only under the impair feature. This page is public and is built without that feature, so a link to either kind resolves to nothing — and an unresolved intra-doc link is an error under the documentation build, not a warning. Both are written in plain code font instead, so the prose can still name the thing it is describing without breaking that build. Turning one back into a link is what reddens it.

Structs§

AbortOnDrop 🔒
A spawned task that is aborted when this value is dropped.
BoundGuard 🔒
The published bound listener’s registration. Un-publishes on drop.
ClientLeg 🔒
The client-facing leg’s two mutable facts, under one lock.
ControlAttachment 🔒
A session’s attachment to its proxy’s control plane.
ControlLeg 🔒
The two ends of one control-stream direction’s request channel.
ControlPlane 🔒
The state a ProxyControl reads, owned by the proxy and shared with every handle it hands out.
LegSetup 🔒
What the control plane knows about the two legs from the configuration the proxy was built with, and cannot learn any other way.
ProxyControl
A handle onto a running proxy.
SessionGuard 🔒
One live session’s registration. Ends it on drop.
SessionHandle 🔒
What the control plane keeps for one live session.
ShapeControl 🔒
This proxy’s shaping profile, and whether it paces.
StreamEntry 🔒
What the registry keeps for one live stream.
StreamGuard 🔒
One live stream’s registration. Ends it on drop.
StreamRegistry 🔒
Every forwarded stream that is still live, and the Gate each one releases when it ends.

Enums§

ControlError
Why a control-plane request could not be carried out.
SessionCommand 🔒
A request delivered to one session’s command task.
StreamCommand 🔒
A request delivered to the task that owns one forwarded stream.

Constants§

COMMAND_QUEUE_DEPTH 🔒
How many commands a session’s inbox holds before a sender has to wait.