Skip to main content

Context

Struct Context 

Source
pub struct Context {
Show 20 fields pub quic: Connection, pub peer_datagrams: Arc<AtomicBool>, pub auth: Authenticator, pub auth_failures: Mutex<AuthFailures>, pub max_auth_failures: u32, pub authenticated: AuthGate, pub remote: SocketAddr, pub policy: Policy, pub quota: Quota, pub max_streams_bidi: u32, pub tunnels: Arc<AtomicU64>, pub stall_budget: Duration, pub connect_timeout: Option<Duration>, pub resolver: ConnectionResolver, pub ip_family_preference: IpFamilyPreference, pub unanswered_packet_budget: u32, pub unanswered_connection_budget: AtomicU32, pub unanswered_closures: Sampler, pub policy_refusals: Sampler, pub limit_refusals: Sampler, /* private fields */
}
Expand description

Everything a request handler needs from the connection it arrived on.

Built once per connection and held in an Arc, so sharing it with a request costs one refcount bump rather than a copy of eighteen fields. Deliberately not Clone: a second whole Context would be a second view of the same connection and nothing else. That one Arc is also the only sharing the members need, so they are stored inline. The two that keep an Arc of their own, peer_datagrams and tunnels, do so because something outside this connection’s request handling holds them as well.

The UDP-specific members are here rather than in udp because a connection owns them regardless of which tunnel type ends up using them: both are settled at the handshake, before the first request arrives.

Fields§

§quic: Connection

The QUIC connection this request arrived on.

Most uses are a datagram being sent, and only the sending half: an inbound datagram is routed to the request stream it names by the HTTP/3 connection, and reaches a session through the crate::h3api::DatagramReceiver that stream handed it (D79). The rest are connection-wide decisions a request can reach — closing after repeated authentication failures, and raising the stream allowance once one succeeds (Context::mark_authenticated) — which is why the handle is named for what it is rather than for what it mostly does.

§peer_datagrams: Arc<AtomicBool>

Whether the peer advertised SETTINGS_H3_DATAGRAM = 1.

RFC 9297 §2.1.1 forbids sending QUIC datagrams when this is false; such sessions fall back to DATAGRAM capsules on the request stream. Shared with the HTTP/3 connection rather than copied from it, for the reason crate::h3::connection gives.

§auth: Authenticator

The credentials every request is checked against.

§auth_failures: Mutex<AuthFailures>

Authentication failures seen on this connection so far, bucketed by who they were aimed at.

Connection-scoped on purpose: no shared table across connections means no eviction policy and no memory that an attacker can grow. The lock is a std::sync::Mutex, held for a bucket lookup and a sum and never across an await; it is here rather than an atomic because the buckets and the total taken from them have to move together.

§max_auth_failures: u32

Failures tolerated before the connection is closed. Zero disables it.

§authenticated: AuthGate

Whether any request on this connection has passed the credentials check.

Set once and never cleared. It is what lifts D76’s bound on how long a connection may go without sending a request: a client that has proved who it is may hold an idle connection for as long as the transport’s own idle timeout allows, which is what makes a proxy worth reconnecting to. A server with no users configured has nothing to check, so its first request sets this too – the flag means “this peer got past the door”, not “credentials were seen”.

Its transition is load-bearing as well as its value: the false-to-true edge is what raises this connection’s bidirectional stream allowance to the configured one, exactly once — which is why it is an AuthGate and not the bare atomic it wraps. See Context::mark_authenticated.

Owned by crate::quic rather than allocated here, because it is read from outside the connection as well: the accept loop needs to know which of the connections it is holding has never got past the door, so that a full server can take that slot back rather than refuse a client that has credentials.

§remote: SocketAddr

The peer’s address, for logs that a fail2ban rule can act on.

§policy: Policy

Which destinations this proxy may reach.

§quota: Quota

How many tunnels this connection may hold open at once.

§max_streams_bidi: u32

The bidirectional stream allowance this connection is granted once it has authenticated: [limits] max_streams_bidi, in full.

It is not what the handshake advertised. A connection is accepted on the small allowance quic::INITIAL_BIDI_STREAMS describes and raised to this by Context::mark_authenticated, so the number is carried here for the one moment it is needed — snapshotted with the rest of this connection’s [limits], so a reload changes what connections accepted from then on are worth and leaves a running one alone.

§tunnels: Arc<AtomicU64>

How many tunnels this connection has been granted a slot for so far.

Counted once per request that gets past Quota::acquire, TCP and CONNECT-UDP alike, so this is slots taken rather than requests made: a request refused before the slot — 407, a malformed message, the tunnel limit itself — never reaches it. It is not tunnels that carried anything, though: the slot is taken before the target is judged, so a destination the policy rejects and a target that cannot be reached are both counted here. Owned by crate::quic, which reads it once when the connection ends to report it on the closing line (D72); nothing here ever reads it back.

§stall_budget: Duration

The longest one tunnel may make no progress before it is given up on.

Both tunnel kinds spend it, which is why it is not named for either: a CONNECT-UDP session that goes this long without a packet in either direction is reclaimed, and once one direction of a TCP tunnel has ended cleanly, each write in the surviving direction has this long to complete (tcp module docs). Both come from [limits] udp_session_timeout, whose name is the CONNECT-UDP half alone for compatibility – the field documents the other half.

§connect_timeout: Option<Duration>

Budget for reaching a target, or None when it is disabled.

Spent twice per request and separately — once on name resolution, once on the whole list of addresses it resolved to — so the worst case a tunnel slot is held before any byte flows is twice this.

§resolver: ConnectionResolver

This connection’s share of the blocking pool, spent on name resolution.

Per connection rather than per request, because what it bounds is a connection: one reserved lookup slot nobody can take away plus a capped draw on the server-wide allowance, so a client whose targets never resolve can neither park the whole pool nor stop anyone else’s names from resolving (D90).

§ip_family_preference: IpFamilyPreference

Which address family a resolved target is tried on first.

Snapshotted with the rest of the connection’s [limits], so a reload changes what connections accepted from then on do and leaves a running one alone.

§unanswered_packet_budget: u32

Packets a UDP session may send before its target has answered.

§unanswered_connection_budget: AtomicU32

Packets this connection may still send towards targets that have never answered, across every session it opens.

The per-session budget above is what RFC 9298 §7 asks for and it is recreated by opening a session, which is free: a client that spends one session’s allowance closes the stream and opens another, and the mitigation becomes a constant factor rather than a bound. Stream turnover is not bounded by anything (max_streams_bidi bounds how many are open at once and quinn issues fresh credit as streams complete), so this is the counter an attacker cannot recreate: it is created with the connection and it is only ever spent.

A session whose target answers repays what that session spent, and nothing else does (D84, the 2026-09-05 addendum). The repayment is that session’s own charged_unanswered count and it is capped at the total the connection was created with, so no sequence of sessions can lift the counter above where it started. The objection this rule had to answer, a client keeping one consenting target on the side and buying back packet for packet the allowance it is spending on a silent one, does not apply to an own-session refund: a silent session’s spend stays spent, and the bound is unchanged for what it was written for, packets to targets that never answer.

A connection that spends the total with nothing outstanding to repay it keeps it spent for its own life, and a client that wants another allowance pays for another handshake (D84, the 2026-09-07 addendum).

Zero means uncapped, the same way unanswered_packet_budget = 0 does: the operator switched the mitigation off.

§unanswered_closures: Sampler

How many sessions this connection has lost to that total, and when that is worth saying out loud again.

One line per closed session would be one line per stream a peer opens, which is the flood crate::logfmt::Sampler exists to stop.

§policy_refusals: Sampler

How many destinations this connection’s policy has refused, and when that is worth saying out loud again.

The warning is deliberately loud (D44 left it at WARN while demoting the blackhole line beside it, because a client probing loopback through the proxy is what SSRF looks like from here) and deliberately cheap for a peer to provoke: an IP literal takes no resolver slot, opens no socket, and holds its tunnel slot only for as long as the refusal takes. One warning per request would let a peer decide how much of this host’s journal is left for anybody else. See crate::logfmt::Sampler for the schedule and for why silencing the repeats was not the answer.

§limit_refusals: Sampler

The same, for requests refused because this connection is already holding every tunnel it may.

Reaching the limit costs a peer max_targets_per_conn live tunnels, which is real; staying there costs it nothing at all, and every further request was a warning for as long as it cared to keep asking.

Implementations§

Source§

impl Context

Source

pub fn new( config: &Config, quic: Connection, peer_datagrams: Arc<AtomicBool>, resolver: &ResolverBudget, tunnels: Arc<AtomicU64>, authenticated: AuthGate, ) -> Self

Builds the context for one accepted connection.

peer_datagrams comes from the HTTP/3 connection, which keeps writing to it; tunnels is the connection’s tunnel counter and authenticated its door flag, both created by crate::quic so that they outlive this context — the first is read once the connection is over, the second while it runs.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more