sozu_lib/udp.rs
1//! UDP load-balancing I/O shell + mio event-loop wiring (issue #1273).
2//!
3//! This module is the **impure** half of the UDP datapath: it owns every
4//! syscall, the buffer copies, the per-flow connected upstream sockets, the
5//! timer arming, the `BackendMap`/health/metrics edges, and the slab/token
6//! bookkeeping. It drives the pure sans-io core in
7//! [`crate::protocol::udp`] (the `UdpManager` /
8//! `UdpFlow` two-level split) through `ManagerInput` / `Output`.
9//!
10//! Architecture (mirrors `tcp.rs`, but UDP is **one-listener-many-flows**):
11//! - [`UdpListener`] wraps the single `mio::net::UdpSocket` a listener binds.
12//! There is **no accept loop** — a readable event means "datagrams waiting",
13//! not "a new connection".
14//! - [`UdpProxy`] holds the listeners, the per-listener [`UdpManager`]s, the
15//! shared `BackendMap`, the session slab and the registry. It does **not**
16//! implement `ProxyConfiguration` / `L7Proxy` (their signatures are
17//! `TcpStream`-bound); the server calls its inherent
18//! [`notify`](UdpProxy::notify) directly.
19//! - [`UdpListenerSession`] is the `ProxySession` the server's generic
20//! readiness path drives. One session backs one listener; its
21//! `update_readiness` demuxes by token (listener-token = client recv;
22//! upstream-token = backend recv). It owns the per-flow connected sockets and
23//! the `upstream_token -> FlowId` map.
24//!
25//! UDP never goes through `accept()` / `create_session()`: a `Protocol::UDP`
26//! listener readable event falls through `Server::ready`'s generic arm into
27//! `ProxySession::ready`.
28//!
29//! Long-form lifecycle (datapath, NAT return, teardown, control plane,
30//! hardening): `lib/src/protocol/udp/LIFECYCLE.md`.
31
32use std::{
33 cell::RefCell,
34 collections::{BTreeMap, HashMap, VecDeque, hash_map::Entry},
35 io::ErrorKind,
36 net::SocketAddr,
37 os::unix::io::AsRawFd,
38 rc::Rc,
39 time::{Duration, Instant},
40};
41
42use mio::{Interest, Registry, Token, net::UdpSocket, unix::SourceFd};
43use sozu_command::{
44 listener_key::ListenerKey,
45 logging::ansi_palette,
46 proto::command::{
47 Cluster, LoadBalancingAlgorithms, LoadMetric, RequestUdpFrontend, UdpAffinityKey,
48 UdpListenerConfig, UpdateUdpListenerConfig, WorkerRequest, WorkerResponse,
49 request::RequestType,
50 },
51};
52
53use crate::metrics::names;
54use crate::{
55 CachedTags, ListenerError, ListenerHandler, Protocol, ProxyError, ProxySession,
56 SessionIsToBeClosed,
57 backends::BackendMap,
58 pool::Pool,
59 protocol::udp::{
60 CloseReason, ClusterConfig, ConfigEvent, DropReason, FlowId, ManagerInput, MetricEvent,
61 Output, UdpManager,
62 },
63 server::{SessionManager, TIMER},
64 socket::{udp_bind, udp_connect},
65 sozu_command::{ready::Ready, state::ClusterId},
66};
67
68mod health;
69pub use health::UdpHealthChecker;
70
71/// Per-session log envelope (tag `UDP`). The colored output uses the unified
72/// scheme: bold bright-white protocol label, light-grey keyword, gray keys and
73/// bright-white values. Honours the `colored` flag via [`ansi_palette`].
74macro_rules! log_context {
75 ($self:expr) => {{
76 let (open, reset, grey, gray, white) = ansi_palette();
77 format!(
78 "[- - - -]\t{open}UDP{reset}\t{grey}Listener{reset}({gray}token{reset}={white}{token}{reset}, {gray}address{reset}={white}{address}{reset})\t >>>",
79 open = open,
80 reset = reset,
81 grey = grey,
82 gray = gray,
83 white = white,
84 token = $self.listener_token.0,
85 address = $self.address,
86 )
87 }};
88}
89
90/// Module-level prefix for [`UdpProxy`] callbacks (notify, listener add/remove,
91/// soft/hard stop) which own a listener/token map but have no per-session
92/// token. Produces a bold bright-white `UDP` label in colored mode.
93macro_rules! log_module_context {
94 () => {{
95 let (open, reset, _, _, _) = sozu_command::logging::ansi_palette();
96 format!("{open}UDP{reset}\t >>>", open = open, reset = reset)
97 }};
98}
99
100/// Per-flow log envelope (tag `UDP-FLOW`). Renders the flow's stable id plus
101/// client/backend addresses so flow lines stay filterable.
102macro_rules! log_flow_context {
103 ($flow:expr, $client:expr, $backend:expr) => {{
104 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
105 format!(
106 "[- - - -]\t{open}UDP-FLOW{reset}\t{grey}Flow{reset}({gray}id{reset}={white}{id}{reset}, {gray}client{reset}={white}{client}{reset}, {gray}backend{reset}={white}{backend:?}{reset})\t >>>",
107 open = open,
108 reset = reset,
109 grey = grey,
110 gray = gray,
111 white = white,
112 id = $flow,
113 client = $client,
114 backend = $backend,
115 )
116 }};
117}
118
119/// Bound on the per-flow upstream write queue (return path to one backend). A
120/// connected upstream socket only buffers one flow's traffic, so a small cap is
121/// enough to ride out a transient `EWOULDBLOCK` (kernel send buffer momentarily
122/// full) without letting a slow/stalled backend balloon memory. Past the cap we
123/// drop the datagram (`udp.datagrams.dropped.wq_full`) — UDP is best-effort and
124/// the client retries.
125const UPSTREAM_WRITE_QUEUE_CAP: usize = 64;
126/// Bound on the per-listener client-return write queue (replies fanned back to
127/// many clients through the single listener socket). Sized larger than the
128/// per-flow cap because it is shared across every flow's return traffic.
129const CLIENT_WRITE_QUEUE_CAP: usize = 256;
130
131/// What `WriteQueue::drain` should do after a single send attempt: keep
132/// draining, stop because the socket went `WouldBlock` (rearm WRITABLE), or stop
133/// because the send hit a hard error (drop the datagram, keep draining the rest).
134enum SendOutcome {
135 /// The datagram was written; pop it and continue.
136 Sent,
137 /// `WouldBlock`: leave the datagram at the front and stop draining.
138 WouldBlock,
139 /// A hard error (e.g. ECONNREFUSED): drop this datagram + count it, continue.
140 Dropped,
141}
142
143/// A bounded FIFO of datagrams awaiting a writable socket. The egress fast path
144/// (a `send`/`send_to` that succeeds immediately) never touches this; it is only
145/// engaged when the kernel send buffer is full (`WouldBlock`). On overflow the
146/// oldest-still-pending order is preserved and the *new* datagram is dropped
147/// (the queued ones are closer to leaving the socket).
148///
149/// The queue stores `(SocketAddr, Vec<u8>)`: the upstream variant ignores the
150/// address (a connected socket's `send` has an implicit destination) while the
151/// client-return variant uses it as the `send_to` destination. Keeping one type
152/// for both lets the drain loop and the unit tests stay shared.
153struct WriteQueue {
154 queue: VecDeque<(SocketAddr, Vec<u8>)>,
155 cap: usize,
156}
157
158impl WriteQueue {
159 fn new(cap: usize) -> Self {
160 WriteQueue {
161 queue: VecDeque::new(),
162 cap,
163 }
164 }
165
166 fn is_empty(&self) -> bool {
167 self.queue.is_empty()
168 }
169
170 /// Current depth. Only used by the unit tests; the runtime drains by the
171 /// `is_empty` / `drain` return value, not a length read.
172 #[cfg(test)]
173 fn len(&self) -> usize {
174 self.queue.len()
175 }
176
177 /// Enqueue a datagram for later draining. Returns `true` if it was accepted,
178 /// `false` if the queue was at capacity (caller drops + counts `wq_full`).
179 #[must_use]
180 fn push(&mut self, dst: SocketAddr, payload: Vec<u8>) -> bool {
181 if self.queue.len() >= self.cap {
182 return false;
183 }
184 self.queue.push_back((dst, payload));
185 // Invariant: the bounded FIFO never grows past its cap. The guard above
186 // is the only growth site, so depth <= cap holds after every push.
187 debug_assert!(
188 self.queue.len() <= self.cap,
189 "WriteQueue overran its cap: len {} > cap {}",
190 self.queue.len(),
191 self.cap,
192 );
193 true
194 }
195
196 /// Drain in FIFO order, calling `send` for each datagram. Stops on the first
197 /// `WouldBlock` (leaving that datagram queued for the next writable event) or
198 /// when empty. Hard-errored datagrams are popped and counted via
199 /// `SendOutcome::Dropped`. Returns `true` if the queue is now empty (the
200 /// caller can drop WRITABLE interest back to READABLE-only).
201 fn drain<F: FnMut(&SocketAddr, &[u8]) -> SendOutcome>(&mut self, mut send: F) -> bool {
202 while let Some((dst, payload)) = self.queue.front() {
203 match send(dst, payload) {
204 SendOutcome::Sent | SendOutcome::Dropped => {
205 self.queue.pop_front();
206 }
207 SendOutcome::WouldBlock => break,
208 }
209 }
210 self.queue.is_empty()
211 }
212}
213
214/// One UDP listener: a single `mio::net::UdpSocket` plus its routing config.
215/// Unlike a TCP listener there is no accept loop — a readable event is a batch
216/// of datagrams the session drains to `WouldBlock`.
217pub struct UdpListener {
218 active: SessionIsToBeClosed,
219 address: SocketAddr,
220 cluster_id: Option<ClusterId>,
221 config: UdpListenerConfig,
222 socket: Option<UdpSocket>,
223 /// A socket this listener holds but has NOT registered.
224 ///
225 /// `activate()` parks an inherited socket here when `Registry::register`
226 /// fails, instead of letting the `?` drop and close a descriptor
227 /// `Listeners::get_*` has already removed from the SCM table (sozu#1342).
228 /// The next activation reuses it rather than binding a second one.
229 ///
230 /// Deliberately NOT `socket`: every other consumer reads that field's `Some` as
231 /// "registered and live" — `soft_stop` and `hard_stop` deregister it and
232 /// report the failure, `give_back_listener` hands it back as an activated
233 /// socket, `accept` accepts on it. A parked socket is none of those things,
234 /// and putting it there made a failed registration answer `ENOENT` from
235 /// `deregister` and fail the whole soft stop.
236 parked_socket: Option<UdpSocket>,
237 tags: BTreeMap<String, CachedTags>,
238 token: Token,
239}
240
241impl ListenerHandler for UdpListener {
242 fn get_addr(&self) -> &SocketAddr {
243 &self.address
244 }
245
246 fn get_tags(&self, key: &str) -> Option<&CachedTags> {
247 self.tags.get(key)
248 }
249
250 fn set_tags(&mut self, key: String, tags: Option<BTreeMap<String, String>>) {
251 match tags {
252 Some(tags) => self.tags.insert(key, CachedTags::new(tags)),
253 None => self.tags.remove(&key),
254 };
255 }
256
257 fn protocol(&self) -> Protocol {
258 Protocol::UDP
259 }
260
261 fn public_address(&self) -> SocketAddr {
262 self.config
263 .public_address
264 .map(|addr| addr.into())
265 .unwrap_or(self.address)
266 }
267}
268
269impl UdpListener {
270 /// This listener's identity: its address, plus the network interface its
271 /// socket is bound to when it has one.
272 pub fn key(&self) -> ListenerKey {
273 ListenerKey::new(self.address, self.config.interface.as_deref())
274 }
275
276 /// Whether this listener is the one `key` names.
277 pub fn is(&self, key: &ListenerKey) -> bool {
278 key.matches(&self.address, self.config.interface.as_deref())
279 }
280
281 fn new(config: UdpListenerConfig, token: Token) -> Result<UdpListener, ListenerError> {
282 Ok(UdpListener {
283 cluster_id: None,
284 socket: None,
285 parked_socket: None,
286 token,
287 address: config.address.into(),
288 config,
289 active: false,
290 tags: BTreeMap::new(),
291 })
292 }
293
294 /// Validate that a worker can build this UDP listener configuration WITHOUT
295 /// constructing the full listener or binding a socket. UDP listener
296 /// construction has no fallible config today (no rustls context, no answer
297 /// templates; a bad bind surfaces later as an `ActivateListener` failure),
298 /// so this currently always succeeds. It exists for surface parity with the
299 /// HTTP/HTTPS validators the main process calls before committing an
300 /// `Add*Listener` to `ConfigState` and fanning it out (sozu#1301), and is
301 /// the hook for any future UDP-config validation.
302 pub fn validate_config(_config: &UdpListenerConfig) -> Result<(), ListenerError> {
303 Ok(())
304 }
305
306 /// Bind (or adopt an SCM-passed) socket and register it `READABLE`. The
307 /// READABLE registration is what drives `Server::ready` for this listener —
308 /// there is no accept path.
309 pub fn activate(
310 &mut self,
311 registry: &Registry,
312 udp_socket: Option<UdpSocket>,
313 ) -> Result<Token, ProxyError> {
314 if self.active {
315 return Ok(self.token);
316 }
317 let address: SocketAddr = self.config.address.into();
318
319 // Reuse a socket a previous attempt parked here after a failed
320 // registration, rather than binding a second one. Taking it
321 // unconditionally is what keeps the two from coexisting: if the caller
322 // ALSO handed one over, the parked socket is disposed of deliberately
323 // below instead of being silently overwritten.
324 let parked = self.parked_socket.take();
325 let mut socket = match (udp_socket, parked) {
326 (Some(inherited), Some(parked)) => {
327 // Not reachable while `Listeners::get_*` removes the entry it
328 // hands over and `scm_listeners` is written once: a parked
329 // socket means this address's entry was already consumed, so
330 // the caller cannot have another. Neither of those is enforced
331 // here, so dispose of the loser explicitly and loudly rather
332 // than letting an assignment close it — that silent close is
333 // sozu#1342 itself.
334 warn!(
335 "{} closing the socket parked on the UDP listener for {}: an \
336 inherited socket arrived for the same address and is the one being \
337 activated",
338 log_module_context!(),
339 address
340 );
341 drop(parked);
342 inherited
343 }
344 (Some(inherited), None) => inherited,
345 (None, Some(parked)) => parked,
346 (None, None) => udp_bind(address, self.config.interface.as_deref())
347 .map_err(|e| ProxyError::BindToSocket(address, e))?,
348 };
349
350 let registration = registry
351 .register(&mut socket, self.token, Interest::READABLE)
352 .map_err(ProxyError::RegisterListener);
353 if let Err(error) = registration {
354 // Park the socket instead of letting the `?` drop it. `register`
355 // is fallible (EEXIST for a descriptor still registered because
356 // `notify_deactivate_listener` only logs a failed deregister,
357 // EBADF, ENOMEM), and a socket still held in a local is dropped —
358 // and closed — on the way out. For an SCM-inherited socket that is
359 // the sozu#1342 close in a second place: `Listeners::get_*` has
360 // already removed the entry, so nothing else owns the descriptor.
361 // It goes to `parked_socket`, never to `socket`, because every other
362 // consumer of that field reads `Some` as "registered and live".
363 self.parked_socket = Some(socket);
364 return Err(error);
365 }
366
367 self.socket = Some(socket);
368 self.active = true;
369 Ok(self.token)
370 }
371
372 /// Apply a partial-update patch to this UDP listener's live config. Fields
373 /// absent in the patch (`None`) are preserved.
374 pub fn update_config(&mut self, patch: &UpdateUdpListenerConfig) {
375 if let Some(v) = patch.public_address {
376 self.config.public_address = Some(v);
377 }
378 if let Some(v) = patch.front_timeout {
379 self.config.front_timeout = v;
380 }
381 if let Some(v) = patch.back_timeout {
382 self.config.back_timeout = v;
383 }
384 if let Some(v) = patch.max_rx_datagram_size {
385 self.config.max_rx_datagram_size = v;
386 }
387 if let Some(v) = patch.max_flows {
388 self.config.max_flows = v;
389 }
390 }
391}
392
393/// The UDP proxy. Holds the listeners, one `UdpManager` per listener token, the
394/// shared `BackendMap`, the session slab and a cloned registry. Does NOT
395/// implement `ProxyConfiguration` / `L7Proxy`; the server drives it through the
396/// inherent [`notify`](Self::notify) plus the activate/give-back helpers.
397pub struct UdpProxy {
398 fronts: HashMap<ClusterId, Token>,
399 backends: Rc<RefCell<BackendMap>>,
400 listeners: HashMap<Token, Rc<RefCell<UdpListener>>>,
401 /// The built listener session per listener token. The server inserts the
402 /// same `Rc` into the slab at the listener token; the proxy keeps a clone so
403 /// it can drive per-flow teardown (soft/hard stop) without downcasting the
404 /// slab's `dyn ProxySession`. Cleared on listener removal/stop.
405 listener_sessions: HashMap<Token, Rc<RefCell<UdpListenerSession>>>,
406 /// One sans-io manager per listener token, sharing the listener's lifecycle.
407 managers: HashMap<Token, Rc<RefCell<UdpManager>>>,
408 /// Cluster routing for each listener token (set by the UDP frontend).
409 cluster_for_listener: HashMap<Token, ClusterId>,
410 /// Last `AddCluster`-supplied UDP knobs per cluster id. Cached so a frontend
411 /// added AFTER its cluster still picks up `responses` / `requests` / PPv2 /
412 /// affinity — `AddCluster` and `AddUdpFrontend` arrive in either order, and
413 /// neither must clobber the other's contribution to a manager's
414 /// `ClusterConfig`.
415 cluster_udp_config: HashMap<ClusterId, sozu_command::proto::command::UdpClusterConfig>,
416 /// Last `AddCluster`-supplied `max_connections_per_ip` /
417 /// `max_connections_per_subnet` overrides per cluster, cached for the same
418 /// order-independence as `cluster_udp_config`. They live on the `Cluster`
419 /// message itself, not in its `udp` block. Only these cluster-level values
420 /// apply to UDP: the global defaults are deliberately NOT inherited, so a
421 /// global cap set for HTTP/TCP never starts dropping UDP flows.
422 cluster_flow_limits: HashMap<ClusterId, (Option<u64>, Option<u64>)>,
423 /// The worker's boot-time `subnet_ipv4_prefix` / `subnet_ipv6_prefix`,
424 /// copied from the `SessionManager` at construction. A copy rather than a
425 /// borrow: the server calls into this proxy (`add_listener`) while it
426 /// holds the `SessionManager` mutably.
427 subnet_prefixes: (u32, u32),
428 registry: Registry,
429 sessions: Rc<RefCell<SessionManager>>,
430 #[allow(dead_code)]
431 pool: Rc<RefCell<Pool>>,
432 /// Fixed hash seed injected once into every manager. It is deliberately the
433 /// same constant on every worker and across restarts so HRW/Maglev affinity
434 /// is reproducible cluster-wide — a per-worker random seed would scatter the
435 /// same flow key onto a different backend on each worker and reshuffle it on
436 /// every restart, defeating the documented affinity-stability contract (see
437 /// [`crate::load_balancing::DEFAULT_HASH_SEED`]).
438 hash_seed: u64,
439 /// Global `max_connections` (from `ServerConfig`). Used to clamp a UDP
440 /// listener's auto `max_flows` so a single listener cannot inflate the
441 /// shared `SessionManager` slab and starve HTTP/TCP.
442 max_connections: usize,
443 /// Global `buffer_size` (from `ServerConfig`). Used to clamp a listener's
444 /// `max_rx_datagram_size` so a hostile `AddUdpListener` cannot allocate a
445 /// multi-GB recv buffer.
446 buffer_size: usize,
447 /// Endpoint-bound active health prober (TCP probe + hysteresis +
448 /// fail-open). Driven from the server event loop via
449 /// [`UdpProxy::health_poll`] / [`UdpProxy::health_ready`].
450 health: UdpHealthChecker,
451}
452
453impl UdpProxy {
454 pub fn new(
455 registry: Registry,
456 sessions: Rc<RefCell<SessionManager>>,
457 pool: Rc<RefCell<Pool>>,
458 backends: Rc<RefCell<BackendMap>>,
459 max_connections: usize,
460 buffer_size: usize,
461 ) -> UdpProxy {
462 // Fixed seed, identical on every worker and across restarts: HRW/Maglev
463 // affinity must be reproducible cluster-wide. A per-worker/per-restart
464 // random seed (as a `process::id()` + `Instant::now()` mix would be)
465 // would route the same flow key to a different backend on each worker
466 // and reshuffle it on every restart. Reuse the LB module's canonical
467 // affinity seed so UDP and the HTTP/TCP affinity hashers agree.
468 let hash_seed = crate::load_balancing::DEFAULT_HASH_SEED;
469 let subnet_prefixes = {
470 let s = sessions.borrow();
471 (s.subnet_ipv4_prefix, s.subnet_ipv6_prefix)
472 };
473 UdpProxy {
474 backends,
475 listeners: HashMap::new(),
476 listener_sessions: HashMap::new(),
477 managers: HashMap::new(),
478 cluster_for_listener: HashMap::new(),
479 cluster_udp_config: HashMap::new(),
480 cluster_flow_limits: HashMap::new(),
481 subnet_prefixes,
482 fronts: HashMap::new(),
483 registry,
484 sessions,
485 pool,
486 hash_seed,
487 max_connections,
488 buffer_size,
489 health: UdpHealthChecker::new(),
490 }
491 }
492
493 /// Drive the UDP health prober one event-loop step (server calls this once
494 /// per iteration, mirroring `HealthChecker::poll`). Non-blocking.
495 ///
496 /// The prober borrows `self.registry` directly. It must not clone it:
497 /// `Registry::try_clone` is an `fcntl(F_DUPFD_CLOEXEC)` of the epoll
498 /// descriptor and dropping the clone is a `close`, so a clone here cost
499 /// two syscalls on **every** event-loop turn, with or without a UDP
500 /// cluster, and silently skipped health probing whenever the worker was
501 /// out of descriptors (`EMFILE`).
502 pub fn health_poll(&mut self) {
503 self.health.poll(&self.backends, &self.registry);
504 }
505
506 /// Record mio readiness for a UDP health-probe socket.
507 pub fn health_ready(&mut self, token: Token) {
508 self.health.ready(token);
509 }
510
511 /// Whether `token` is a UDP health-probe socket this proxy owns.
512 pub fn health_owns_token(&self, token: Token) -> bool {
513 self.health.owns_token(token)
514 }
515
516 pub fn add_listener(
517 &mut self,
518 config: UdpListenerConfig,
519 token: Token,
520 ) -> Result<Token, ProxyError> {
521 match self.listeners.entry(token) {
522 Entry::Vacant(entry) => {
523 let mut config = config;
524 let max_flows = effective_max_flows(config.max_flows, self.max_connections);
525 // Defense in depth: cap the recv-buffer sizing to `buffer_size`
526 // so a hostile `AddUdpListener` (`max_rx_datagram_size =
527 // u32::MAX`) can't allocate a multi-GB buffer. Write the clamped
528 // value back into the stored config so the manager AND the
529 // session's `recv_buf` (sized from `config` in `new()`) agree on
530 // the same bound.
531 let max_rx = clamp_max_rx(config.max_rx_datagram_size as usize, self.buffer_size);
532 config.max_rx_datagram_size = max_rx as u32;
533 let front = Duration::from_secs(u64::from(config.front_timeout));
534 let back = Duration::from_secs(u64::from(config.back_timeout));
535 let listener = UdpListener::new(config, token).map_err(ProxyError::AddListener)?;
536 entry.insert(Rc::new(RefCell::new(listener)));
537 let cluster_cfg = ClusterConfig {
538 front_timeout: front,
539 back_timeout: back,
540 subnet_ipv4_prefix: self.subnet_prefixes.0,
541 subnet_ipv6_prefix: self.subnet_prefixes.1,
542 ..Default::default()
543 };
544 self.managers.insert(
545 token,
546 Rc::new(RefCell::new(UdpManager::new(
547 cluster_cfg,
548 max_flows,
549 max_rx,
550 self.hash_seed,
551 ))),
552 );
553 self.adopt_sibling_route(token);
554 Ok(token)
555 }
556 _ => Err(ProxyError::ListenerAlreadyPresent),
557 }
558 }
559
560 /// A UDP frontend names its listener by address alone, so a listener
561 /// added next to one already on its address (on another interface) takes
562 /// over the cluster that one routes to, in its own state and in its flow
563 /// manager: every listener on an address routes the same way, whenever it
564 /// was added (sozu-proxy/sozu#719).
565 fn adopt_sibling_route(&mut self, token: Token) {
566 let address = self.listeners[&token].borrow().address;
567 let Some((cluster_id, tags)) = self
568 .listeners
569 .iter()
570 .filter(|(sibling, listener)| {
571 **sibling != token && listener.borrow().address == address
572 })
573 .min_by_key(|(sibling, _)| **sibling)
574 .map(|(_, listener)| {
575 let listener = listener.borrow();
576 (listener.cluster_id.clone(), listener.tags.clone())
577 })
578 else {
579 return;
580 };
581 {
582 let mut listener = self.listeners[&token].borrow_mut();
583 listener.tags = tags;
584 listener.cluster_id.clone_from(&cluster_id);
585 }
586 if let Some(cluster_id) = cluster_id {
587 self.cluster_for_listener.insert(token, cluster_id);
588 if let Some(mgr) = self.managers.get(&token) {
589 let cfg = {
590 let l = self.listeners[&token].borrow();
591 self.cluster_config_for(&l, token)
592 };
593 mgr.borrow_mut().handle_input(
594 ManagerInput::Config(ConfigEvent::SetCluster(cfg)),
595 Instant::now(),
596 );
597 }
598 }
599 }
600
601 pub fn remove_listener(&mut self, address: &ListenerKey) -> SessionIsToBeClosed {
602 let len = self.listeners.len();
603 let mut removed_tokens = Vec::new();
604 self.listeners.retain(|token, l| {
605 if l.borrow().is(address) {
606 removed_tokens.push(*token);
607 false
608 } else {
609 true
610 }
611 });
612 let now = Instant::now();
613 for token in removed_tokens {
614 self.cluster_for_listener.remove(&token);
615 // Drive per-flow teardown THROUGH the manager (emits FlowEvicted +
616 // CloseFlow per flow) BEFORE dropping the manager, so the
617 // active-flows gauge is decremented once per flow and the per-flow
618 // upstream sockets + slab slots are freed. Removing the manager first
619 // would silently leak the gauge by N.
620 if let Some(session) = self.listener_sessions.remove(&token) {
621 session.borrow_mut().close_all_flows(now);
622 }
623 self.managers.remove(&token);
624 }
625 self.listeners.len() < len
626 }
627
628 /// What this proxy would do with a socket handed to
629 /// [`activate_listener`](Self::activate_listener) for `addr`. See
630 /// [`crate::InheritedSocketFate`].
631 ///
632 /// The event loop is single-threaded, so nothing can change the listener's
633 /// `active` flag between this answer and the `activate_listener` call that
634 /// acts on it.
635 pub fn inherited_socket_fate(&self, addr: &ListenerKey) -> crate::InheritedSocketFate {
636 use crate::InheritedSocketFate;
637 match self
638 .listeners
639 .values()
640 .find(|listener| listener.borrow().is(addr))
641 {
642 None => InheritedSocketFate::Unclaimed,
643 Some(listener) if listener.borrow().active => InheritedSocketFate::Refused,
644 Some(_) => InheritedSocketFate::Adopted,
645 }
646 }
647
648 pub fn activate_listener(
649 &self,
650 addr: &ListenerKey,
651 udp_socket: Option<UdpSocket>,
652 ) -> Result<Token, ProxyError> {
653 let listener = self
654 .listeners
655 .values()
656 .find(|listener| listener.borrow().is(addr))
657 .ok_or(ProxyError::NoListenerFound(addr.clone()))?;
658
659 listener.borrow_mut().activate(&self.registry, udp_socket)
660 }
661
662 /// Whether the real [`UdpListenerSession`] for `token` is already
663 /// installed, i.e. [`build_session`](Self::build_session) has run for it
664 /// and no teardown has removed it since.
665 ///
666 /// `listener_sessions` is populated by `build_session` alone and emptied by
667 /// `give_back_listener` (deactivate), `remove_listener` and soft/hard stop,
668 /// so membership tracks the installed session exactly. `Server` reads it to
669 /// tell a first `ActivateListener` from a repeat: `UdpListener::activate`
670 /// answers `Ok(token)` for an already-active listener without doing any
671 /// work, and rebuilding on that answer would destroy the live session.
672 pub fn has_listener_session(&self, token: Token) -> bool {
673 self.listener_sessions.contains_key(&token)
674 }
675
676 /// The slab token reserved for the listener at `address`, if this proxy
677 /// holds one.
678 ///
679 /// A listener owns exactly one slab slot for its whole `AddListener` ->
680 /// `RemoveListener` lifetime, and no `remove_listener` implementation
681 /// touches the session slab. `Server` therefore reads the token here
682 /// BEFORE dropping the listener, so that reserved slot is released exactly
683 /// once, at the end of the lifetime.
684 pub fn listener_token(&self, address: &ListenerKey) -> Option<Token> {
685 self.listeners
686 .iter()
687 .find(|(_, listener)| listener.borrow().is(address))
688 .map(|(token, _)| *token)
689 }
690
691 /// The cluster the listener at `address` routes to, for tests that drive
692 /// the proxy through `Server::notify_proxys` (`lib/src/server.rs`).
693 #[cfg(test)]
694 pub(crate) fn listener_cluster(&self, address: &ListenerKey) -> Option<ClusterId> {
695 let token = self.listener_token(address)?;
696 self.cluster_for_listener.get(&token).cloned()
697 }
698
699 /// Build the [`UdpListenerSession`] that drives this listener's datagrams.
700 /// The server inserts the returned session into the slab **at the listener
701 /// token**, replacing the `ListenSession` placeholder, so the generic
702 /// readiness path reaches `UdpListenerSession::update_readiness`. Returns
703 /// `None` if the listener token is unknown.
704 pub fn build_session(&mut self, token: Token) -> Option<Rc<RefCell<UdpListenerSession>>> {
705 let listener = self.listeners.get(&token)?.clone();
706 let manager = self.managers.get(&token)?.clone();
707 let registry = self.registry.try_clone().ok()?;
708 let session = Rc::new(RefCell::new(UdpListenerSession::new(
709 listener,
710 manager,
711 self.backends.clone(),
712 registry,
713 self.sessions.clone(),
714 token,
715 )));
716 // Keep a clone so soft/hard stop can drive per-flow teardown.
717 self.listener_sessions.insert(token, session.clone());
718 Some(session)
719 }
720
721 pub fn give_back_listeners(&mut self) -> Vec<(ListenerKey, UdpSocket)> {
722 self.listeners
723 .values()
724 .filter_map(|listener| {
725 let mut owned = listener.borrow_mut();
726 if let Some(socket) = owned.socket.take() {
727 owned.active = false;
728 return Some((owned.key(), socket));
729 }
730 None
731 })
732 .collect()
733 }
734
735 pub fn give_back_listener(
736 &mut self,
737 address: &ListenerKey,
738 ) -> Result<(Token, UdpSocket), ProxyError> {
739 let listener = self
740 .listeners
741 .values()
742 .find(|listener| listener.borrow().is(address))
743 .ok_or(ProxyError::NoListenerFound(address.clone()))?;
744
745 let (token, taken) = {
746 let mut owned = listener.borrow_mut();
747 let taken = owned.socket.take().ok_or(ProxyError::UnactivatedListener)?;
748 owned.active = false;
749 (owned.token, taken)
750 };
751 // Deactivating removes the listener from service: tear down its active
752 // flows THROUGH the manager so the per-flow upstream slab slots + fds
753 // don't dangle and the active-flows gauge is decremented once per flow
754 // (the manager is RETAINED here — not removed from `self.managers` — so
755 // `close_all` also resets its flow table, keeping manager and shell
756 // consistent for a later reactivate).
757 if let Some(session) = self.listener_sessions.remove(&token) {
758 session.borrow_mut().close_all_flows(Instant::now());
759 }
760 Ok((token, taken))
761 }
762
763 pub fn update_listener(&mut self, patch: UpdateUdpListenerConfig) -> Result<(), ProxyError> {
764 let address: SocketAddr = patch.address.into();
765 let listener = self
766 .listeners
767 .values()
768 .find(|l| l.borrow().address == address)
769 .ok_or(ProxyError::NoListenerFound(address.into()))?;
770 {
771 let mut l = listener.borrow_mut();
772 l.update_config(&patch);
773 // Clamp the stored rx size in place (defense in depth): keep the
774 // listener config, the manager, and the session's `recv_buf` agreeing
775 // on the same `buffer_size`-bounded value.
776 l.config.max_rx_datagram_size =
777 clamp_max_rx(l.config.max_rx_datagram_size as usize, self.buffer_size) as u32;
778 }
779
780 // Reflect the timeout / cap / rx-size changes into the manager for new
781 // flows. Existing flows keep their captured config (stable contract).
782 if let Some(token) = self
783 .listeners
784 .iter()
785 .find(|(_, l)| l.borrow().address == address)
786 .map(|(t, _)| *t)
787 && let Some(mgr) = self.managers.get(&token)
788 {
789 let now = Instant::now();
790 let (cfg, max_flows, max_rx) = {
791 let l = listener.borrow();
792 (
793 self.cluster_config_for(&l, token),
794 effective_max_flows(l.config.max_flows, self.max_connections),
795 // Defense in depth: clamp the raised rx size to `buffer_size`
796 // so a hostile `UpdateUdpListener` can't grow the recv buffer
797 // past the global cap.
798 clamp_max_rx(l.config.max_rx_datagram_size as usize, self.buffer_size),
799 )
800 };
801 {
802 let mut m = mgr.borrow_mut();
803 m.handle_input(ManagerInput::Config(ConfigEvent::SetCluster(cfg)), now);
804 m.handle_input(
805 ManagerInput::Config(ConfigEvent::SetMaxFlows(max_flows)),
806 now,
807 );
808 m.handle_input(
809 ManagerInput::Config(ConfigEvent::SetMaxRxDatagramSize(max_rx)),
810 now,
811 );
812 }
813 // Resize the live session's recv scratch to match the (clamped) new
814 // rx size. Without this, a config that RAISES `max_rx_datagram_size`
815 // would leave `recv_buf` at its old (smaller) length: `recv_from`
816 // would kernel-truncate datagrams between the old and new size while
817 // the manager's `len > max_rx` check (now the larger value) passes
818 // them, forwarding them truncated. The `+ 1` mirrors `new()`: it lets
819 // the manager observe `len == max_rx + 1 > max_rx` and drop an
820 // oversized datagram (`DropReason::Truncated`) instead of silently
821 // truncating it. Existing flows keep their captured config; only the
822 // shared recv buffer tracks the live rx size.
823 if let Some(session) = self.listener_sessions.get(&token) {
824 session.borrow_mut().resize_recv_buf(max_rx);
825 }
826 }
827 Ok(())
828 }
829
830 /// The tokens of every listener on `address`, whatever its interface. A
831 /// UDP frontend names its listener by address alone, so it applies to all
832 /// the listeners sharing that address on different interfaces.
833 fn listener_tokens_at(&self, address: SocketAddr) -> Result<Vec<Token>, ProxyError> {
834 let mut tokens: Vec<Token> = self
835 .listeners
836 .iter()
837 .filter(|(_, listener)| listener.borrow().address == address)
838 .map(|(token, _)| *token)
839 .collect();
840 tokens.sort();
841 if tokens.is_empty() {
842 return Err(ProxyError::NoListenerFound(address.into()));
843 }
844 Ok(tokens)
845 }
846
847 pub fn add_udp_front(&mut self, front: RequestUdpFrontend) -> Result<(), ProxyError> {
848 let address = front.address.into();
849 let cluster_id = ClusterId::from(front.cluster_id);
850 for token in self.listener_tokens_at(address)? {
851 {
852 let mut listener = self.listeners[&token].borrow_mut();
853 self.fronts.insert(cluster_id.clone(), listener.token);
854 listener.set_tags(address.to_string(), Some(front.tags.clone()));
855 listener.cluster_id = Some(cluster_id.clone());
856 }
857 self.cluster_for_listener.insert(token, cluster_id.clone());
858
859 // Commit the cluster routing into the manager so admitted flows know
860 // which cluster to select a backend from (`BackendSource::select`).
861 if let Some(mgr) = self.managers.get(&token) {
862 let cfg = {
863 let l = self.listeners[&token].borrow();
864 self.cluster_config_for(&l, token)
865 };
866 mgr.borrow_mut().handle_input(
867 ManagerInput::Config(ConfigEvent::SetCluster(cfg)),
868 Instant::now(),
869 );
870 }
871 }
872 Ok(())
873 }
874
875 pub fn remove_udp_front(&mut self, front: RequestUdpFrontend) -> Result<(), ProxyError> {
876 let address = front.address.into();
877 for token in self.listener_tokens_at(address)? {
878 {
879 let mut listener = self.listeners[&token].borrow_mut();
880 listener.set_tags(address.to_string(), None);
881 if let Some(cluster_id) = listener.cluster_id.take() {
882 self.fronts.remove(&cluster_id);
883 }
884 }
885 self.cluster_for_listener.remove(&token);
886 // Drop the routing in the manager — new datagrams now have no backend.
887 if let Some(mgr) = self.managers.get(&token) {
888 mgr.borrow_mut().handle_input(
889 ManagerInput::Config(ConfigEvent::SetCluster(ClusterConfig::default())),
890 Instant::now(),
891 );
892 }
893 }
894 Ok(())
895 }
896
897 /// Build a [`ClusterConfig`] for a listener from its current frontend
898 /// cluster routing + timeouts. The per-cluster knobs (responses/requests/
899 /// PPv2/affinity) are populated by [`apply_cluster`](Self::apply_cluster)
900 /// when an `AddCluster` carries a `udp` block; absent that they stay at the
901 /// proto defaults.
902 fn cluster_config_for(&self, listener: &UdpListener, _token: Token) -> ClusterConfig {
903 let cluster = listener.cluster_id.clone().unwrap_or_default();
904 let mut cfg = ClusterConfig {
905 cluster: cluster.clone(),
906 front_timeout: Duration::from_secs(u64::from(listener.config.front_timeout)),
907 back_timeout: Duration::from_secs(u64::from(listener.config.back_timeout)),
908 ..Default::default()
909 };
910 // Fold in the cluster's cached UDP knobs (set by a prior or later
911 // `AddCluster`) so the order of `AddCluster` vs `AddUdpFrontend` does not
912 // matter — both rebuilds of a manager's `ClusterConfig` converge on the
913 // same per-cluster contract.
914 if let Some(udp) = self.cluster_udp_config.get(&cluster) {
915 apply_udp_knobs(&mut cfg, udp);
916 }
917 // Per-source flow limits: opt-in, from the cluster's own
918 // `max_connections_per_ip` / `max_connections_per_subnet` only. An
919 // absent value is unlimited; the global defaults are NOT inherited.
920 // The subnet prefixes are the worker's boot-time ones.
921 let (per_ip, per_subnet) = self
922 .cluster_flow_limits
923 .get(&cluster)
924 .copied()
925 .unwrap_or_default();
926 cfg.max_flows_per_ip = per_ip.unwrap_or(0);
927 cfg.max_flows_per_subnet = per_subnet.unwrap_or(0);
928 (cfg.subnet_ipv4_prefix, cfg.subnet_ipv6_prefix) = self.subnet_prefixes;
929 cfg
930 }
931
932 /// The flow manager of the listener at `token`.
933 #[cfg(test)]
934 pub(crate) fn manager(&self, token: Token) -> Option<Rc<RefCell<UdpManager>>> {
935 self.managers.get(&token).cloned()
936 }
937
938 /// Apply an `AddCluster` to every listener routing to it: fold the UDP
939 /// cluster knobs (affinity / responses / requests / PPv2) into the
940 /// manager's `ClusterConfig`, and rebuild the LB policy (HRW/Maglev table)
941 /// via the shared `BackendMap`.
942 fn apply_cluster(&mut self, cluster: &Cluster) {
943 // 1. LB policy (HRW/Maglev rebuild happens inside the BackendMap).
944 self.backends
945 .borrow_mut()
946 .set_load_balancing_policy_for_cluster(
947 &cluster.cluster_id,
948 LoadBalancingAlgorithms::try_from(cluster.load_balancing).unwrap_or_default(),
949 cluster
950 .load_metric
951 .and_then(|n| LoadMetric::try_from(n).ok()),
952 );
953
954 // 1b. Health settings from the cluster's `udp.health` block, if any.
955 // Mode HEALTH_OFF / no health block disables probing for this cluster.
956 let health_settings = cluster.udp.as_ref().and_then(|udp| {
957 udp.health.as_ref().and_then(|h| {
958 let mode = h
959 .mode
960 .and_then(|m| sozu_command::proto::command::UdpHealthMode::try_from(m).ok());
961 match mode {
962 Some(sozu_command::proto::command::UdpHealthMode::HealthOff) => None,
963 // TCP_PROBE (default when a health block is present) and
964 // UDP_PROBE both schedule the TCP-probe state machine; the
965 // app UDP-probe payload rides along for the secondary check.
966 _ => Some(health::UdpHealthSettings::from_proto(h)),
967 }
968 })
969 });
970 self.health
971 .set_cluster(&cluster.cluster_id, health_settings, &self.registry);
972
973 // 1c. Cache the UDP knobs so a frontend added in EITHER order picks them
974 // up via `cluster_config_for`. An `AddCluster` without a `udp` block
975 // clears the cache (back to proto defaults).
976 match &cluster.udp {
977 Some(udp) => {
978 self.cluster_udp_config
979 .insert(cluster.cluster_id.as_str().into(), udp.clone());
980 }
981 None => {
982 self.cluster_udp_config.remove(cluster.cluster_id.as_str());
983 }
984 }
985 self.cluster_flow_limits.insert(
986 cluster.cluster_id.as_str().into(),
987 (
988 cluster.max_connections_per_ip,
989 cluster.max_connections_per_subnet,
990 ),
991 );
992
993 // 2. Per-cluster UDP knobs into the managers routing to this cluster.
994 // `cluster_config_for` now folds the cached knobs in, so the rebuild is
995 // identical regardless of whether the frontend or the cluster came
996 // first.
997 let now = Instant::now();
998 let tokens: Vec<Token> = self
999 .cluster_for_listener
1000 .iter()
1001 .filter(|(_, c)| ***c == *cluster.cluster_id)
1002 .map(|(t, _)| *t)
1003 .collect();
1004 for token in tokens {
1005 let Some(listener) = self.listeners.get(&token) else {
1006 continue;
1007 };
1008 let cfg = {
1009 let l = listener.borrow();
1010 self.cluster_config_for(&l, token)
1011 };
1012 if let Some(mgr) = self.managers.get(&token) {
1013 mgr.borrow_mut()
1014 .handle_input(ManagerInput::Config(ConfigEvent::SetCluster(cfg)), now);
1015 }
1016 }
1017 }
1018
1019 /// Inherent dispatch entry point — the server calls this directly (UDP does
1020 /// not implement `ProxyConfiguration`). Handles UDP frontends, cluster
1021 /// config, listener removal, and stop. No accept / create_session.
1022 pub fn notify(&mut self, message: WorkerRequest) -> WorkerResponse {
1023 let request_type = match message.content.request_type {
1024 Some(t) => t,
1025 None => return WorkerResponse::error(message.id, "Empty request"),
1026 };
1027 match request_type {
1028 RequestType::AddUdpFrontend(front) => match self.add_udp_front(front) {
1029 Ok(()) => WorkerResponse::ok(message.id),
1030 Err(err) => WorkerResponse::error(message.id, err),
1031 },
1032 RequestType::RemoveUdpFrontend(front) => match self.remove_udp_front(front) {
1033 Ok(()) => WorkerResponse::ok(message.id),
1034 Err(err) => WorkerResponse::error(message.id, err),
1035 },
1036 RequestType::AddCluster(cluster) => {
1037 self.apply_cluster(&cluster);
1038 WorkerResponse::ok(message.id)
1039 }
1040 RequestType::RemoveCluster(cluster_id) => {
1041 let tokens: Vec<Token> = self
1042 .cluster_for_listener
1043 .iter()
1044 .filter(|(_, c)| ***c == *cluster_id)
1045 .map(|(t, _)| *t)
1046 .collect();
1047 for token in tokens {
1048 if let Some(mgr) = self.managers.get(&token) {
1049 mgr.borrow_mut().handle_input(
1050 ManagerInput::Config(ConfigEvent::SetCluster(ClusterConfig::default())),
1051 Instant::now(),
1052 );
1053 }
1054 }
1055 self.cluster_udp_config.remove(cluster_id.as_str());
1056 self.cluster_flow_limits.remove(cluster_id.as_str());
1057 self.health.remove_cluster(&cluster_id, &self.registry);
1058 WorkerResponse::ok(message.id)
1059 }
1060 RequestType::SoftStop(_) => {
1061 info!(
1062 "{} {} processing soft shutdown",
1063 log_module_context!(),
1064 message.id
1065 );
1066 // Drain: admit no new flows. Then actively tear down existing
1067 // flows so the worker reaches `base_sessions_count` and exits
1068 // promptly instead of waiting out each flow's idle timeout. UDP
1069 // has no half-sent response to preserve, so an immediate flow
1070 // teardown on soft-stop is the right graceful behavior (a stray
1071 // in-flight reply may be lost, which is acceptable for a
1072 // best-effort datagram proxy).
1073 let now = Instant::now();
1074 for mgr in self.managers.values() {
1075 mgr.borrow_mut()
1076 .handle_input(ManagerInput::Config(ConfigEvent::Drain), now);
1077 }
1078 // Drive teardown through each manager (FlowEvicted + CloseFlow
1079 // per flow) so the active-flows gauge balances to zero.
1080 for session in self.listener_sessions.values() {
1081 session.borrow_mut().close_all_flows(now);
1082 }
1083 self.listener_sessions.clear();
1084 let listeners = std::mem::take(&mut self.listeners);
1085 for l in listeners.values() {
1086 l.borrow_mut()
1087 .socket
1088 .take()
1089 .map(|mut sock| self.registry.deregister(&mut sock));
1090 }
1091 WorkerResponse::processing(message.id)
1092 }
1093 RequestType::HardStop(_) => {
1094 info!("{} {} hard shutdown", log_module_context!(), message.id);
1095 let now = Instant::now();
1096 // Drive teardown through each manager (FlowEvicted + CloseFlow
1097 // per flow) so the active-flows gauge balances to zero before the
1098 // managers are dropped below.
1099 for session in self.listener_sessions.values() {
1100 session.borrow_mut().close_all_flows(now);
1101 }
1102 self.listener_sessions.clear();
1103 for (_, l) in std::mem::take(&mut self.listeners) {
1104 l.borrow_mut()
1105 .socket
1106 .take()
1107 .map(|mut sock| self.registry.deregister(&mut sock));
1108 }
1109 self.managers.clear();
1110 WorkerResponse::ok(message.id)
1111 }
1112 RequestType::Status(_) => {
1113 info!("{} {} status", log_module_context!(), message.id);
1114 WorkerResponse::ok(message.id)
1115 }
1116 RequestType::RemoveListener(remove) => {
1117 if !self.remove_listener(&remove.listener_key()) {
1118 WorkerResponse::error(
1119 message.id,
1120 format!("no UDP listener to remove at address {:?}", remove.address),
1121 )
1122 } else {
1123 WorkerResponse::ok(message.id)
1124 }
1125 }
1126 command => {
1127 debug!(
1128 "{} {} unsupported message for UDP proxy, ignoring {:?}",
1129 log_module_context!(),
1130 message.id,
1131 command
1132 );
1133 WorkerResponse::error(message.id, "unsupported message")
1134 }
1135 }
1136 }
1137}
1138
1139/// Fold a proto [`UdpClusterConfig`](sozu_command::proto::command::UdpClusterConfig)
1140/// into a [`ClusterConfig`], applying the proto defaults for absent fields.
1141/// Single source of truth so `apply_cluster` (live push) and
1142/// `cluster_config_for` (frontend add / rebuild) never diverge.
1143fn apply_udp_knobs(cfg: &mut ClusterConfig, udp: &sozu_command::proto::command::UdpClusterConfig) {
1144 cfg.affinity_with_port = matches!(
1145 udp.affinity_key
1146 .and_then(|k| UdpAffinityKey::try_from(k).ok()),
1147 Some(UdpAffinityKey::SourceIpPort)
1148 );
1149 cfg.responses = udp.responses.unwrap_or(0);
1150 cfg.requests = udp.requests.unwrap_or(0);
1151 cfg.send_proxy_protocol = udp.send_proxy_protocol.unwrap_or(false);
1152 cfg.proxy_protocol_every_datagram = udp.proxy_protocol_every_datagram.unwrap_or(false);
1153}
1154
1155/// Fallback auto `max_flows` when `RLIMIT_NOFILE` can't be read.
1156const DEFAULT_AUTO_MAX_FLOWS: usize = 1024;
1157
1158/// `max_flows == 0` means "auto": ~70% of the soft `RLIMIT_NOFILE`, so the fd
1159/// budget adapts to the host without hand-tuning (one fd + one slab slot per
1160/// flow). Falls back to a conservative constant when the limit can't be read.
1161///
1162/// The auto derivation is clamped to `slab_headroom` (the global
1163/// `max_connections`): every admitted flow consumes one shared `SessionManager`
1164/// slab slot, so an unclamped ~70%-of-RLIMIT value on a host with a very large
1165/// fd limit could try to inflate the slab to hundreds of thousands of entries
1166/// and starve HTTP/TCP. An *explicitly configured* `max_flows` is honoured as-is
1167/// (the operator opted in); only the auto value is capped. A `slab_headroom` of
1168/// 0 (no connection budget configured) disables the clamp.
1169fn effective_max_flows(configured: u32, slab_headroom: usize) -> usize {
1170 if configured != 0 {
1171 return configured as usize;
1172 }
1173 let auto = {
1174 #[cfg(unix)]
1175 {
1176 let mut limit = libc::rlimit {
1177 rlim_cur: 0,
1178 rlim_max: 0,
1179 };
1180 // SAFETY: `getrlimit` writes a fully-initialised `rlimit` into
1181 // `limit`; we read the result only on success (`== 0`).
1182 let ret = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1183 if ret == 0 && limit.rlim_cur > 0 {
1184 let soft = limit.rlim_cur;
1185 ((soft.saturating_mul(7)) / 10).max(1) as usize
1186 } else {
1187 DEFAULT_AUTO_MAX_FLOWS
1188 }
1189 }
1190 #[cfg(not(unix))]
1191 {
1192 DEFAULT_AUTO_MAX_FLOWS
1193 }
1194 };
1195 if slab_headroom == 0 {
1196 auto
1197 } else {
1198 auto.min(slab_headroom).max(1)
1199 }
1200}
1201
1202/// Clamp a configured `max_rx_datagram_size` to the global `buffer_size`
1203/// (defense in depth): the per-session `recv_buf` is sized `max_rx + 1`, so a
1204/// raw worker `AddUdpListener`/`UpdateUdpListener` carrying e.g.
1205/// `max_rx_datagram_size = u32::MAX` must NOT be able to allocate a ~4 GB
1206/// buffer. A `buffer_size` of 0 (unset) leaves the value untouched.
1207fn clamp_max_rx(configured: usize, buffer_size: usize) -> usize {
1208 if buffer_size == 0 {
1209 configured
1210 } else {
1211 configured.min(buffer_size)
1212 }
1213}
1214
1215/// The `ProxySession` backing one UDP listener. The server's generic readiness
1216/// path drives this (UDP is not in the listen-accept arm). It owns the per-flow
1217/// connected upstream sockets and demuxes events by token.
1218pub struct UdpListenerSession {
1219 /// The listener this session serves.
1220 listener: Rc<RefCell<UdpListener>>,
1221 /// The sans-io manager for this listener.
1222 manager: Rc<RefCell<UdpManager>>,
1223 /// Shared backend map for LB selection.
1224 backends: Rc<RefCell<BackendMap>>,
1225 /// Cloned mio registry for per-flow socket (de)registration.
1226 registry: Registry,
1227 /// Slab the per-flow upstream tokens are inserted into (the same `Rc` the
1228 /// listener-session is registered under: multi-token pattern).
1229 sessions: Rc<RefCell<SessionManager>>,
1230 /// The listener's own token (client recv + timer key).
1231 listener_token: Token,
1232 /// Listener address (for logging).
1233 address: SocketAddr,
1234 /// Per-flow connected upstream sockets, keyed by their slab token.
1235 upstream_sockets: HashMap<Token, UdpSocket>,
1236 /// Per-flow bounded egress queues for the forward path, keyed by the
1237 /// upstream token. A datagram lands here only when the connected upstream
1238 /// socket returns `WouldBlock`; the socket is then reregistered
1239 /// `READABLE | WRITABLE` and drained on the next writable event. Dropped
1240 /// together with the socket on flow close (no leak, gauge stays correct).
1241 upstream_write_queues: HashMap<Token, WriteQueue>,
1242 /// Bounded egress queue for the client-return path (replies fanned back
1243 /// through the single listener socket via `send_to`). Engaged only when the
1244 /// listener socket returns `WouldBlock`; the listener is then reregistered
1245 /// `READABLE | WRITABLE` and drained on its next writable event.
1246 client_write_queue: WriteQueue,
1247 /// `upstream_token -> FlowId` for NAT-return demux.
1248 upstream_to_flow: HashMap<Token, FlowId>,
1249 /// `FlowId -> upstream_token` to tear down on close.
1250 flow_to_upstream: HashMap<FlowId, Token>,
1251 /// `FlowId -> admission Instant` for `udp.flow.duration` on close.
1252 flow_started: HashMap<FlowId, Instant>,
1253 /// `FlowId -> (client, backend)` for the close access log.
1254 flow_endpoints: HashMap<FlowId, (SocketAddr, Option<SocketAddr>)>,
1255 /// The client source whose datagram is currently being drained. Lets
1256 /// `on_send_to_backend` resolve the right per-flow upstream socket for an
1257 /// already-established flow (where no `OpenUpstream` precedes the send).
1258 /// `None` while draining backend-side or timer-driven outputs.
1259 in_flight_client: Option<SocketAddr>,
1260 /// The flow whose upstream was just opened in this drain pass — covers the
1261 /// new-flow path where `OpenUpstream{flow}` immediately precedes the first
1262 /// `SendToBackend`.
1263 in_flight_flow: Option<FlowId>,
1264 /// Shadow of the manager's flow table: client source address → `FlowId`.
1265 /// Lets the shell resolve the owning flow for a `SendToBackend` on an
1266 /// established flow from the in-flight client source. Kept in lockstep with
1267 /// `OpenUpstream` / `CloseFlow`.
1268 client_key_to_flow: HashMap<SocketAddr, FlowId>,
1269 /// Reusable recv scratch buffer, sized to `max_rx_datagram_size`.
1270 recv_buf: Vec<u8>,
1271 /// The currently-armed `TIMER` handle, so a re-arm cancels the previous
1272 /// deadline instead of leaking timer-slab entries.
1273 ///
1274 /// `None` whenever nothing is armed, which includes the moment an expiry is
1275 /// delivered: `Timer::poll` removes the entry from its slab. `timeout`
1276 /// clears it there so the field means what it says locally, rather than
1277 /// holding a handle that is merely harmless. Harmless it is —
1278 /// `cancel_timeout` rejects a delivered handle outright, because
1279 /// `set_timeout_at` clamps every new entry past `self.tick` while a
1280 /// delivered one sat at or below it, so no successor in that slab slot can
1281 /// ever match its tick
1282 /// (`test_a_delivered_timeout_handle_cannot_cancel_its_slot_successor`,
1283 /// `lib/src/timer.rs`). Keeping the invariant local beats resting on that
1284 /// two-hop argument from another module.
1285 timer_handle: Option<crate::timer::Timeout>,
1286}
1287
1288impl UdpListenerSession {
1289 #[allow(clippy::too_many_arguments)]
1290 pub fn new(
1291 listener: Rc<RefCell<UdpListener>>,
1292 manager: Rc<RefCell<UdpManager>>,
1293 backends: Rc<RefCell<BackendMap>>,
1294 registry: Registry,
1295 sessions: Rc<RefCell<SessionManager>>,
1296 listener_token: Token,
1297 ) -> UdpListenerSession {
1298 let (address, max_rx) = {
1299 let l = listener.borrow();
1300 (l.address, l.config.max_rx_datagram_size as usize)
1301 };
1302 UdpListenerSession {
1303 listener,
1304 manager,
1305 backends,
1306 registry,
1307 sessions,
1308 listener_token,
1309 address,
1310 upstream_sockets: HashMap::new(),
1311 upstream_write_queues: HashMap::new(),
1312 client_write_queue: WriteQueue::new(CLIENT_WRITE_QUEUE_CAP),
1313 upstream_to_flow: HashMap::new(),
1314 flow_to_upstream: HashMap::new(),
1315 flow_started: HashMap::new(),
1316 flow_endpoints: HashMap::new(),
1317 in_flight_client: None,
1318 in_flight_flow: None,
1319 client_key_to_flow: HashMap::new(),
1320 // Size the recv scratch to `max_rx + 1`, NOT `max_rx`: a UDP
1321 // `recv_from` truncates the datagram to the buffer length and
1322 // silently discards the tail. If the buffer were exactly `max_rx`,
1323 // an oversized datagram would arrive as a `max_rx`-byte payload —
1324 // indistinguishable from a legal one — and be forwarded truncated.
1325 // The extra byte lets the manager observe `len == max_rx + 1 >
1326 // max_rx` and drop it (`DropReason::Truncated`) instead.
1327 recv_buf: vec![0u8; max_rx.saturating_add(1).max(1)],
1328 timer_handle: None,
1329 }
1330 }
1331
1332 /// Resize the recv scratch buffer to `max_rx + 1` (the same `+ 1` sizing as
1333 /// [`new`](Self::new): the extra byte lets the manager observe an oversized
1334 /// datagram as `len == max_rx + 1 > max_rx` and drop it as `Truncated`
1335 /// rather than silently forwarding a kernel-truncated payload). Called from
1336 /// `UdpProxy::update_listener` when a config push changes the listener's
1337 /// `max_rx_datagram_size`. Resizing larger zero-fills the new tail; resizing
1338 /// smaller truncates the (idle, between-datagram) scratch — both are safe
1339 /// because the buffer holds no live datagram across calls.
1340 fn resize_recv_buf(&mut self, max_rx: usize) {
1341 self.recv_buf.resize(max_rx.saturating_add(1).max(1), 0u8);
1342 }
1343
1344 /// Drain every datagram waiting on the listener socket into the manager.
1345 /// Edge-triggered epoll: loop `recv_from` to `WouldBlock`.
1346 fn ingest_client(&mut self, now: Instant) {
1347 // Pull the socket out behind a short borrow then operate on it.
1348 loop {
1349 let result = {
1350 let listener = self.listener.borrow();
1351 let Some(socket) = listener.socket.as_ref() else {
1352 return;
1353 };
1354 socket.recv_from(&mut self.recv_buf)
1355 };
1356 match result {
1357 Ok((len, src)) => {
1358 // Mark the in-flight client so `on_send_to_backend` can
1359 // resolve the owning flow's upstream socket for an
1360 // already-established flow (no `OpenUpstream` precedes it).
1361 self.in_flight_client = Some(src);
1362 self.in_flight_flow = None;
1363 let len = len.min(self.recv_buf.len());
1364 // Borrow the payload via a split so the mutable borrow of
1365 // `self.manager` does not alias `self.recv_buf`.
1366 let payload: &[u8] = &self.recv_buf[..len];
1367 // SAFETY-free: `handle_input` only reads the slice; the
1368 // borrow checker is satisfied because `recv_buf` and
1369 // `manager` are disjoint fields.
1370 let mgr = self.manager.clone();
1371 // The backend set, borrowed for this admission only. The
1372 // manager selects from it; the shell no longer does
1373 // (#1340, Question 6).
1374 let backends = self.backends.clone();
1375 let mut backends = backends.borrow_mut();
1376 mgr.borrow_mut().handle_input(
1377 ManagerInput::ClientDatagram {
1378 src,
1379 payload,
1380 backends: &mut *backends,
1381 },
1382 now,
1383 );
1384 drop(backends);
1385 // Drain outputs after each datagram to bound queue growth.
1386 self.drain_outputs(now);
1387 self.in_flight_client = None;
1388 }
1389 Err(ref e) if e.kind() == ErrorKind::WouldBlock => break,
1390 Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
1391 Err(e) => {
1392 debug!(
1393 "{} recv_from error on UDP listener: {}",
1394 log_context!(self),
1395 e
1396 );
1397 break;
1398 }
1399 }
1400 }
1401 }
1402
1403 /// Drain datagrams waiting on one flow's connected upstream socket into the
1404 /// manager as `BackendDatagram`s.
1405 fn ingest_upstream(&mut self, upstream_token: Token, now: Instant) {
1406 let Some(&flow) = self.upstream_to_flow.get(&upstream_token) else {
1407 return;
1408 };
1409 loop {
1410 let result = {
1411 let Some(socket) = self.upstream_sockets.get(&upstream_token) else {
1412 return;
1413 };
1414 socket.recv(&mut self.recv_buf)
1415 };
1416 match result {
1417 Ok(len) => {
1418 let len = len.min(self.recv_buf.len());
1419 let payload: &[u8] = &self.recv_buf[..len];
1420 let mgr = self.manager.clone();
1421 mgr.borrow_mut()
1422 .handle_input(ManagerInput::BackendDatagram { flow, payload }, now);
1423 self.drain_outputs(now);
1424 }
1425 Err(ref e) if e.kind() == ErrorKind::WouldBlock => break,
1426 Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
1427 Err(e) => {
1428 debug!(
1429 "{} recv error on upstream socket: {}",
1430 log_context!(self),
1431 e
1432 );
1433 break;
1434 }
1435 }
1436 }
1437 }
1438
1439 /// Drain the manager's output queue, acting on each `Output`.
1440 fn drain_outputs(&mut self, now: Instant) {
1441 let mgr = self.manager.clone();
1442 loop {
1443 let out = mgr.borrow_mut().poll_output();
1444 let Some(out) = out else { break };
1445 match out {
1446 Output::OpenUpstream { flow, backend } => self.on_open_upstream(flow, backend, now),
1447 Output::SendToBackend(transmit) => self.on_send_to_backend(transmit),
1448 Output::SendToClient(transmit) => self.on_send_to_client(transmit),
1449 Output::ArmTimer(deadline) => self.arm_timer(deadline, now),
1450 Output::Metric(ev) => Self::record_metric(ev),
1451 Output::CloseFlow(flow) => self.on_close_flow(flow),
1452 Output::Drop(reason) => Self::record_drop(reason),
1453 }
1454 }
1455 }
1456
1457 fn on_open_upstream(&mut self, flow: FlowId, backend: SocketAddr, now: Instant) {
1458 let mut socket = match udp_connect(backend) {
1459 Ok(socket) => socket,
1460 Err(e) => {
1461 // EMFILE/ENFILE/connect refusal → shed this flow, never panic.
1462 warn!(
1463 "{} could not open upstream socket to {}: {}; shedding flow {}",
1464 log_context!(self),
1465 backend,
1466 e,
1467 flow
1468 );
1469 incr!(names::udp::FLOWS_SHED);
1470 // The manager already moved this flow toward Established and
1471 // counted it; abort it so the `max_flows` slot frees immediately
1472 // (FlowEvicted balances the gauge) instead of squatting until the
1473 // idle timeout. Keep the FLOWS_SHED metric above for the
1474 // EMFILE/refused case. `abort_flow` enqueues FlowEvicted +
1475 // CloseFlow for the surrounding `drain_outputs` loop.
1476 self.manager
1477 .borrow_mut()
1478 .abort_flow(flow, now, CloseReason::Aborted);
1479 return;
1480 }
1481 };
1482 // Multi-token pattern, templated on the back-token allocation in
1483 // `TcpSession::connect_to_backend` (`lib/src/tcp.rs`): a fresh slab
1484 // slot under the SAME listener-session Rc, registered READABLE so its
1485 // readiness reaches `Server::ready` → demuxed back to this session by
1486 // `update_readiness`. The flow-table cap (`max_flows`) already bounds
1487 // how many upstream sockets/slots can exist, so the slab cannot grow
1488 // unbounded here.
1489 let upstream_token = {
1490 let mut s = self.sessions.borrow_mut();
1491 // The listener token indexes a slab slot this session does not
1492 // own: `RemoveListener` frees it (`server.rs`'s `slab.try_remove`)
1493 // without this session ever being told. A deactivate does NOT —
1494 // it re-arms the slot with the inert `ListenSession` placeholder
1495 // (`Server::reserve_listen_token`) — so `RemoveListener` is the
1496 // only path that empties the key under us. Indexing a freed key
1497 // panics the whole worker, so read it fallibly and shed the flow
1498 // instead — an upstream socket registered under a slot nothing
1499 // can demux back is unusable anyway.
1500 let Some(listener_session) = s.slab.get(self.listener_token.0).cloned() else {
1501 drop(s);
1502 error!(
1503 "{} listener token {:?} is no longer in the slab; shedding flow {}",
1504 log_context!(self),
1505 self.listener_token,
1506 flow
1507 );
1508 // No `udp.flows.shed`: that counter documents new flows
1509 // dropped at the `max_flows` cap or under fd pressure
1510 // (doc/configure.md), which this is not. Same choice as the
1511 // registration-failure path just below, which also aborts
1512 // without touching it. Like it, abort the flow the manager
1513 // already counted so its `max_flows` slot frees now
1514 // (FlowEvicted balances the gauge).
1515 self.manager
1516 .borrow_mut()
1517 .abort_flow(flow, now, CloseReason::Aborted);
1518 return;
1519 };
1520 let entry = s.slab.vacant_entry();
1521 let token = Token(entry.key());
1522 entry.insert(listener_session);
1523 token
1524 };
1525 if let Err(e) = self
1526 .registry
1527 .register(&mut socket, upstream_token, Interest::READABLE)
1528 {
1529 error!(
1530 "{} could not register upstream socket: {}",
1531 log_context!(self),
1532 e
1533 );
1534 self.sessions.borrow_mut().slab.try_remove(upstream_token.0);
1535 // The flow is Established in the manager but has no usable upstream
1536 // socket: abort it so its `max_flows` slot frees now (FlowEvicted
1537 // balances the gauge) rather than squatting until idle timeout.
1538 self.manager
1539 .borrow_mut()
1540 .abort_flow(flow, now, CloseReason::Aborted);
1541 return;
1542 }
1543 self.upstream_sockets.insert(upstream_token, socket);
1544 self.upstream_to_flow.insert(upstream_token, flow);
1545 self.flow_to_upstream.insert(flow, upstream_token);
1546 // `flow_to_upstream` and `upstream_to_flow` are inverse maps: the token
1547 // in one points back to the flow in the other. A broken inverse would let
1548 // a backend reply demux to the wrong client (a NAT-return mismatch).
1549 debug_assert_eq!(
1550 self.upstream_to_flow.get(&upstream_token),
1551 Some(&flow),
1552 "upstream_to_flow must map the new token back to its flow"
1553 );
1554 debug_assert_eq!(
1555 self.flow_to_upstream.get(&flow),
1556 Some(&upstream_token),
1557 "flow_to_upstream must map the flow back to its upstream token"
1558 );
1559 self.flow_started.insert(flow, Instant::now());
1560 // The client source for this flow is the one currently in flight.
1561 let client = self.in_flight_client.unwrap_or(self.address);
1562 self.flow_endpoints.insert(flow, (client, Some(backend)));
1563 if let Some(src) = self.in_flight_client {
1564 self.client_key_to_flow.insert(src, flow);
1565 // The shadow flow-table only ever holds live flows: the flow we just
1566 // mapped must have a live upstream token (it is the one we just
1567 // opened). Pairs the on-close drop in `on_close_flow`.
1568 debug_assert!(
1569 self.flow_to_upstream.contains_key(&flow),
1570 "client_key_to_flow points at flow {flow} with no live upstream token"
1571 );
1572 }
1573 // This flow's first SendToBackend (if any) follows immediately.
1574 self.in_flight_flow = Some(flow);
1575 }
1576
1577 fn on_send_to_backend(&mut self, transmit: crate::protocol::udp::Transmit) {
1578 // Resolve the owning flow precisely (NOT by `transmit.dst`, which two
1579 // flows to the same backend would alias — sending on the wrong
1580 // connected socket misroutes that backend's reply to the wrong client).
1581 // * new flow: `OpenUpstream{flow}` set `in_flight_flow` just before.
1582 // * established flow: resolve via the in-flight client source through
1583 // the shell-side `client source -> flow` shadow of the flow table.
1584 let flow = self.in_flight_flow.or_else(|| {
1585 self.in_flight_client
1586 .and_then(|src| self.client_key_to_flow.get(&src).copied())
1587 });
1588 let token = flow.and_then(|f| self.flow_to_upstream.get(&f).copied());
1589 let Some(token) = token else {
1590 // No resolved flow / upstream socket: this is not a queue-full drop.
1591 incr!(names::udp::DROPPED_UNKNOWN_FLOW);
1592 return;
1593 };
1594 let Some(socket) = self.upstream_sockets.get(&token) else {
1595 // Socket already gone (flow closed mid-drain): unknown-flow, not
1596 // queue-full.
1597 incr!(names::udp::DROPPED_UNKNOWN_FLOW);
1598 return;
1599 };
1600 // If a queue is already backed up for this flow, preserve FIFO order —
1601 // do NOT jump the line with the fast-path `send`. Append (or drop on
1602 // overflow) and let the WRITABLE drain catch up.
1603 if let Some(q) = self.upstream_write_queues.get_mut(&token)
1604 && !q.is_empty()
1605 {
1606 if !q.push(transmit.dst, transmit.payload) {
1607 debug!("{} upstream write queue full, dropping", log_context!(self));
1608 incr!(names::udp::DROPPED_WQ_FULL);
1609 }
1610 return;
1611 }
1612 match socket.send(&transmit.payload) {
1613 Ok(_) => {}
1614 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
1615 // Kernel send buffer full: enqueue (bounded) and arm WRITABLE so
1616 // the next writable event drains it. Drop + metric only at cap.
1617 let q = self
1618 .upstream_write_queues
1619 .entry(token)
1620 .or_insert_with(|| WriteQueue::new(UPSTREAM_WRITE_QUEUE_CAP));
1621 if q.push(transmit.dst, transmit.payload) {
1622 self.arm_upstream_writable(token);
1623 } else {
1624 debug!("{} upstream write queue full, dropping", log_context!(self));
1625 incr!(names::udp::DROPPED_WQ_FULL);
1626 }
1627 }
1628 Err(e) => {
1629 debug!("{} upstream send error: {}", log_context!(self), e);
1630 // Hard send error (e.g. ECONNREFUSED), not a queue-full drop.
1631 incr!(names::udp::DROPPED_SEND_ERROR);
1632 }
1633 }
1634 }
1635
1636 /// Reregister a flow's connected upstream socket for `READABLE | WRITABLE`
1637 /// so a queued forward datagram gets a writable wake (the edge-triggered
1638 /// analog of `signal_pending_write`). Idempotent enough — mio coalesces a
1639 /// repeated interest set.
1640 fn arm_upstream_writable(&mut self, token: Token) {
1641 if let Some(socket) = self.upstream_sockets.get_mut(&token)
1642 && let Err(e) =
1643 self.registry
1644 .reregister(socket, token, Interest::READABLE | Interest::WRITABLE)
1645 {
1646 debug!(
1647 "{} could not arm WRITABLE on upstream socket: {}",
1648 log_context!(self),
1649 e
1650 );
1651 }
1652 }
1653
1654 /// Drop a flow's upstream socket back to `READABLE`-only once its write queue
1655 /// has fully drained, so an empty socket no longer wakes the loop on every
1656 /// writable edge (a permanently-WRITABLE UDP socket would busy-loop).
1657 fn disarm_upstream_writable(&mut self, token: Token) {
1658 if let Some(socket) = self.upstream_sockets.get_mut(&token)
1659 && let Err(e) = self.registry.reregister(socket, token, Interest::READABLE)
1660 {
1661 debug!(
1662 "{} could not disarm WRITABLE on upstream socket: {}",
1663 log_context!(self),
1664 e
1665 );
1666 }
1667 }
1668
1669 /// Drain a flow's upstream write queue on a writable event. Re-sends in FIFO
1670 /// order until `WouldBlock` or empty; on empty, drops WRITABLE interest.
1671 fn drain_upstream_queue(&mut self, token: Token) {
1672 let Some(mut queue) = self.upstream_write_queues.remove(&token) else {
1673 return;
1674 };
1675 let socket = self.upstream_sockets.get(&token);
1676 let Some(socket) = socket else {
1677 // Socket gone (flow closed mid-drain): discard the queue.
1678 return;
1679 };
1680 let emptied = queue.drain(|_dst, payload| match socket.send(payload) {
1681 Ok(_) => SendOutcome::Sent,
1682 Err(ref e) if e.kind() == ErrorKind::WouldBlock => SendOutcome::WouldBlock,
1683 Err(_) => SendOutcome::Dropped,
1684 });
1685 if emptied {
1686 self.disarm_upstream_writable(token);
1687 } else {
1688 // Still backed up: put the queue back and keep WRITABLE armed.
1689 self.upstream_write_queues.insert(token, queue);
1690 }
1691 }
1692
1693 fn on_send_to_client(&mut self, transmit: crate::protocol::udp::Transmit) {
1694 // Preserve FIFO: if the client-return queue is already backed up, append
1695 // (or drop on overflow) rather than jumping the line via the fast path.
1696 if !self.client_write_queue.is_empty() {
1697 if !self.client_write_queue.push(transmit.dst, transmit.payload) {
1698 debug!("{} client write queue full, dropping", log_context!(self));
1699 incr!(names::udp::DROPPED_WQ_FULL);
1700 }
1701 return;
1702 }
1703 let send_result = {
1704 let listener = self.listener.borrow();
1705 let Some(socket) = listener.socket.as_ref() else {
1706 return;
1707 };
1708 socket.send_to(&transmit.payload, transmit.dst)
1709 };
1710 match send_result {
1711 Ok(_) => {}
1712 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
1713 // Listener send buffer full: enqueue (bounded) + arm WRITABLE on
1714 // the listener so the next writable event drains it.
1715 if self.client_write_queue.push(transmit.dst, transmit.payload) {
1716 self.arm_client_writable();
1717 } else {
1718 debug!("{} client write queue full, dropping", log_context!(self));
1719 incr!(names::udp::DROPPED_WQ_FULL);
1720 }
1721 }
1722 Err(e) => {
1723 debug!("{} client send_to error: {}", log_context!(self), e);
1724 // Hard send_to error, not a queue-full drop.
1725 incr!(names::udp::DROPPED_SEND_ERROR);
1726 }
1727 }
1728 }
1729
1730 /// Reregister the listener socket for `READABLE | WRITABLE` so a queued
1731 /// client-return datagram gets a writable wake. The listener token routes
1732 /// the writable event back into `update_readiness`.
1733 fn arm_client_writable(&mut self) {
1734 let listener = self.listener.borrow();
1735 let fd = match listener.socket.as_ref() {
1736 Some(socket) => socket.as_raw_fd(),
1737 None => return,
1738 };
1739 if let Err(e) = self.registry.reregister(
1740 &mut SourceFd(&fd),
1741 self.listener_token,
1742 Interest::READABLE | Interest::WRITABLE,
1743 ) {
1744 debug!(
1745 "{} could not arm WRITABLE on listener socket: {}",
1746 log_context!(self),
1747 e
1748 );
1749 }
1750 }
1751
1752 /// Drop the listener socket back to `READABLE`-only once the client-return
1753 /// queue is empty (a permanently-WRITABLE listener would busy-loop).
1754 fn disarm_client_writable(&mut self) {
1755 let listener = self.listener.borrow();
1756 let fd = match listener.socket.as_ref() {
1757 Some(socket) => socket.as_raw_fd(),
1758 None => return,
1759 };
1760 if let Err(e) =
1761 self.registry
1762 .reregister(&mut SourceFd(&fd), self.listener_token, Interest::READABLE)
1763 {
1764 debug!(
1765 "{} could not disarm WRITABLE on listener socket: {}",
1766 log_context!(self),
1767 e
1768 );
1769 }
1770 }
1771
1772 /// Drain the client-return write queue on a listener writable event. Re-sends
1773 /// in FIFO order until `WouldBlock` or empty; on empty, drops WRITABLE.
1774 fn drain_client_queue(&mut self) {
1775 let mut queue = std::mem::replace(&mut self.client_write_queue, WriteQueue::new(0));
1776 let emptied = {
1777 let listener = self.listener.borrow();
1778 let Some(socket) = listener.socket.as_ref() else {
1779 // Listener socket gone: discard the queue and restore an empty one.
1780 self.client_write_queue = WriteQueue::new(CLIENT_WRITE_QUEUE_CAP);
1781 return;
1782 };
1783 queue.drain(|dst, payload| match socket.send_to(payload, *dst) {
1784 Ok(_) => SendOutcome::Sent,
1785 Err(ref e) if e.kind() == ErrorKind::WouldBlock => SendOutcome::WouldBlock,
1786 Err(_) => SendOutcome::Dropped,
1787 })
1788 };
1789 // Restore the (possibly still-backed-up) queue; preserve its capacity.
1790 queue.cap = CLIENT_WRITE_QUEUE_CAP;
1791 self.client_write_queue = queue;
1792 if emptied {
1793 self.disarm_client_writable();
1794 }
1795 }
1796
1797 /// The body of [`ProxySession::timeout`], evaluated at `now` instead of
1798 /// the current instant, so a test can deliver a wheel expiry at an exact
1799 /// point of a simulated clock.
1800 fn timeout_at(&mut self, token: Token, now: Instant) -> SessionIsToBeClosed {
1801 if token == self.listener_token {
1802 // The wheel entry that fired is CONSUMED: `Timer::poll` removed it
1803 // from the timer's slab. Say so, so the field is true on its own
1804 // terms rather than by an argument made in `timer.rs` — a handle
1805 // left here is inert (`cancel_timeout`'s tick guard can never match
1806 // a delivered entry) but claims an arming that does not exist.
1807 self.timer_handle = None;
1808 self.manager.borrow_mut().handle_timeout(now);
1809 // Re-arm: `handle_timeout` cleared the manager's `armed_deadline`
1810 // for the same reason, so `reschedule` emits a fresh ArmTimer
1811 // whenever a flow is still scheduled — including when this expiry
1812 // found nothing due, which is the common case for an entry the
1813 // wheel delivered early (`crate::timer` rounds to the nearest
1814 // tick). `drain_outputs` applies it. Never close the listener on a
1815 // flow timeout.
1816 self.drain_outputs(now);
1817 }
1818 false
1819 }
1820
1821 /// Map the single manager-wide `ArmTimer(deadline)` onto the thread-local
1822 /// `TIMER`, keyed by the **listener token**: when it fires, the run loop
1823 /// calls `Server::timeout(listener_token)` → `session.timeout(token)` →
1824 /// `manager.handle_timeout(now)`.
1825 fn arm_timer(&mut self, deadline: Instant, now: Instant) {
1826 let delay = deadline.saturating_duration_since(now);
1827 TIMER.with(|timer| {
1828 let mut timer = timer.borrow_mut();
1829 // Cancel the previous deadline so re-arming does not leak timer-slab
1830 // entries. This is the re-arm-while-still-armed path (a datagram
1831 // pushed the deadline back); on the expiry path `timeout` has
1832 // already cleared the handle, because the entry it named is gone.
1833 if let Some(old) = self.timer_handle.take() {
1834 let _ = timer.cancel_timeout(&old);
1835 }
1836 // Counted from `now`, the instant `deadline` was computed against,
1837 // not from a fresh clock read: the entry then lands on the grid
1838 // point `deadline` itself maps to, however late this drain runs.
1839 self.timer_handle = Some(timer.set_timeout_from(now, delay, self.listener_token));
1840 });
1841 }
1842
1843 fn on_close_flow(&mut self, flow: FlowId) {
1844 if let Some(token) = self.flow_to_upstream.remove(&flow) {
1845 if let Some(mut socket) = self.upstream_sockets.remove(&token)
1846 && let Err(e) = self.registry.deregister(&mut socket)
1847 {
1848 debug!("{} deregister upstream on close: {}", log_context!(self), e);
1849 }
1850 // Drop any queued (un-drained) forward datagrams with the socket so
1851 // the per-flow queue cannot leak; gauge correctness is preserved
1852 // because the queue holds bytes, not flow-count state.
1853 self.upstream_write_queues.remove(&token);
1854 self.upstream_to_flow.remove(&token);
1855 self.sessions.borrow_mut().slab.try_remove(token.0);
1856 // On close, all per-flow maps drop the flow together: neither
1857 // direction of the upstream-token map may still reference it.
1858 debug_assert!(
1859 !self.upstream_to_flow.values().any(|&f| f == flow),
1860 "on_close_flow left upstream_to_flow referencing closed flow {flow}"
1861 );
1862 debug_assert!(
1863 !self.flow_to_upstream.contains_key(&flow),
1864 "on_close_flow left flow_to_upstream entry for closed flow {flow}"
1865 );
1866 }
1867 if let Some(started) = self.flow_started.remove(&flow) {
1868 let duration = started.elapsed();
1869 time!(names::udp::FLOW_DURATION, duration.as_millis());
1870 }
1871 let (client, backend) = self
1872 .flow_endpoints
1873 .remove(&flow)
1874 .unwrap_or((self.address, None));
1875 // Drop the shadow flow-table entry if it still points at this flow.
1876 if self.client_key_to_flow.get(&client) == Some(&flow) {
1877 self.client_key_to_flow.remove(&client);
1878 }
1879 // The shadow flow-table must no longer map THIS flow id. A surviving
1880 // entry would misroute a later established-flow `SendToBackend` onto a
1881 // freed upstream token.
1882 debug_assert!(
1883 !self.client_key_to_flow.values().any(|&f| f == flow),
1884 "on_close_flow left client_key_to_flow referencing closed flow {flow}"
1885 );
1886 info!("{} flow closed", log_flow_context!(flow, client, backend));
1887 }
1888
1889 fn record_metric(ev: MetricEvent) {
1890 match ev {
1891 MetricEvent::FlowCreated => {
1892 incr!(names::udp::FLOWS_CREATED);
1893 gauge_add!(names::udp::ACTIVE_FLOWS, 1);
1894 }
1895 MetricEvent::FlowEvicted => {
1896 incr!(names::udp::FLOWS_EVICTED);
1897 gauge_add!(names::udp::ACTIVE_FLOWS, -1);
1898 }
1899 MetricEvent::FlowShed => {
1900 incr!(names::udp::FLOWS_SHED);
1901 }
1902 MetricEvent::FlowShedSourceLimit => {
1903 incr!(names::udp::FLOWS_SHED);
1904 incr!(names::udp::FLOWS_SHED_SOURCE_LIMIT);
1905 }
1906 MetricEvent::DatagramIn(bytes) => {
1907 incr!(names::udp::DATAGRAMS_IN);
1908 count!(names::udp::BYTES_IN, bytes as i64);
1909 }
1910 MetricEvent::DatagramOut(bytes) => {
1911 incr!(names::udp::DATAGRAMS_OUT);
1912 count!(names::udp::BYTES_OUT, bytes as i64);
1913 }
1914 MetricEvent::DatagramDropped(reason) => Self::record_drop(reason),
1915 }
1916 }
1917
1918 fn record_drop(reason: DropReason) {
1919 incr!(names::udp::DATAGRAMS_DROPPED);
1920 match reason {
1921 DropReason::Invalid => incr!(names::udp::DROPPED_INVALID),
1922 DropReason::Truncated => incr!(names::udp::DROPPED_TRUNCATED),
1923 DropReason::NoBackend => incr!(names::udp::DROPPED_NO_BACKEND),
1924 DropReason::Shed => incr!(names::udp::DROPPED_SHED),
1925 DropReason::UnknownFlow => incr!(names::udp::DROPPED_UNKNOWN_FLOW),
1926 }
1927 }
1928
1929 /// Tear down every active flow on this listener **through the manager**, so
1930 /// each close emits `FlowEvicted` + `CloseFlow` exactly once and the shell's
1931 /// normal `on_close_flow` handler frees the upstream
1932 /// socket + slab slot and decrements `udp.active_flows`. Used on soft/hard
1933 /// stop, listener remove, and listener deactivate so the worker reaches its
1934 /// `base_sessions_count` and exits promptly instead of waiting out every
1935 /// flow's idle timeout — and so the active-flows gauge does not leak by N
1936 /// (the bug the old direct-teardown path had: it cleared the shell maps
1937 /// without telling the manager, so `FlowEvicted` never fired).
1938 ///
1939 /// On a deactivate where the manager is *retained* (not dropped), this also
1940 /// resets the manager's flow table to empty, keeping manager and shell
1941 /// consistent. The listener socket and the listener-session slab slot are
1942 /// left intact (the listener slot is part of `base_sessions_count` and
1943 /// reclaimed when the worker exits); only the connectionless per-flow slots
1944 /// — which are NOT counted in `nb_connections` and so removed without
1945 /// `decr` — are freed.
1946 pub fn close_all_flows(&mut self, now: Instant) {
1947 // Drive teardown through the manager: it emits one `FlowEvicted` +
1948 // `CloseFlow` per live flow into its output queue. Draining those runs
1949 // `record_metric(FlowEvicted)` (the single gauge decrement) and
1950 // `on_close_flow` (frees socket + slab slot + shell maps) per flow.
1951 self.manager.borrow_mut().close_all(now);
1952 self.drain_outputs(now);
1953 // After `close_all`, the manager's flow table is empty and it armed no
1954 // new timer (an empty table has no deadline, so `reschedule` emits
1955 // nothing). Cancel any residual shell timer so it can't fire against a
1956 // now-flowless listener.
1957 if let Some(handle) = self.timer_handle.take() {
1958 TIMER.with(|timer| {
1959 let _ = timer.borrow_mut().cancel_timeout(&handle);
1960 });
1961 }
1962 }
1963}
1964
1965impl ProxySession for UdpListenerSession {
1966 fn protocol(&self) -> Protocol {
1967 Protocol::UDPListen
1968 }
1969
1970 fn update_readiness(&mut self, token: Token, events: Ready) {
1971 // WRITABLE first: a previously-`WouldBlock` socket can now accept queued
1972 // egress. Drain before reading so the kernel send buffer has room before
1973 // this pass enqueues more (and so a writable-only event still drains).
1974 if events.is_writable() {
1975 if token == self.listener_token {
1976 self.drain_client_queue();
1977 } else if self.upstream_to_flow.contains_key(&token) {
1978 self.drain_upstream_queue(token);
1979 }
1980 }
1981 if !events.is_readable() {
1982 return;
1983 }
1984 let now = Instant::now();
1985 if token == self.listener_token {
1986 self.ingest_client(now);
1987 } else if self.upstream_to_flow.contains_key(&token) {
1988 self.ingest_upstream(token, now);
1989 }
1990 }
1991
1992 fn ready(&mut self, _session: Rc<RefCell<dyn ProxySession>>) -> SessionIsToBeClosed {
1993 // All work happens in `update_readiness` (it has the firing token; the
1994 // generic `ready()` does not). Never close the listener session here —
1995 // it lives for the listener's lifetime.
1996 false
1997 }
1998
1999 fn timeout(&mut self, token: Token) -> SessionIsToBeClosed {
2000 self.timeout_at(token, Instant::now())
2001 }
2002
2003 fn close(&mut self) {
2004 // Tear down any still-live flows THROUGH the manager (FlowEvicted +
2005 // CloseFlow per flow) so `udp.active_flows` balances to zero even if the
2006 // server reaps this listener session directly without a prior
2007 // proxy-driven `close_all_flows`. In the common path
2008 // (`close_all_flows` already ran) the manager has no live flows, so this
2009 // is a cheap no-op. Also cancels the residual idle timer.
2010 self.close_all_flows(Instant::now());
2011 // Any leftover egress queues / shell maps are dropped here (the per-flow
2012 // ones were already freed by the manager-driven close above; this only
2013 // resets the client-return queue, which is not flow-state).
2014 self.upstream_write_queues.clear();
2015 self.client_write_queue = WriteQueue::new(CLIENT_WRITE_QUEUE_CAP);
2016 self.upstream_to_flow.clear();
2017 self.flow_to_upstream.clear();
2018 // Deregister + drop the listener socket. Never shutdown(Both) — UDP has
2019 // no connection to shut down; just deregister + drop the fds.
2020 let mut listener = self.listener.borrow_mut();
2021 if let Some(socket) = listener.socket.as_ref() {
2022 let fd = socket.as_raw_fd();
2023 let _ = self.registry.deregister(&mut SourceFd(&fd));
2024 }
2025 listener.active = false;
2026 }
2027
2028 fn last_event(&self) -> Instant {
2029 // The listener session lives for the listener's lifetime — it is never a
2030 // zombie even when idle. Mirror `ListenSession::last_event` (which
2031 // returns `now`) so the `zombie_check` (which has no listener-protocol
2032 // exclusion) never reaps a quiet UDP listener. Per-flow idle reaping is
2033 // handled by the manager's timer wheel, not the zombie sweep.
2034 Instant::now()
2035 }
2036
2037 fn print_session(&self) {
2038 error!(
2039 "{} UDP listener session: {} active flows, {} upstream sockets",
2040 log_context!(self),
2041 self.manager.borrow().flow_count(),
2042 self.upstream_sockets.len(),
2043 );
2044 }
2045
2046 fn frontend_token(&self) -> Token {
2047 self.listener_token
2048 }
2049
2050 fn shutting_down(&mut self) -> SessionIsToBeClosed {
2051 // The listener session is a *listener*, not a connection: it was never
2052 // counted by `SessionManager::incr` (only accepted sessions are), so it
2053 // MUST NOT be routed through the connection-counted
2054 // `shut_down_sessions_by_frontend_tokens` path — that calls `decr`,
2055 // which underflows `nb_connections` and panics (`assert!(nb != 0)` at
2056 // `server.rs`). This mirrors `ListenSession::shutting_down` (which also
2057 // returns `false`). Soft-stop draining is owned by
2058 // `UdpProxy::notify(SoftStop)`: it flips every manager to `Drain` and
2059 // deregisters the listener sockets, so existing flows reach teardown and
2060 // no new flow is admitted. The slot is reclaimed when the worker exits.
2061 false
2062 }
2063
2064 fn cluster_id(&self) -> Option<ClusterId> {
2065 self.listener.borrow().cluster_id.clone()
2066 }
2067}
2068
2069#[allow(unused_imports)]
2070pub(crate) use {log_context, log_flow_context, log_module_context};
2071
2072#[cfg(test)]
2073mod tests {
2074 use super::*;
2075
2076 use std::cell::Cell;
2077
2078 #[test]
2079 fn effective_max_flows_explicit_value_is_used() {
2080 // An explicit value is honoured verbatim, ignoring the slab headroom
2081 // clamp (the operator opted in).
2082 assert_eq!(effective_max_flows(42, 0), 42);
2083 assert_eq!(effective_max_flows(42, 10), 42);
2084 }
2085
2086 #[test]
2087 fn effective_max_flows_auto_is_positive() {
2088 // 0 = auto: derives ~70% of RLIMIT_NOFILE, always >= 1.
2089 assert!(effective_max_flows(0, 0) >= 1);
2090 }
2091
2092 #[test]
2093 fn effective_max_flows_auto_is_clamped_to_slab_headroom() {
2094 // Auto derivation is capped at the slab headroom (max_connections) so a
2095 // UDP listener can't inflate the shared slab. A headroom of 0 disables
2096 // the clamp.
2097 assert!(effective_max_flows(0, 4) <= 4);
2098 assert!(effective_max_flows(0, 4) >= 1);
2099 }
2100
2101 #[test]
2102 fn clamp_max_rx_respects_buffer_size() {
2103 // The configured rx size is clamped to buffer_size; an unset (0)
2104 // buffer_size leaves it untouched.
2105 assert_eq!(clamp_max_rx(u32::MAX as usize, 16_384), 16_384);
2106 assert_eq!(clamp_max_rx(1_024, 16_384), 1_024);
2107 assert_eq!(clamp_max_rx(u32::MAX as usize, 0), u32::MAX as usize);
2108 }
2109
2110 fn addr(port: u16) -> SocketAddr {
2111 SocketAddr::from(([127, 0, 0, 1], port))
2112 }
2113
2114 /// A fake socket whose `send` outcome is scripted per call. Lets the
2115 /// `WriteQueue` state machine be exercised with zero real I/O: we feed a
2116 /// sequence of `SendOutcome`s and record what was sent in order.
2117 struct FakeSocket {
2118 /// Outcomes returned by successive `send` calls (front = next).
2119 script: RefCell<VecDeque<SendOutcome>>,
2120 /// Payloads that `Sent`/`Dropped` actually consumed, in order.
2121 consumed: RefCell<Vec<Vec<u8>>>,
2122 /// Default outcome once the script is exhausted.
2123 default: Cell<bool>, // true = Sent, false = WouldBlock
2124 }
2125
2126 impl FakeSocket {
2127 fn new(script: Vec<SendOutcome>, default_sent: bool) -> Self {
2128 FakeSocket {
2129 script: RefCell::new(script.into()),
2130 consumed: RefCell::new(Vec::new()),
2131 default: Cell::new(default_sent),
2132 }
2133 }
2134
2135 fn send(&self, payload: &[u8]) -> SendOutcome {
2136 let outcome = self.script.borrow_mut().pop_front().unwrap_or({
2137 if self.default.get() {
2138 SendOutcome::Sent
2139 } else {
2140 SendOutcome::WouldBlock
2141 }
2142 });
2143 if matches!(outcome, SendOutcome::Sent | SendOutcome::Dropped) {
2144 self.consumed.borrow_mut().push(payload.to_vec());
2145 }
2146 outcome
2147 }
2148 }
2149
2150 #[test]
2151 fn write_queue_push_until_full_then_drops() {
2152 let mut q = WriteQueue::new(2);
2153 assert!(q.is_empty());
2154 assert!(q.push(addr(1), vec![1]));
2155 assert!(q.push(addr(2), vec![2]));
2156 assert_eq!(q.len(), 2);
2157 // At capacity: the third push is rejected (caller drops + counts).
2158 assert!(!q.push(addr(3), vec![3]));
2159 assert_eq!(q.len(), 2);
2160 }
2161
2162 #[test]
2163 fn write_queue_drains_in_fifo_order_on_writable() {
2164 let mut q = WriteQueue::new(8);
2165 for i in 0..4u8 {
2166 assert!(q.push(addr(i as u16), vec![i]));
2167 }
2168 // All sends succeed: drain empties the queue, FIFO preserved.
2169 let sock = FakeSocket::new(vec![], true);
2170 let emptied = q.drain(|_dst, payload| sock.send(payload));
2171 assert!(emptied);
2172 assert!(q.is_empty());
2173 assert_eq!(
2174 *sock.consumed.borrow(),
2175 vec![vec![0u8], vec![1u8], vec![2u8], vec![3u8]]
2176 );
2177 }
2178
2179 #[test]
2180 fn write_queue_stops_on_wouldblock_and_resumes() {
2181 let mut q = WriteQueue::new(8);
2182 for i in 0..3u8 {
2183 assert!(q.push(addr(i as u16), vec![i]));
2184 }
2185 // First send ok, then WouldBlock: drain sends one and stops, leaving two.
2186 let sock = FakeSocket::new(vec![SendOutcome::Sent, SendOutcome::WouldBlock], false);
2187 let emptied = q.drain(|_dst, payload| sock.send(payload));
2188 assert!(!emptied);
2189 assert_eq!(q.len(), 2);
2190 assert_eq!(*sock.consumed.borrow(), vec![vec![0u8]]);
2191 // Front is still the second datagram (FIFO preserved across the stall).
2192 assert_eq!(q.queue.front().unwrap().1, vec![1u8]);
2193 // Second writable event: now everything goes through.
2194 let sock2 = FakeSocket::new(vec![], true);
2195 let emptied2 = q.drain(|_dst, payload| sock2.send(payload));
2196 assert!(emptied2);
2197 assert!(q.is_empty());
2198 assert_eq!(*sock2.consumed.borrow(), vec![vec![1u8], vec![2u8]]);
2199 }
2200
2201 #[test]
2202 fn write_queue_hard_error_drops_one_and_continues() {
2203 let mut q = WriteQueue::new(8);
2204 for i in 0..3u8 {
2205 assert!(q.push(addr(i as u16), vec![i]));
2206 }
2207 // Middle datagram hard-errors: it is popped + skipped, the rest proceed.
2208 let sock = FakeSocket::new(
2209 vec![SendOutcome::Sent, SendOutcome::Dropped, SendOutcome::Sent],
2210 true,
2211 );
2212 let emptied = q.drain(|_dst, payload| sock.send(payload));
2213 assert!(emptied);
2214 assert!(q.is_empty());
2215 // The dropped datagram (1) was consumed-for-accounting but the queue is
2216 // empty and the surviving datagrams (0, 2) went out in order.
2217 assert_eq!(
2218 *sock.consumed.borrow(),
2219 vec![vec![0u8], vec![1u8], vec![2u8]]
2220 );
2221 }
2222
2223 #[test]
2224 fn write_queue_empties_cleanly_when_already_empty() {
2225 let mut q = WriteQueue::new(4);
2226 let sock = FakeSocket::new(vec![], true);
2227 // Draining an empty queue is a no-op that reports emptied = true.
2228 let emptied = q.drain(|_dst, payload| sock.send(payload));
2229 assert!(emptied);
2230 assert!(q.is_empty());
2231 assert!(sock.consumed.borrow().is_empty());
2232 }
2233
2234 /// `on_open_upstream` clones the listener session out of the slab to
2235 /// register the new flow's upstream socket under a second token (the
2236 /// multi-token pattern). That slot belongs to the SERVER, not to this
2237 /// session: `RemoveListener` frees it with `slab.try_remove` and tells the
2238 /// session nothing (a deactivate does not — it re-arms the slot with the
2239 /// inert placeholder, see `Server::reserve_listen_token`). Indexing a freed
2240 /// slab key panics, and this one runs on the datagram path of a listener
2241 /// the operator just touched.
2242 ///
2243 /// No datagram sequence reaches the pairing on its own -- `Server::ready`
2244 /// will not dispatch to a session whose slab entry is gone, so the flow
2245 /// that would call this never starts -- which is exactly why the slot is
2246 /// read fallibly rather than trusted: nothing else re-validates
2247 /// `listener_token` for the whole life of the session. The state is
2248 /// synthesized here directly.
2249 ///
2250 /// The lost wakeup end to end: a real `TIMER` wheel, a real
2251 /// `UdpListenerSession`, a real `UdpManager` — and an idle flow that must
2252 /// actually be evicted.
2253 ///
2254 /// `crate::timer` rounds a delay to the NEAREST tick, so the listener's
2255 /// single wheel entry is delivered up to 50 ms BEFORE the flow's idle
2256 /// deadline when the poll lands on the tick grid
2257 /// (`test_timeout_fires_up_to_half_a_tick_early`, `lib/src/timer.rs`) and
2258 /// up to 99 ms off-grid (see `duration_to_tick`), and the delivery
2259 /// CONSUMES the entry. Any earliness at all is enough for what follows.
2260 /// `UdpListenerSession::timeout` then runs `handle_timeout` at a `now`
2261 /// short of the deadline: nothing is due, nothing closes. Unless the
2262 /// manager re-arms on a fire it did not act on, no wheel entry exists any
2263 /// more and this flow is never reaped — it holds its `max_flows` slot, its
2264 /// upstream socket, its slab slot and its `udp.active_flows` count until
2265 /// some *other* flow's deadline happens to move the manager's minimum.
2266 ///
2267 /// The early fire is forced, not hoped for. The wheel sits on a fixed
2268 /// 100 ms grid anchored at the thread-local `TIMER`'s private `start`, so
2269 /// the test first parks a throwaway entry to read one grid point out of the
2270 /// wheel, then places the flow's deadline 30 ms past a later grid point.
2271 /// 30 ms rounds down, so the entry is delivered at that grid point — 30 ms
2272 /// early — and the assertions below check that against the wheel rather
2273 /// than assuming it.
2274 ///
2275 /// The clock is simulated, not the wheel. The real `TIMER`, with its real
2276 /// nearest-tick rounding, is armed with `Timer::set_timeout_from` and
2277 /// polled with `Timer::poll_at`, and the session handles each expiry
2278 /// through `timeout_at` — all at instants the test derives from one
2279 /// `Instant::now()` read. The defect lives entirely in `duration_to_tick`'s
2280 /// arithmetic on those instants, so nothing is lost, and no scheduling
2281 /// delay can move an arming onto the next grid point or push the early
2282 /// fire past the deadline (issue #1619).
2283 ///
2284 /// To SEE THIS RED: remove `self.armed_deadline = None;` from the top of
2285 /// `UdpManager::handle_timeout` (`lib/src/protocol/udp/manager.rs`). The
2286 /// `wheel re-armed after the early fire` assertion fails first, reporting a
2287 /// `next wheel date` of `tv_sec: 18446744073760881` — the `TICK_MAX`
2288 /// sentinel every slot carries when the wheel holds nothing — and with that
2289 /// assertion removed the final one fails with `the idle flow was never
2290 /// evicted, left: 1 right: 0`.
2291 ///
2292 /// The other half of the rule, `self.timer_handle = None;` below, is NOT
2293 /// covered by this test and no test here forces it red — nor could one:
2294 /// a delivered handle is already inert, since `cancel_timeout`'s tick guard
2295 /// can never match the successor in its slab slot
2296 /// (`test_a_delivered_timeout_handle_cannot_cancel_its_slot_successor`,
2297 /// `lib/src/timer.rs`, pins that). Clearing it is hygiene — it keeps the
2298 /// field's meaning local instead of load-bearing on `timer.rs` internals —
2299 /// not a fix for a reachable defect.
2300 #[test]
2301 fn an_early_wheel_fire_still_evicts_the_idle_flow() {
2302 use crate::backends::Backend;
2303 use crate::testing::{ServerParts, prebuild_server, provide_port};
2304
2305 const CLUSTER: &str = "udp-idle-eviction";
2306
2307 let ServerParts {
2308 registry,
2309 sessions,
2310 pool,
2311 backends,
2312 ..
2313 } = prebuild_server(16, 16384, false).expect("could not prebuild a test server");
2314
2315 // A reachable backend so the flow is admitted and established rather
2316 // than dropped by the core's no-backend path (`DropReason::NoBackend`).
2317 backends.borrow_mut().add_backend(
2318 CLUSTER,
2319 Backend::new(
2320 "backend-1",
2321 SocketAddr::from(([127, 0, 0, 1], provide_port())),
2322 None,
2323 None,
2324 None,
2325 ),
2326 );
2327
2328 let address =
2329 sozu_command::proto::command::SocketAddress::new_v4(127, 0, 0, 1, provide_port());
2330 let config = sozu_command::config::ListenerBuilder::new_udp(address)
2331 .to_udp(None)
2332 .expect("could not build a UdpListenerConfig for the test");
2333
2334 let listener_token = {
2335 let mut session_manager = sessions.borrow_mut();
2336 let entry = session_manager.slab.vacant_entry();
2337 let token = Token(entry.key());
2338 entry.insert(Rc::new(RefCell::new(crate::server::ListenSession {
2339 protocol: Protocol::UDPListen,
2340 })));
2341 token
2342 };
2343
2344 let mut proxy = UdpProxy::new(registry, sessions.clone(), pool, backends, 16, 16384);
2345 proxy
2346 .add_listener(config, listener_token)
2347 .expect("could not add the test UDP listener");
2348 proxy
2349 .activate_listener(&address.into(), None)
2350 .expect("could not activate the test UDP listener");
2351 let session = proxy
2352 .build_session(listener_token)
2353 .expect("could not build the test UDP listener session");
2354
2355 // A ruler for the wheel: park a throwaway entry, read the absolute
2356 // instant the wheel would fire it, and leave it there to be consumed
2357 // below (cancelling it would leave a stale `next_tick` behind).
2358 //
2359 // `now` is the simulated clock's origin, and the only clock read that
2360 // steers this test. It is taken inside `TIMER.with`, after the thread-local
2361 // wheel exists, so its private `start` can never lie after it.
2362 let probe_token = Token(usize::MAX);
2363 let (now, grid) = TIMER.with(|timer| {
2364 let mut timer = timer.borrow_mut();
2365 let now = Instant::now();
2366 timer.set_timeout_from(now, Duration::ZERO, probe_token);
2367 let grid = timer
2368 .next_poll_date()
2369 .expect("the probe must put a date on the wheel");
2370 (now, grid)
2371 });
2372
2373 // The grid point the flow's entry will be delivered at: strictly after
2374 // the probe's (so they land in different wheel slots) and far enough
2375 // ahead that the flow is armed before it.
2376 let mut fires_at = grid + Duration::from_millis(100);
2377 while fires_at < now + Duration::from_millis(60) {
2378 fires_at += Duration::from_millis(100);
2379 }
2380 // 30 ms past a grid point rounds DOWN to it: delivered 30 ms early.
2381 let idle_timeout = (fires_at - now) + Duration::from_millis(30);
2382
2383 {
2384 let session = session.borrow_mut();
2385 let mut manager = session.manager.borrow_mut();
2386 manager.handle_input(
2387 ManagerInput::Config(ConfigEvent::SetCluster(ClusterConfig {
2388 cluster: CLUSTER.into(),
2389 front_timeout: idle_timeout,
2390 back_timeout: idle_timeout,
2391 ..Default::default()
2392 })),
2393 now,
2394 );
2395 let mut backends = session.backends.borrow_mut();
2396 manager.handle_input(
2397 ManagerInput::ClientDatagram {
2398 src: SocketAddr::from(([127, 0, 0, 1], 41000)),
2399 payload: b"query",
2400 backends: &mut *backends,
2401 },
2402 now,
2403 );
2404 }
2405 // The real shell: this is what arms the wheel, via `Output::ArmTimer`.
2406 session.borrow_mut().drain_outputs(now);
2407 assert_eq!(
2408 session.borrow().manager.borrow().flow_count(),
2409 1,
2410 "the datagram must admit exactly one flow"
2411 );
2412
2413 // Consume the ruler. Its slot is reset by the poll, so the wheel is
2414 // clean and `next_poll_date` now reports the flow's entry alone.
2415 assert_eq!(
2416 TIMER.with(|timer| timer.borrow_mut().poll_at(grid)),
2417 Some(probe_token),
2418 "the probe entry must be delivered first"
2419 );
2420
2421 // The early fire, measured against the real wheel rather than assumed.
2422 let wheel_at = TIMER
2423 .with(|timer| timer.borrow().next_poll_date())
2424 .expect("the listener's wheel entry must be armed");
2425 let deadline = session
2426 .borrow()
2427 .manager
2428 .borrow()
2429 .poll_timeout()
2430 .expect("the manager must have armed an idle deadline");
2431 assert!(
2432 wheel_at < deadline,
2433 "precondition: the wheel must fire EARLY — wheel {wheel_at:?} < deadline \
2434 {deadline:?} (grid {grid:?}, expected fire {fires_at:?})"
2435 );
2436
2437 // The early fire itself, exactly as `Server`'s run loop drives it, at
2438 // the wheel's own date.
2439 let fire = wheel_at;
2440 assert_eq!(
2441 TIMER.with(|timer| timer.borrow_mut().poll_at(fire)),
2442 Some(listener_token),
2443 "the listener's wheel entry must be delivered at the wheel's date"
2444 );
2445 assert!(
2446 fire < deadline,
2447 "the fire must land before the flow's deadline for this test to mean anything"
2448 );
2449 session.borrow_mut().timeout_at(listener_token, fire);
2450
2451 // Nothing was due, so nothing closed — and the wheel entry is gone.
2452 assert_eq!(
2453 session.borrow().manager.borrow().flow_count(),
2454 1,
2455 "nothing was due at the early fire: the flow must survive it"
2456 );
2457 // The wheel must hold a real, imminent entry again. `next_poll_date`
2458 // mins over every slot and an empty wheel's slots all read `TICK_MAX`,
2459 // so it is `Some` even with nothing armed — the bound is what makes this
2460 // an assertion. The re-armed entry lands one tick past the deadline: the
2461 // manager asks for `deadline`, which is 30 ms past a grid point the wheel
2462 // has already passed, so `set_timeout_at` pushes it to the next one.
2463 let rearmed = TIMER
2464 .with(|timer| timer.borrow().next_poll_date())
2465 .expect("next_poll_date always reports a date");
2466 assert!(
2467 rearmed <= deadline + Duration::from_millis(100),
2468 "wheel re-armed after the early fire: an expiry the manager did not \
2469 act on still consumed the entry, so it owes a fresh ArmTimer \
2470 (next wheel date {rearmed:?}, deadline {deadline:?})"
2471 );
2472
2473 // Past the deadline AND past the grid point the re-arm rounded up to,
2474 // the run loop drains the wheel again.
2475 let later = deadline + Duration::from_millis(150);
2476 while let Some(token) = TIMER.with(|timer| timer.borrow_mut().poll_at(later)) {
2477 session.borrow_mut().timeout_at(token, later);
2478 }
2479 assert_eq!(
2480 session.borrow().manager.borrow().flow_count(),
2481 0,
2482 "an early fire must not lose the wakeup: the idle flow was never evicted"
2483 );
2484 }
2485
2486 /// To SEE THIS RED: restore `s.slab[self.listener_token.0].clone()` in
2487 /// `on_open_upstream` -- this test then panics with "invalid key".
2488 #[test]
2489 fn opening_an_upstream_without_a_listener_slab_entry_sheds_the_flow() {
2490 use crate::testing::{ServerParts, prebuild_server, provide_port};
2491
2492 let ServerParts {
2493 registry,
2494 sessions,
2495 pool,
2496 backends,
2497 ..
2498 } = prebuild_server(16, 16384, false).expect("could not prebuild a test server");
2499
2500 let address =
2501 sozu_command::proto::command::SocketAddress::new_v4(127, 0, 0, 1, provide_port());
2502 let config = sozu_command::config::ListenerBuilder::new_udp(address)
2503 .to_udp(None)
2504 .expect("could not build a UdpListenerConfig for the test");
2505
2506 let listener_token = {
2507 let mut session_manager = sessions.borrow_mut();
2508 let entry = session_manager.slab.vacant_entry();
2509 let token = Token(entry.key());
2510 entry.insert(Rc::new(RefCell::new(crate::server::ListenSession {
2511 protocol: Protocol::UDPListen,
2512 })));
2513 token
2514 };
2515
2516 let mut proxy = UdpProxy::new(registry, sessions.clone(), pool, backends, 16, 16384);
2517 proxy
2518 .add_listener(config, listener_token)
2519 .expect("could not add the test UDP listener");
2520 proxy
2521 .activate_listener(&address.into(), None)
2522 .expect("could not activate the test UDP listener");
2523 let session = proxy
2524 .build_session(listener_token)
2525 .expect("could not build the test UDP listener session");
2526
2527 // The `RemoveListener` that frees the slot behind the session's back.
2528 sessions.borrow_mut().slab.remove(listener_token.0);
2529
2530 let slab_len = sessions.borrow().slab.len();
2531 session.borrow_mut().on_open_upstream(
2532 0,
2533 SocketAddr::from(([127, 0, 0, 1], provide_port())),
2534 Instant::now(),
2535 );
2536 assert_eq!(
2537 sessions.borrow().slab.len(),
2538 slab_len,
2539 "a shed flow must not leave an upstream slab slot behind"
2540 );
2541 }
2542}
2543
2544#[cfg(test)]
2545mod listener_sibling_tests {
2546 use sozu_command::{config::ListenerBuilder, proto::command::SocketAddress};
2547
2548 use super::*;
2549 use crate::testing::{ServerParts, prebuild_server, provide_port};
2550
2551 fn add(proxy: &mut UdpProxy, address: SocketAddress, interface: Option<&str>, token: usize) {
2552 let config = ListenerBuilder::new_udp(address)
2553 .with_interface(interface)
2554 .to_udp(None)
2555 .expect("could not build the listener config");
2556 proxy
2557 .add_listener(config, Token(token))
2558 .expect("could not add the listener");
2559 }
2560
2561 /// A UDP listener added on an address that already routes to a cluster
2562 /// takes that route over from the listener already there, in its own
2563 /// state and in its flow manager, so it forwards from its first datagram
2564 /// (sozu-proxy/sozu#719).
2565 #[test]
2566 fn a_listener_added_next_to_a_sibling_takes_over_its_cluster() {
2567 let ServerParts {
2568 registry,
2569 sessions,
2570 pool,
2571 backends,
2572 ..
2573 } = prebuild_server(16, 16384, false).expect("could not prebuild a test server");
2574 let mut proxy = UdpProxy::new(registry, sessions, pool, backends, 16, 16384);
2575 let address = SocketAddress::new_v4(0, 0, 0, 0, provide_port());
2576 add(&mut proxy, address, None, 0);
2577 proxy
2578 .add_udp_front(RequestUdpFrontend {
2579 cluster_id: "cluster-a".to_owned(),
2580 address,
2581 ..Default::default()
2582 })
2583 .expect("add the frontend");
2584 add(&mut proxy, address, Some("lo"), 1);
2585
2586 assert_eq!(
2587 proxy.listeners[&Token(1)].borrow().cluster_id.as_deref(),
2588 Some("cluster-a"),
2589 "the listener added after the frontend must route to its cluster"
2590 );
2591 assert_eq!(
2592 proxy
2593 .cluster_for_listener
2594 .get(&Token(1))
2595 .map(|c| c.as_ref()),
2596 Some("cluster-a"),
2597 "the flow manager side must know the cluster too"
2598 );
2599 }
2600}