pub struct SessionManager {
pub max_connections: usize,
pub nb_connections: usize,
pub can_accept: bool,
pub slab: Slab<Rc<RefCell<dyn ProxySession>>>,
pub max_connections_per_ip: u64,
pub retry_after: u32,
pub max_connections_per_subnet: u64,
pub subnet_ipv4_prefix: u32,
pub subnet_ipv6_prefix: u32,
pub max_connection_attempts: u32,
/* private fields */
}Fields§
§max_connections: usize§nb_connections: usize§can_accept: bool§slab: Slab<Rc<RefCell<dyn ProxySession>>>§max_connections_per_ip: u64Default per-(cluster, source-IP) connection limit. 0 disables
the feature; cluster-level overrides take precedence at check
time.
retry_after: u32Default Retry-After header value (seconds) for HTTP 429
responses emitted on per-(cluster, source-IP) limit hit. 0
omits the header.
max_connections_per_subnet: u64Default per-(cluster, source-SUBNET) connection limit. 0
disables the subnet limiter, which is the default; cluster-level
overrides take precedence at check time.
This is a SECOND, INDEPENDENT counter standing beside
max_connections_per_ip, not a replacement for it. The per-IP
cap serves the API-gateway case — one abusive client, one
address. The per-subnet cap serves the denial-of-service case,
where an attacker holding a routed prefix (an ordinary
residential IPv6 /64) defeats a per-address counter outright
by taking a fresh address for every connection
(sozu-proxy/sozu#1270). Both gates are consulted and BOTH must
admit, so “10 per IP AND 100 per /64” is expressible.
subnet_ipv4_prefix: u32IPv4 prefix length, in bits, defining a “subnet” for the counter above. Boot-time only: changing a mask would re-key every live counter, so there is no runtime setter for it. 32 masks nothing.
subnet_ipv6_prefix: u32IPv6 prefix length, in bits. Boot-time only, as above. 128 masks nothing.
max_connection_attempts: u32Default backend connection attempt budget of a request, the first
attempt included: ServerConfig::max_connection_attempts, changed at
runtime by SetMaxConnectionAttempts. A cluster’s own
max_connection_attempts takes precedence
(Self::effective_max_connection_attempts). Validated, so between
1 and 255.
Implementations§
Source§impl SessionManager
impl SessionManager
pub fn new( slab: Slab<Rc<RefCell<dyn ProxySession>>>, max_connections: usize, max_connections_per_ip: u64, retry_after: u32, max_connections_per_subnet: u64, subnet_ipv4_prefix: u32, subnet_ipv6_prefix: u32, ) -> Rc<RefCell<Self>>
Sourcepub fn effective_max_connection_attempts(
&self,
override_value: Option<u32>,
) -> u32
pub fn effective_max_connection_attempts( &self, override_value: Option<u32>, ) -> u32
Resolve the backend connection attempt budget of a request routed to
a cluster whose own setting is override_value: the cluster’s value
when it sets one, the global default otherwise (sozu-proxy/sozu#1800).
Sourcepub fn effective_max_connections_per_ip(
&self,
override_value: Option<u64>,
) -> u64
pub fn effective_max_connections_per_ip( &self, override_value: Option<u64>, ) -> u64
Resolve the effective per-(cluster, source-IP) limit. override_value
is the cluster-level setting from the proto Cluster message:
None inherits the global default, Some(0) is explicit
“unlimited”, Some(n > 0) overrides.
Sourcepub fn effective_retry_after(&self, override_value: Option<u32>) -> u32
pub fn effective_retry_after(&self, override_value: Option<u32>) -> u32
Resolve the effective Retry-After header value. Some(0) (or
the global default of 0) signals “omit the header” — caller
must skip emission rather than render Retry-After: 0.
Sourcepub fn effective_max_connections_per_subnet(
&self,
override_value: Option<u64>,
) -> u64
pub fn effective_max_connections_per_subnet( &self, override_value: Option<u64>, ) -> u64
Resolve the effective per-(cluster, source-SUBNET) limit. Same
three-state contract as effective_max_connections_per_ip:
None inherits the global default, Some(0) is explicit
“unlimited”, Some(n > 0) overrides.
Sourcepub fn subnet_key(&self, ip: &IpAddr) -> IpAddr
pub fn subnet_key(&self, ip: &IpAddr) -> IpAddr
Derive the subnet counter key for ip under this manager’s
configured prefixes. Allocation-free — IpAddr is Copy.
Sourcepub fn cluster_subnet_at_limit(
&self,
token: Token,
cluster_id: &str,
ip: &IpAddr,
override_value: Option<u64>,
) -> bool
pub fn cluster_subnet_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, override_value: Option<u64>, ) -> bool
Returns true when admitting token to one more connection for
(cluster, subnet_of(ip)) would exceed the resolved subnet
limit. Exact mirror of cluster_ip_at_limit on the subnet
counter: 0 is unlimited, and a token already holding a slot
for this (cluster, subnet) is never at the limit.
The subnet key is derived only AFTER the limit == 0 early
return, so a deployment that never enables this feature pays no
masking work on the hot path at all.
Sourcepub fn cluster_connection_at_limit(
&self,
token: Token,
cluster_id: &str,
ip: &IpAddr,
ip_override: Option<u64>,
subnet_override: Option<u64>,
) -> bool
pub fn cluster_connection_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, ip_override: Option<u64>, subnet_override: Option<u64>, ) -> bool
THE gate. Returns true when EITHER the per-IP cap or the
per-subnet cap would be exceeded by admitting token to one more
connection for cluster_id from ip.
Both enforcement points — the H1+H2 mux router and raw TCP — call this rather than the two halves, so the conjunction “admit only if BOTH caps allow it” is stated once and cannot drift between protocols. The per-IP half is evaluated first and is byte-for-byte the pre-existing check; the subnet half is the single line this feature adds to the admission path.
Sourcepub fn cluster_ip_at_limit(
&self,
token: Token,
cluster_id: &str,
ip: &IpAddr,
override_value: Option<u64>,
) -> bool
pub fn cluster_ip_at_limit( &self, token: Token, cluster_id: &str, ip: &IpAddr, override_value: Option<u64>, ) -> bool
Returns true when admitting token to one more connection for
(cluster, ip) would exceed the resolved limit. 0 is treated
as unlimited. A token that already holds a slot for this
(cluster, ip) is NEVER at the limit — H2 sessions multiplex
many streams to the same cluster on a single connection, and
the limit governs distinct frontend connections, not streams.
Hot-path: called for every cluster-resolving request from
mux/router::plan_connect. The nested-map storage lets both lookups
borrow cluster_id and ip; no per-call allocation runs here
in steady state.
Sourcepub fn track_cluster_ip(
&mut self,
token: Token,
cluster_id: ClusterId,
ip: IpAddr,
)
pub fn track_cluster_ip( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, )
Account token’s active connection against (cluster, ip).
Idempotent within a token: a second call for the same
(cluster, ip) is a no-op so H2 retries / multi-stream opens
to the same cluster do not double-count.
Copies no cluster id: entry(cluster_id.clone()) only bumps the
ClusterId count. A first (token, cluster) observation
allocates the new slot’s IP set; later IPs under it reuse the
set, and a repeat call allocates nothing.
── A FAILED backend dial keeps the slot, and that is deliberate ──
Router::plan_connect (lib/src/protocol/mux/router.rs) claims
here, after the Self::cluster_ip_at_limit gate but BEFORE
backend selection and the dial. Six fallible exits follow the
claim and none of them releases. Since sozu-proxy/sozu#1519 they
sit in two functions rather than one: Router::plan_connect
keeps the two pool-reuse refusals, and Mux::dial_backend with the
Mux::attach_dialed it calls (lib/src/protocol/mux/mod.rs) own the
four on the fresh-dial
path — backend selection, buffer-pool exhaustion, the new
connection refusing the stream, and the L7Proxy::register_socket
rollback, which restores its gauges and slab entry but not the
slot. The split moved no ordering: the claim still precedes all
six. Self::untrack_all_cluster_ip is the only release, and
session teardown is its only caller — the
L7Proxy::remove_session impls on HttpProxy / HttpsProxy for
the mux, TcpSession::close for raw TCP.
An unpaired claim reads like an oversight; it is not one, and
sozu-proxy/sozu#1521 is where the reasoning was measured rather
than assumed. Do not pair a release with those exits as part of a
refactor — it would LOOSEN a connection limit under exactly the
conditions a client can induce.
What makes the unpaired claim cheap on H1 is that a failed dial ENDS the frontend connection, so the slot returns with it:
Mux::ready_innerjoins the plan and the dial into a singleResultwithand_then, so every post-claim exit from either function reaches the sameBackendConnectionErrorarm of its pending-link loop, which answers the stream from the template registry instead of retrying. No arm there returnsSessionResult::Closeof its own.- Every built-in template in
lib/src/protocol/kawa_h1/answers.rscarriesConnection: close, soset_default_answer_with_retry_after(lib/src/protocol/mux/answers.rs) clearsHttpContext::keep_alive_frontend. On H1 that one bit is what closes the connection; H2 does not read it. - H1: once the answer flushes, the response-complete branch of
ConnectionH1::writable(lib/src/protocol/mux/h1.rs) takesConnectionH1::defer_close_for_tls_flushinstead of the keep-alive reset, so the session closes after that one flush. - H2: the answer ends its own stream only (RFC 9113 §8.2.2:
Connectionis not an H2 field, sozu-proxy/sozu#1740). The connection stays open, so the slot is held until the client closes it or it times out. - TCP never reaches an answer at all: any error out of
TcpSession::connect_to_backend(lib/src/tcp.rs) becomesSessionResult::Closein the same event-loop pass. - A registered,
Normalbackend that is down may not fail on the dial at all. The dial —Mux::dial_backend(lib/src/protocol/mux/mod.rs) for HTTP,Backend::try_connect(lib/src/backends.rs) for TCP — is non-blocking: a refusal the kernel reports synchronously surfaces asBackendError::ConnectionFailures, which the mux re-queues for another backend, while anEINPROGRESSconnect returnsOkand its refusal (or itsconnect_timeout) arrives later as a backend failure, so the stream retries throughEndStreamAction::Reconnect. The re-track is the no-op above, so one slot — not one per attempt — is held for at mostmax_connection_attemptsattempts (sozu-proxy/sozu#1800).
The window IS the connection’s whole lifetime in two cases: any
H2 connection, and an H1 connection answered by an operator
template that omits Connection: close (the opt-out
doc/rate-limit-design.md records for the 429 template). Either
holds its one slot per (connection, cluster) until the client goes
away, as a connection whose dial succeeded does.
Sourcepub fn track_cluster_subnet(
&mut self,
token: Token,
cluster_id: ClusterId,
ip: IpAddr,
override_value: Option<u64>,
)
pub fn track_cluster_subnet( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, override_value: Option<u64>, )
Account token’s active connection against
(cluster, subnet_of(ip)). Mirror of track_cluster_ip on the
subnet counter, with one deliberate difference: it is a NO-OP
while the resolved subnet limit is 0.
That conditional is what keeps the default genuinely free. The
per-IP index is populated unconditionally, so an operator who
never enables the subnet cap would otherwise pay a second hash,
a second allocation and a second per-session entry forever for a
feature they do not use. The cost is that enabling the cap at
runtime does not retroactively count sessions already in flight;
the counter converges as those sessions close. The per-IP path
has the same shape of gap after clear_cluster_ip_tracking.
Sourcepub fn track_cluster_connection(
&mut self,
token: Token,
cluster_id: ClusterId,
ip: IpAddr,
subnet_override: Option<u64>,
)
pub fn track_cluster_connection( &mut self, token: Token, cluster_id: ClusterId, ip: IpAddr, subnet_override: Option<u64>, )
Account token’s active connection against BOTH counters. The
companion of cluster_connection_at_limit: the two enforcement
points take the gate and the slot through one call each, so the
per-IP and per-subnet bookkeeping can never fall out of step
between the mux and raw TCP.
Sourcepub fn untrack_all_cluster_ip(&mut self, token: Token)
pub fn untrack_all_cluster_ip(&mut self, token: Token)
Drain every slot held by token — per-IP AND per-subnet — and
apply the matching decrements. Called on session teardown only —
there is no per-stream untrack because the limit is
per-connection, not per-stream. Removes empty inner maps so the
outer connections_per_cluster_ip does not retain
(cluster_id, empty_map) orphans across cluster lifetimes.
The name is historical: this is the SINGLE release point every
protocol already calls on close (HttpSession, HttpsSession,
TcpSession), so the subnet counter is drained from here rather
than from a second call each of those three sites would have to
remember. A missed release leaks a slot until worker restart,
which is exactly the failure a second entry point would invite.
Sourcepub fn subnet_tracking_is_empty(&self) -> bool
pub fn subnet_tracking_is_empty(&self) -> bool
Whether both halves of the subnet accounting are empty.
Exists so a test can assert that a DISABLED subnet limiter leaves no bookkeeping behind at all — “inert” and “free” are different claims, and only the second one is checkable from outside.
Sourcepub fn clear_cluster_subnet_tracking(&mut self)
pub fn clear_cluster_subnet_tracking(&mut self)
Wipe every per-(cluster, source-SUBNET) accounting bucket.
Called by the runtime SetMaxConnectionsPerSubnet(0) path.
Deliberately does NOT touch the per-IP buckets: the two limiters
are independent, and disabling one must not silently reset the
other’s live counters.
Sourcepub fn clear_cluster_ip_tracking(&mut self)
pub fn clear_cluster_ip_tracking(&mut self)
Wipe every per-(cluster, source-IP) accounting bucket. Called by
the runtime SetMaxConnectionsPerIp(0) path so disabling the
feature does not leave dead bookkeeping behind that a future
re-enable would consult.
Sourcepub fn at_capacity(&self) -> bool
pub fn at_capacity(&self) -> bool
The slab is considered at capacity if it contains more sessions than twice max_connections
Sourcepub fn accept_slab_threshold(&self) -> usize
pub fn accept_slab_threshold(&self) -> usize
The slab fill level at which at_capacity flips to true and the
accept queue is flushed. Reported as slab.accept_threshold so the
per-iteration slab.accept_threshold_percent gauge in the run loop
can chart proximity to this gate, distinct from raw slab usage.
The constant 10 + 2 * max_connections is the historical pre-knob
budget; configured slab capacity is
10 + slab_entries_per_connection * max_connections (see
command/src/config.rs) and can be larger, so slab.usage_percent
(against slab.capacity()) and slab.accept_threshold_percent
(against this gate) are emitted as independent gauges.
Sourcepub fn check_limits(&mut self) -> bool
pub fn check_limits(&mut self) -> bool
Check the number of connections against max_connections, and the slab capacity. Returns false if limits are reached.