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`](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 logging::ansi_palette,
45 proto::command::{
46 Cluster, LoadBalancingAlgorithms, LoadMetric, RequestUdpFrontend, UdpAffinityKey,
47 UdpListenerConfig, UpdateUdpListenerConfig, WorkerRequest, WorkerResponse,
48 request::RequestType,
49 },
50};
51
52use crate::metrics::names;
53use crate::{
54 CachedTags, ListenerError, ListenerHandler, Protocol, ProxyError, ProxySession,
55 SessionIsToBeClosed,
56 backends::BackendMap,
57 pool::Pool,
58 protocol::udp::{
59 CloseReason, ClusterConfig, ConfigEvent, DropReason, FlowId, ManagerInput, MetricEvent,
60 Output, UdpManager,
61 },
62 server::{SessionManager, TIMER},
63 socket::{udp_bind, udp_connect},
64 sozu_command::{ready::Ready, state::ClusterId},
65};
66
67mod health;
68pub use health::UdpHealthChecker;
69
70/// Per-session log envelope (tag `UDP`). The colored output uses the unified
71/// scheme: bold bright-white protocol label, light-grey keyword, gray keys and
72/// bright-white values. Honours the `colored` flag via [`ansi_palette`].
73macro_rules! log_context {
74 ($self:expr) => {{
75 let (open, reset, grey, gray, white) = ansi_palette();
76 format!(
77 "[- - - -]\t{open}UDP{reset}\t{grey}Listener{reset}({gray}token{reset}={white}{token}{reset}, {gray}address{reset}={white}{address}{reset})\t >>>",
78 open = open,
79 reset = reset,
80 grey = grey,
81 gray = gray,
82 white = white,
83 token = $self.listener_token.0,
84 address = $self.address,
85 )
86 }};
87}
88
89/// Module-level prefix for [`UdpProxy`] callbacks (notify, listener add/remove,
90/// soft/hard stop) which own a listener/token map but have no per-session
91/// token. Produces a bold bright-white `UDP` label in colored mode.
92macro_rules! log_module_context {
93 () => {{
94 let (open, reset, _, _, _) = sozu_command::logging::ansi_palette();
95 format!("{open}UDP{reset}\t >>>", open = open, reset = reset)
96 }};
97}
98
99/// Per-flow log envelope (tag `UDP-FLOW`). Renders the flow's stable id plus
100/// client/backend addresses so flow lines stay filterable.
101macro_rules! log_flow_context {
102 ($flow:expr, $client:expr, $backend:expr) => {{
103 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
104 format!(
105 "[- - - -]\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 >>>",
106 open = open,
107 reset = reset,
108 grey = grey,
109 gray = gray,
110 white = white,
111 id = $flow,
112 client = $client,
113 backend = $backend,
114 )
115 }};
116}
117
118/// Bound on the per-flow upstream write queue (return path to one backend). A
119/// connected upstream socket only buffers one flow's traffic, so a small cap is
120/// enough to ride out a transient `EWOULDBLOCK` (kernel send buffer momentarily
121/// full) without letting a slow/stalled backend balloon memory. Past the cap we
122/// drop the datagram (`udp.datagrams.dropped.wq_full`) — UDP is best-effort and
123/// the client retries.
124const UPSTREAM_WRITE_QUEUE_CAP: usize = 64;
125/// Bound on the per-listener client-return write queue (replies fanned back to
126/// many clients through the single listener socket). Sized larger than the
127/// per-flow cap because it is shared across every flow's return traffic.
128const CLIENT_WRITE_QUEUE_CAP: usize = 256;
129
130/// What `WriteQueue::drain` should do after a single send attempt: keep
131/// draining, stop because the socket went `WouldBlock` (rearm WRITABLE), or stop
132/// because the send hit a hard error (drop the datagram, keep draining the rest).
133enum SendOutcome {
134 /// The datagram was written; pop it and continue.
135 Sent,
136 /// `WouldBlock`: leave the datagram at the front and stop draining.
137 WouldBlock,
138 /// A hard error (e.g. ECONNREFUSED): drop this datagram + count it, continue.
139 Dropped,
140}
141
142/// A bounded FIFO of datagrams awaiting a writable socket. The egress fast path
143/// (a `send`/`send_to` that succeeds immediately) never touches this; it is only
144/// engaged when the kernel send buffer is full (`WouldBlock`). On overflow the
145/// oldest-still-pending order is preserved and the *new* datagram is dropped
146/// (the queued ones are closer to leaving the socket).
147///
148/// The queue stores `(SocketAddr, Vec<u8>)`: the upstream variant ignores the
149/// address (a connected socket's `send` has an implicit destination) while the
150/// client-return variant uses it as the `send_to` destination. Keeping one type
151/// for both lets the drain loop and the unit tests stay shared.
152struct WriteQueue {
153 queue: VecDeque<(SocketAddr, Vec<u8>)>,
154 cap: usize,
155}
156
157impl WriteQueue {
158 fn new(cap: usize) -> Self {
159 WriteQueue {
160 queue: VecDeque::new(),
161 cap,
162 }
163 }
164
165 fn is_empty(&self) -> bool {
166 self.queue.is_empty()
167 }
168
169 /// Current depth. Only used by the unit tests; the runtime drains by the
170 /// `is_empty` / `drain` return value, not a length read.
171 #[cfg(test)]
172 fn len(&self) -> usize {
173 self.queue.len()
174 }
175
176 /// Enqueue a datagram for later draining. Returns `true` if it was accepted,
177 /// `false` if the queue was at capacity (caller drops + counts `wq_full`).
178 #[must_use]
179 fn push(&mut self, dst: SocketAddr, payload: Vec<u8>) -> bool {
180 if self.queue.len() >= self.cap {
181 return false;
182 }
183 self.queue.push_back((dst, payload));
184 // Invariant: the bounded FIFO never grows past its cap. The guard above
185 // is the only growth site, so depth <= cap holds after every push.
186 debug_assert!(
187 self.queue.len() <= self.cap,
188 "WriteQueue overran its cap: len {} > cap {}",
189 self.queue.len(),
190 self.cap,
191 );
192 true
193 }
194
195 /// Drain in FIFO order, calling `send` for each datagram. Stops on the first
196 /// `WouldBlock` (leaving that datagram queued for the next writable event) or
197 /// when empty. Hard-errored datagrams are popped and counted via
198 /// `SendOutcome::Dropped`. Returns `true` if the queue is now empty (the
199 /// caller can drop WRITABLE interest back to READABLE-only).
200 fn drain<F: FnMut(&SocketAddr, &[u8]) -> SendOutcome>(&mut self, mut send: F) -> bool {
201 while let Some((dst, payload)) = self.queue.front() {
202 match send(dst, payload) {
203 SendOutcome::Sent | SendOutcome::Dropped => {
204 self.queue.pop_front();
205 }
206 SendOutcome::WouldBlock => break,
207 }
208 }
209 self.queue.is_empty()
210 }
211}
212
213/// One UDP listener: a single `mio::net::UdpSocket` plus its routing config.
214/// Unlike a TCP listener there is no accept loop — a readable event is a batch
215/// of datagrams the session drains to `WouldBlock`.
216pub struct UdpListener {
217 active: SessionIsToBeClosed,
218 address: SocketAddr,
219 cluster_id: Option<String>,
220 config: UdpListenerConfig,
221 socket: Option<UdpSocket>,
222 tags: BTreeMap<String, CachedTags>,
223 token: Token,
224}
225
226impl ListenerHandler for UdpListener {
227 fn get_addr(&self) -> &SocketAddr {
228 &self.address
229 }
230
231 fn get_tags(&self, key: &str) -> Option<&CachedTags> {
232 self.tags.get(key)
233 }
234
235 fn set_tags(&mut self, key: String, tags: Option<BTreeMap<String, String>>) {
236 match tags {
237 Some(tags) => self.tags.insert(key, CachedTags::new(tags)),
238 None => self.tags.remove(&key),
239 };
240 }
241
242 fn protocol(&self) -> Protocol {
243 Protocol::UDP
244 }
245
246 fn public_address(&self) -> SocketAddr {
247 self.config
248 .public_address
249 .map(|addr| addr.into())
250 .unwrap_or(self.address)
251 }
252}
253
254impl UdpListener {
255 fn new(config: UdpListenerConfig, token: Token) -> Result<UdpListener, ListenerError> {
256 Ok(UdpListener {
257 cluster_id: None,
258 socket: None,
259 token,
260 address: config.address.into(),
261 config,
262 active: false,
263 tags: BTreeMap::new(),
264 })
265 }
266
267 /// Validate that a worker can build this UDP listener configuration WITHOUT
268 /// constructing the full listener or binding a socket. UDP listener
269 /// construction has no fallible config today (no rustls context, no answer
270 /// templates; a bad bind surfaces later as an `ActivateListener` failure),
271 /// so this currently always succeeds. It exists for surface parity with the
272 /// HTTP/HTTPS validators the main process calls before committing an
273 /// `Add*Listener` to `ConfigState` and fanning it out (sozu#1301), and is
274 /// the hook for any future UDP-config validation.
275 pub fn validate_config(_config: &UdpListenerConfig) -> Result<(), ListenerError> {
276 Ok(())
277 }
278
279 /// Bind (or adopt an SCM-passed) socket and register it `READABLE`. The
280 /// READABLE registration is what drives `Server::ready` for this listener —
281 /// there is no accept path.
282 pub fn activate(
283 &mut self,
284 registry: &Registry,
285 udp_socket: Option<UdpSocket>,
286 ) -> Result<Token, ProxyError> {
287 if self.active {
288 return Ok(self.token);
289 }
290
291 let mut socket = match udp_socket {
292 Some(socket) => socket,
293 None => {
294 let address: SocketAddr = self.config.address.into();
295 udp_bind(address).map_err(|e| ProxyError::BindToSocket(address, e))?
296 }
297 };
298
299 registry
300 .register(&mut socket, self.token, Interest::READABLE)
301 .map_err(ProxyError::RegisterListener)?;
302
303 self.socket = Some(socket);
304 self.active = true;
305 Ok(self.token)
306 }
307
308 /// Apply a partial-update patch to this UDP listener's live config. Fields
309 /// absent in the patch (`None`) are preserved.
310 pub fn update_config(&mut self, patch: &UpdateUdpListenerConfig) {
311 if let Some(v) = patch.public_address {
312 self.config.public_address = Some(v);
313 }
314 if let Some(v) = patch.front_timeout {
315 self.config.front_timeout = v;
316 }
317 if let Some(v) = patch.back_timeout {
318 self.config.back_timeout = v;
319 }
320 if let Some(v) = patch.max_rx_datagram_size {
321 self.config.max_rx_datagram_size = v;
322 }
323 if let Some(v) = patch.max_flows {
324 self.config.max_flows = v;
325 }
326 }
327}
328
329/// The UDP proxy. Holds the listeners, one `UdpManager` per listener token, the
330/// shared `BackendMap`, the session slab and a cloned registry. Does NOT
331/// implement `ProxyConfiguration` / `L7Proxy`; the server drives it through the
332/// inherent [`notify`](Self::notify) plus the activate/give-back helpers.
333pub struct UdpProxy {
334 fronts: HashMap<String, Token>,
335 backends: Rc<RefCell<BackendMap>>,
336 listeners: HashMap<Token, Rc<RefCell<UdpListener>>>,
337 /// The built listener session per listener token. The server inserts the
338 /// same `Rc` into the slab at the listener token; the proxy keeps a clone so
339 /// it can drive per-flow teardown (soft/hard stop) without downcasting the
340 /// slab's `dyn ProxySession`. Cleared on listener removal/stop.
341 listener_sessions: HashMap<Token, Rc<RefCell<UdpListenerSession>>>,
342 /// One sans-io manager per listener token, sharing the listener's lifecycle.
343 managers: HashMap<Token, Rc<RefCell<UdpManager>>>,
344 /// Cluster routing for each listener token (set by the UDP frontend).
345 cluster_for_listener: HashMap<Token, ClusterId>,
346 /// Last `AddCluster`-supplied UDP knobs per cluster id. Cached so a frontend
347 /// added AFTER its cluster still picks up `responses` / `requests` / PPv2 /
348 /// affinity — `AddCluster` and `AddUdpFrontend` arrive in either order, and
349 /// neither must clobber the other's contribution to a manager's
350 /// `ClusterConfig`.
351 cluster_udp_config: HashMap<ClusterId, sozu_command::proto::command::UdpClusterConfig>,
352 registry: Registry,
353 sessions: Rc<RefCell<SessionManager>>,
354 #[allow(dead_code)]
355 pool: Rc<RefCell<Pool>>,
356 /// Fixed hash seed injected once into every manager. It is deliberately the
357 /// same constant on every worker and across restarts so HRW/Maglev affinity
358 /// is reproducible cluster-wide — a per-worker random seed would scatter the
359 /// same flow key onto a different backend on each worker and reshuffle it on
360 /// every restart, defeating the documented affinity-stability contract (see
361 /// [`crate::load_balancing::DEFAULT_HASH_SEED`]).
362 hash_seed: u64,
363 /// Global `max_connections` (from `ServerConfig`). Used to clamp a UDP
364 /// listener's auto `max_flows` so a single listener cannot inflate the
365 /// shared `SessionManager` slab and starve HTTP/TCP.
366 max_connections: usize,
367 /// Global `buffer_size` (from `ServerConfig`). Used to clamp a listener's
368 /// `max_rx_datagram_size` so a hostile `AddUdpListener` cannot allocate a
369 /// multi-GB recv buffer.
370 buffer_size: usize,
371 /// Endpoint-bound active health prober (TCP probe + hysteresis +
372 /// fail-open). Driven from the server event loop via
373 /// [`UdpProxy::health_poll`] / [`UdpProxy::health_ready`].
374 health: UdpHealthChecker,
375}
376
377impl UdpProxy {
378 pub fn new(
379 registry: Registry,
380 sessions: Rc<RefCell<SessionManager>>,
381 pool: Rc<RefCell<Pool>>,
382 backends: Rc<RefCell<BackendMap>>,
383 max_connections: usize,
384 buffer_size: usize,
385 ) -> UdpProxy {
386 // Fixed seed, identical on every worker and across restarts: HRW/Maglev
387 // affinity must be reproducible cluster-wide. A per-worker/per-restart
388 // random seed (as a `process::id()` + `Instant::now()` mix would be)
389 // would route the same flow key to a different backend on each worker
390 // and reshuffle it on every restart. Reuse the LB module's canonical
391 // affinity seed so UDP and the HTTP/TCP affinity hashers agree.
392 let hash_seed = crate::load_balancing::DEFAULT_HASH_SEED;
393 UdpProxy {
394 backends,
395 listeners: HashMap::new(),
396 listener_sessions: HashMap::new(),
397 managers: HashMap::new(),
398 cluster_for_listener: HashMap::new(),
399 cluster_udp_config: HashMap::new(),
400 fronts: HashMap::new(),
401 registry,
402 sessions,
403 pool,
404 hash_seed,
405 max_connections,
406 buffer_size,
407 health: UdpHealthChecker::new(),
408 }
409 }
410
411 /// Drive the UDP health prober one event-loop step (server calls this once
412 /// per iteration, mirroring `HealthChecker::poll`). Non-blocking.
413 pub fn health_poll(&mut self) {
414 let registry = self.registry.try_clone();
415 if let Ok(registry) = registry {
416 self.health.poll(&self.backends, ®istry);
417 }
418 }
419
420 /// Record mio readiness for a UDP health-probe socket.
421 pub fn health_ready(&mut self, token: Token) {
422 self.health.ready(token);
423 }
424
425 /// Whether `token` is a UDP health-probe socket this proxy owns.
426 pub fn health_owns_token(&self, token: Token) -> bool {
427 self.health.owns_token(token)
428 }
429
430 pub fn add_listener(
431 &mut self,
432 config: UdpListenerConfig,
433 token: Token,
434 ) -> Result<Token, ProxyError> {
435 match self.listeners.entry(token) {
436 Entry::Vacant(entry) => {
437 let mut config = config;
438 let max_flows = effective_max_flows(config.max_flows, self.max_connections);
439 // Defense in depth: cap the recv-buffer sizing to `buffer_size`
440 // so a hostile `AddUdpListener` (`max_rx_datagram_size =
441 // u32::MAX`) can't allocate a multi-GB buffer. Write the clamped
442 // value back into the stored config so the manager AND the
443 // session's `recv_buf` (sized from `config` in `new()`) agree on
444 // the same bound.
445 let max_rx = clamp_max_rx(config.max_rx_datagram_size as usize, self.buffer_size);
446 config.max_rx_datagram_size = max_rx as u32;
447 let front = Duration::from_secs(u64::from(config.front_timeout));
448 let back = Duration::from_secs(u64::from(config.back_timeout));
449 let listener = UdpListener::new(config, token).map_err(ProxyError::AddListener)?;
450 entry.insert(Rc::new(RefCell::new(listener)));
451 let cluster_cfg = ClusterConfig {
452 front_timeout: front,
453 back_timeout: back,
454 ..Default::default()
455 };
456 self.managers.insert(
457 token,
458 Rc::new(RefCell::new(UdpManager::new(
459 cluster_cfg,
460 max_flows,
461 max_rx,
462 self.hash_seed,
463 ))),
464 );
465 Ok(token)
466 }
467 _ => Err(ProxyError::ListenerAlreadyPresent),
468 }
469 }
470
471 pub fn remove_listener(&mut self, address: SocketAddr) -> SessionIsToBeClosed {
472 let len = self.listeners.len();
473 let mut removed_tokens = Vec::new();
474 self.listeners.retain(|token, l| {
475 if l.borrow().address == address {
476 removed_tokens.push(*token);
477 false
478 } else {
479 true
480 }
481 });
482 let now = Instant::now();
483 for token in removed_tokens {
484 self.cluster_for_listener.remove(&token);
485 // Drive per-flow teardown THROUGH the manager (emits FlowEvicted +
486 // CloseFlow per flow) BEFORE dropping the manager, so the
487 // active-flows gauge is decremented once per flow and the per-flow
488 // upstream sockets + slab slots are freed. Removing the manager first
489 // would silently leak the gauge by N.
490 if let Some(session) = self.listener_sessions.remove(&token) {
491 session.borrow_mut().close_all_flows(now);
492 }
493 self.managers.remove(&token);
494 }
495 self.listeners.len() < len
496 }
497
498 pub fn activate_listener(
499 &self,
500 addr: &SocketAddr,
501 udp_socket: Option<UdpSocket>,
502 ) -> Result<Token, ProxyError> {
503 let listener = self
504 .listeners
505 .values()
506 .find(|listener| listener.borrow().address == *addr)
507 .ok_or(ProxyError::NoListenerFound(*addr))?;
508
509 listener.borrow_mut().activate(&self.registry, udp_socket)
510 }
511
512 /// Build the [`UdpListenerSession`] that drives this listener's datagrams.
513 /// The server inserts the returned session into the slab **at the listener
514 /// token**, replacing the `ListenSession` placeholder, so the generic
515 /// readiness path reaches `UdpListenerSession::update_readiness`. Returns
516 /// `None` if the listener token is unknown.
517 pub fn build_session(&mut self, token: Token) -> Option<Rc<RefCell<UdpListenerSession>>> {
518 let listener = self.listeners.get(&token)?.clone();
519 let manager = self.managers.get(&token)?.clone();
520 let registry = self.registry.try_clone().ok()?;
521 let session = Rc::new(RefCell::new(UdpListenerSession::new(
522 listener,
523 manager,
524 self.backends.clone(),
525 registry,
526 self.sessions.clone(),
527 token,
528 )));
529 // Keep a clone so soft/hard stop can drive per-flow teardown.
530 self.listener_sessions.insert(token, session.clone());
531 Some(session)
532 }
533
534 pub fn give_back_listeners(&mut self) -> Vec<(SocketAddr, UdpSocket)> {
535 self.listeners
536 .values()
537 .filter_map(|listener| {
538 let mut owned = listener.borrow_mut();
539 if let Some(socket) = owned.socket.take() {
540 owned.active = false;
541 return Some((owned.address, socket));
542 }
543 None
544 })
545 .collect()
546 }
547
548 pub fn give_back_listener(
549 &mut self,
550 address: SocketAddr,
551 ) -> Result<(Token, UdpSocket), ProxyError> {
552 let listener = self
553 .listeners
554 .values()
555 .find(|listener| listener.borrow().address == address)
556 .ok_or(ProxyError::NoListenerFound(address))?;
557
558 let (token, taken) = {
559 let mut owned = listener.borrow_mut();
560 let taken = owned.socket.take().ok_or(ProxyError::UnactivatedListener)?;
561 owned.active = false;
562 (owned.token, taken)
563 };
564 // Deactivating removes the listener from service: tear down its active
565 // flows THROUGH the manager so the per-flow upstream slab slots + fds
566 // don't dangle and the active-flows gauge is decremented once per flow
567 // (the manager is RETAINED here — not removed from `self.managers` — so
568 // `close_all` also resets its flow table, keeping manager and shell
569 // consistent for a later reactivate).
570 if let Some(session) = self.listener_sessions.remove(&token) {
571 session.borrow_mut().close_all_flows(Instant::now());
572 }
573 Ok((token, taken))
574 }
575
576 pub fn update_listener(&mut self, patch: UpdateUdpListenerConfig) -> Result<(), ProxyError> {
577 let address: SocketAddr = patch.address.into();
578 let listener = self
579 .listeners
580 .values()
581 .find(|l| l.borrow().address == address)
582 .ok_or(ProxyError::NoListenerFound(address))?;
583 {
584 let mut l = listener.borrow_mut();
585 l.update_config(&patch);
586 // Clamp the stored rx size in place (defense in depth): keep the
587 // listener config, the manager, and the session's `recv_buf` agreeing
588 // on the same `buffer_size`-bounded value.
589 l.config.max_rx_datagram_size =
590 clamp_max_rx(l.config.max_rx_datagram_size as usize, self.buffer_size) as u32;
591 }
592
593 // Reflect the timeout / cap / rx-size changes into the manager for new
594 // flows. Existing flows keep their captured config (stable contract).
595 if let Some(token) = self
596 .listeners
597 .iter()
598 .find(|(_, l)| l.borrow().address == address)
599 .map(|(t, _)| *t)
600 && let Some(mgr) = self.managers.get(&token)
601 {
602 let now = Instant::now();
603 let (cfg, max_flows, max_rx) = {
604 let l = listener.borrow();
605 (
606 self.cluster_config_for(&l, token),
607 effective_max_flows(l.config.max_flows, self.max_connections),
608 // Defense in depth: clamp the raised rx size to `buffer_size`
609 // so a hostile `UpdateUdpListener` can't grow the recv buffer
610 // past the global cap.
611 clamp_max_rx(l.config.max_rx_datagram_size as usize, self.buffer_size),
612 )
613 };
614 {
615 let mut m = mgr.borrow_mut();
616 m.handle_input(ManagerInput::Config(ConfigEvent::SetCluster(cfg)), now);
617 m.handle_input(
618 ManagerInput::Config(ConfigEvent::SetMaxFlows(max_flows)),
619 now,
620 );
621 m.handle_input(
622 ManagerInput::Config(ConfigEvent::SetMaxRxDatagramSize(max_rx)),
623 now,
624 );
625 }
626 // Resize the live session's recv scratch to match the (clamped) new
627 // rx size. Without this, a config that RAISES `max_rx_datagram_size`
628 // would leave `recv_buf` at its old (smaller) length: `recv_from`
629 // would kernel-truncate datagrams between the old and new size while
630 // the manager's `len > max_rx` check (now the larger value) passes
631 // them, forwarding them truncated. The `+ 1` mirrors `new()`: it lets
632 // the manager observe `len == max_rx + 1 > max_rx` and drop an
633 // oversized datagram (`DropReason::Truncated`) instead of silently
634 // truncating it. Existing flows keep their captured config; only the
635 // shared recv buffer tracks the live rx size.
636 if let Some(session) = self.listener_sessions.get(&token) {
637 session.borrow_mut().resize_recv_buf(max_rx);
638 }
639 }
640 Ok(())
641 }
642
643 pub fn add_udp_front(&mut self, front: RequestUdpFrontend) -> Result<(), ProxyError> {
644 let address = front.address.into();
645 let token = {
646 let mut listener = self
647 .listeners
648 .values()
649 .find(|l| l.borrow().address == address)
650 .ok_or(ProxyError::NoListenerFound(address))?
651 .borrow_mut();
652 self.fronts
653 .insert(front.cluster_id.to_string(), listener.token);
654 listener.set_tags(address.to_string(), Some(front.tags));
655 listener.cluster_id = Some(front.cluster_id.clone());
656 listener.token
657 };
658 self.cluster_for_listener
659 .insert(token, front.cluster_id.clone());
660
661 // Commit the cluster routing into the manager so admitted flows know
662 // which cluster to `SelectBackend` against.
663 if let Some(mgr) = self.managers.get(&token) {
664 let listener = self.listeners.get(&token).unwrap();
665 let cfg = {
666 let l = listener.borrow();
667 self.cluster_config_for(&l, token)
668 };
669 mgr.borrow_mut().handle_input(
670 ManagerInput::Config(ConfigEvent::SetCluster(cfg)),
671 Instant::now(),
672 );
673 }
674 Ok(())
675 }
676
677 pub fn remove_udp_front(&mut self, front: RequestUdpFrontend) -> Result<(), ProxyError> {
678 let address = front.address.into();
679 let token = {
680 let mut listener = match self
681 .listeners
682 .values()
683 .find(|l| l.borrow().address == address)
684 {
685 Some(l) => l.borrow_mut(),
686 None => return Err(ProxyError::NoListenerFound(address)),
687 };
688 listener.set_tags(address.to_string(), None);
689 if let Some(cluster_id) = listener.cluster_id.take() {
690 self.fronts.remove(&cluster_id);
691 }
692 listener.token
693 };
694 self.cluster_for_listener.remove(&token);
695 // Drop the routing in the manager — new datagrams now have no backend.
696 if let Some(mgr) = self.managers.get(&token) {
697 mgr.borrow_mut().handle_input(
698 ManagerInput::Config(ConfigEvent::SetCluster(ClusterConfig::default())),
699 Instant::now(),
700 );
701 }
702 Ok(())
703 }
704
705 /// Build a [`ClusterConfig`] for a listener from its current frontend
706 /// cluster routing + timeouts. The per-cluster knobs (responses/requests/
707 /// PPv2/affinity) are populated by [`apply_cluster`](Self::apply_cluster)
708 /// when an `AddCluster` carries a `udp` block; absent that they stay at the
709 /// proto defaults.
710 fn cluster_config_for(&self, listener: &UdpListener, _token: Token) -> ClusterConfig {
711 let cluster = listener.cluster_id.clone().unwrap_or_default();
712 let mut cfg = ClusterConfig {
713 cluster: cluster.clone(),
714 front_timeout: Duration::from_secs(u64::from(listener.config.front_timeout)),
715 back_timeout: Duration::from_secs(u64::from(listener.config.back_timeout)),
716 ..Default::default()
717 };
718 // Fold in the cluster's cached UDP knobs (set by a prior or later
719 // `AddCluster`) so the order of `AddCluster` vs `AddUdpFrontend` does not
720 // matter — both rebuilds of a manager's `ClusterConfig` converge on the
721 // same per-cluster contract.
722 if let Some(udp) = self.cluster_udp_config.get(&cluster) {
723 apply_udp_knobs(&mut cfg, udp);
724 }
725 cfg
726 }
727
728 /// Apply an `AddCluster` to every listener routing to it: fold the UDP
729 /// cluster knobs (affinity / responses / requests / PPv2) into the
730 /// manager's `ClusterConfig`, and rebuild the LB policy (HRW/Maglev table)
731 /// via the shared `BackendMap`.
732 fn apply_cluster(&mut self, cluster: &Cluster) {
733 // 1. LB policy (HRW/Maglev rebuild happens inside the BackendMap).
734 self.backends
735 .borrow_mut()
736 .set_load_balancing_policy_for_cluster(
737 &cluster.cluster_id,
738 LoadBalancingAlgorithms::try_from(cluster.load_balancing).unwrap_or_default(),
739 cluster
740 .load_metric
741 .and_then(|n| LoadMetric::try_from(n).ok()),
742 );
743
744 // 1b. Health settings from the cluster's `udp.health` block, if any.
745 // Mode HEALTH_OFF / no health block disables probing for this cluster.
746 let health_settings = cluster.udp.as_ref().and_then(|udp| {
747 udp.health.as_ref().and_then(|h| {
748 let mode = h
749 .mode
750 .and_then(|m| sozu_command::proto::command::UdpHealthMode::try_from(m).ok());
751 match mode {
752 Some(sozu_command::proto::command::UdpHealthMode::HealthOff) => None,
753 // TCP_PROBE (default when a health block is present) and
754 // UDP_PROBE both schedule the TCP-probe state machine; the
755 // app UDP-probe payload rides along for the secondary check.
756 _ => Some(health::UdpHealthSettings::from_proto(h)),
757 }
758 })
759 });
760 self.health
761 .set_cluster(&cluster.cluster_id, health_settings, &self.registry);
762
763 // 1c. Cache the UDP knobs so a frontend added in EITHER order picks them
764 // up via `cluster_config_for`. An `AddCluster` without a `udp` block
765 // clears the cache (back to proto defaults).
766 match &cluster.udp {
767 Some(udp) => {
768 self.cluster_udp_config
769 .insert(cluster.cluster_id.clone(), udp.clone());
770 }
771 None => {
772 self.cluster_udp_config.remove(&cluster.cluster_id);
773 }
774 }
775
776 // 2. Per-cluster UDP knobs into the managers routing to this cluster.
777 // `cluster_config_for` now folds the cached knobs in, so the rebuild is
778 // identical regardless of whether the frontend or the cluster came
779 // first.
780 let now = Instant::now();
781 let tokens: Vec<Token> = self
782 .cluster_for_listener
783 .iter()
784 .filter(|(_, c)| **c == cluster.cluster_id)
785 .map(|(t, _)| *t)
786 .collect();
787 for token in tokens {
788 let Some(listener) = self.listeners.get(&token) else {
789 continue;
790 };
791 let cfg = {
792 let l = listener.borrow();
793 self.cluster_config_for(&l, token)
794 };
795 if let Some(mgr) = self.managers.get(&token) {
796 mgr.borrow_mut()
797 .handle_input(ManagerInput::Config(ConfigEvent::SetCluster(cfg)), now);
798 }
799 }
800 }
801
802 /// Inherent dispatch entry point — the server calls this directly (UDP does
803 /// not implement `ProxyConfiguration`). Handles UDP frontends, cluster
804 /// config, listener removal, and stop. No accept / create_session.
805 pub fn notify(&mut self, message: WorkerRequest) -> WorkerResponse {
806 let request_type = match message.content.request_type {
807 Some(t) => t,
808 None => return WorkerResponse::error(message.id, "Empty request"),
809 };
810 match request_type {
811 RequestType::AddUdpFrontend(front) => match self.add_udp_front(front) {
812 Ok(()) => WorkerResponse::ok(message.id),
813 Err(err) => WorkerResponse::error(message.id, err),
814 },
815 RequestType::RemoveUdpFrontend(front) => match self.remove_udp_front(front) {
816 Ok(()) => WorkerResponse::ok(message.id),
817 Err(err) => WorkerResponse::error(message.id, err),
818 },
819 RequestType::AddCluster(cluster) => {
820 self.apply_cluster(&cluster);
821 WorkerResponse::ok(message.id)
822 }
823 RequestType::RemoveCluster(cluster_id) => {
824 let tokens: Vec<Token> = self
825 .cluster_for_listener
826 .iter()
827 .filter(|(_, c)| **c == cluster_id)
828 .map(|(t, _)| *t)
829 .collect();
830 for token in tokens {
831 if let Some(mgr) = self.managers.get(&token) {
832 mgr.borrow_mut().handle_input(
833 ManagerInput::Config(ConfigEvent::SetCluster(ClusterConfig::default())),
834 Instant::now(),
835 );
836 }
837 }
838 self.cluster_udp_config.remove(&cluster_id);
839 self.health.remove_cluster(&cluster_id, &self.registry);
840 WorkerResponse::ok(message.id)
841 }
842 RequestType::SoftStop(_) => {
843 info!(
844 "{} {} processing soft shutdown",
845 log_module_context!(),
846 message.id
847 );
848 // Drain: admit no new flows. Then actively tear down existing
849 // flows so the worker reaches `base_sessions_count` and exits
850 // promptly instead of waiting out each flow's idle timeout. UDP
851 // has no half-sent response to preserve, so an immediate flow
852 // teardown on soft-stop is the right graceful behavior (a stray
853 // in-flight reply may be lost, which is acceptable for a
854 // best-effort datagram proxy).
855 let now = Instant::now();
856 for mgr in self.managers.values() {
857 mgr.borrow_mut()
858 .handle_input(ManagerInput::Config(ConfigEvent::Drain), now);
859 }
860 // Drive teardown through each manager (FlowEvicted + CloseFlow
861 // per flow) so the active-flows gauge balances to zero.
862 for session in self.listener_sessions.values() {
863 session.borrow_mut().close_all_flows(now);
864 }
865 self.listener_sessions.clear();
866 let listeners: HashMap<_, _> = self.listeners.drain().collect();
867 for l in listeners.values() {
868 l.borrow_mut()
869 .socket
870 .take()
871 .map(|mut sock| self.registry.deregister(&mut sock));
872 }
873 WorkerResponse::processing(message.id)
874 }
875 RequestType::HardStop(_) => {
876 info!("{} {} hard shutdown", log_module_context!(), message.id);
877 let now = Instant::now();
878 // Drive teardown through each manager (FlowEvicted + CloseFlow
879 // per flow) so the active-flows gauge balances to zero before the
880 // managers are dropped below.
881 for session in self.listener_sessions.values() {
882 session.borrow_mut().close_all_flows(now);
883 }
884 self.listener_sessions.clear();
885 let mut listeners: HashMap<_, _> = self.listeners.drain().collect();
886 for (_, l) in listeners.drain() {
887 l.borrow_mut()
888 .socket
889 .take()
890 .map(|mut sock| self.registry.deregister(&mut sock));
891 }
892 self.managers.clear();
893 WorkerResponse::ok(message.id)
894 }
895 RequestType::Status(_) => {
896 info!("{} {} status", log_module_context!(), message.id);
897 WorkerResponse::ok(message.id)
898 }
899 RequestType::RemoveListener(remove) => {
900 if !self.remove_listener(remove.address.into()) {
901 WorkerResponse::error(
902 message.id,
903 format!("no UDP listener to remove at address {:?}", remove.address),
904 )
905 } else {
906 WorkerResponse::ok(message.id)
907 }
908 }
909 command => {
910 debug!(
911 "{} {} unsupported message for UDP proxy, ignoring {:?}",
912 log_module_context!(),
913 message.id,
914 command
915 );
916 WorkerResponse::error(message.id, "unsupported message")
917 }
918 }
919 }
920}
921
922/// Fold a proto [`UdpClusterConfig`](sozu_command::proto::command::UdpClusterConfig)
923/// into a [`ClusterConfig`], applying the proto defaults for absent fields.
924/// Single source of truth so `apply_cluster` (live push) and
925/// `cluster_config_for` (frontend add / rebuild) never diverge.
926fn apply_udp_knobs(cfg: &mut ClusterConfig, udp: &sozu_command::proto::command::UdpClusterConfig) {
927 cfg.affinity_with_port = matches!(
928 udp.affinity_key
929 .and_then(|k| UdpAffinityKey::try_from(k).ok()),
930 Some(UdpAffinityKey::SourceIpPort)
931 );
932 cfg.responses = udp.responses.unwrap_or(0);
933 cfg.requests = udp.requests.unwrap_or(0);
934 cfg.send_proxy_protocol = udp.send_proxy_protocol.unwrap_or(false);
935 cfg.proxy_protocol_every_datagram = udp.proxy_protocol_every_datagram.unwrap_or(false);
936}
937
938/// Fallback auto `max_flows` when `RLIMIT_NOFILE` can't be read.
939const DEFAULT_AUTO_MAX_FLOWS: usize = 1024;
940
941/// `max_flows == 0` means "auto": ~70% of the soft `RLIMIT_NOFILE`, so the fd
942/// budget adapts to the host without hand-tuning (one fd + one slab slot per
943/// flow). Falls back to a conservative constant when the limit can't be read.
944///
945/// The auto derivation is clamped to `slab_headroom` (the global
946/// `max_connections`): every admitted flow consumes one shared `SessionManager`
947/// slab slot, so an unclamped ~70%-of-RLIMIT value on a host with a very large
948/// fd limit could try to inflate the slab to hundreds of thousands of entries
949/// and starve HTTP/TCP. An *explicitly configured* `max_flows` is honoured as-is
950/// (the operator opted in); only the auto value is capped. A `slab_headroom` of
951/// 0 (no connection budget configured) disables the clamp.
952fn effective_max_flows(configured: u32, slab_headroom: usize) -> usize {
953 if configured != 0 {
954 return configured as usize;
955 }
956 let auto = {
957 #[cfg(unix)]
958 {
959 let mut limit = libc::rlimit {
960 rlim_cur: 0,
961 rlim_max: 0,
962 };
963 // SAFETY: `getrlimit` writes a fully-initialised `rlimit` into
964 // `limit`; we read the result only on success (`== 0`).
965 let ret = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
966 if ret == 0 && limit.rlim_cur > 0 {
967 let soft = limit.rlim_cur;
968 ((soft.saturating_mul(7)) / 10).max(1) as usize
969 } else {
970 DEFAULT_AUTO_MAX_FLOWS
971 }
972 }
973 #[cfg(not(unix))]
974 {
975 DEFAULT_AUTO_MAX_FLOWS
976 }
977 };
978 if slab_headroom == 0 {
979 auto
980 } else {
981 auto.min(slab_headroom).max(1)
982 }
983}
984
985/// Clamp a configured `max_rx_datagram_size` to the global `buffer_size`
986/// (defense in depth): the per-session `recv_buf` is sized `max_rx + 1`, so a
987/// raw worker `AddUdpListener`/`UpdateUdpListener` carrying e.g.
988/// `max_rx_datagram_size = u32::MAX` must NOT be able to allocate a ~4 GB
989/// buffer. A `buffer_size` of 0 (unset) leaves the value untouched.
990fn clamp_max_rx(configured: usize, buffer_size: usize) -> usize {
991 if buffer_size == 0 {
992 configured
993 } else {
994 configured.min(buffer_size)
995 }
996}
997
998/// The `ProxySession` backing one UDP listener. The server's generic readiness
999/// path drives this (UDP is not in the listen-accept arm). It owns the per-flow
1000/// connected upstream sockets and demuxes events by token.
1001pub struct UdpListenerSession {
1002 /// The listener this session serves.
1003 listener: Rc<RefCell<UdpListener>>,
1004 /// The sans-io manager for this listener.
1005 manager: Rc<RefCell<UdpManager>>,
1006 /// Shared backend map for LB selection.
1007 backends: Rc<RefCell<BackendMap>>,
1008 /// Cloned mio registry for per-flow socket (de)registration.
1009 registry: Registry,
1010 /// Slab the per-flow upstream tokens are inserted into (the same `Rc` the
1011 /// listener-session is registered under: multi-token pattern).
1012 sessions: Rc<RefCell<SessionManager>>,
1013 /// The listener's own token (client recv + timer key).
1014 listener_token: Token,
1015 /// Listener address (for logging).
1016 address: SocketAddr,
1017 /// Per-flow connected upstream sockets, keyed by their slab token.
1018 upstream_sockets: HashMap<Token, UdpSocket>,
1019 /// Per-flow bounded egress queues for the forward path, keyed by the
1020 /// upstream token. A datagram lands here only when the connected upstream
1021 /// socket returns `WouldBlock`; the socket is then reregistered
1022 /// `READABLE | WRITABLE` and drained on the next writable event. Dropped
1023 /// together with the socket on flow close (no leak, gauge stays correct).
1024 upstream_write_queues: HashMap<Token, WriteQueue>,
1025 /// Bounded egress queue for the client-return path (replies fanned back
1026 /// through the single listener socket via `send_to`). Engaged only when the
1027 /// listener socket returns `WouldBlock`; the listener is then reregistered
1028 /// `READABLE | WRITABLE` and drained on its next writable event.
1029 client_write_queue: WriteQueue,
1030 /// `upstream_token -> FlowId` for NAT-return demux.
1031 upstream_to_flow: HashMap<Token, FlowId>,
1032 /// `FlowId -> upstream_token` to tear down on close.
1033 flow_to_upstream: HashMap<FlowId, Token>,
1034 /// `FlowId -> admission Instant` for `udp.flow.duration` on close.
1035 flow_started: HashMap<FlowId, Instant>,
1036 /// `FlowId -> (client, backend)` for the close access log.
1037 flow_endpoints: HashMap<FlowId, (SocketAddr, Option<SocketAddr>)>,
1038 /// The client source whose datagram is currently being drained. Lets
1039 /// `on_send_to_backend` resolve the right per-flow upstream socket for an
1040 /// already-established flow (where no `OpenUpstream` precedes the send).
1041 /// `None` while draining backend-side or timer-driven outputs.
1042 in_flight_client: Option<SocketAddr>,
1043 /// The flow whose upstream was just opened in this drain pass — covers the
1044 /// new-flow path where `OpenUpstream{flow}` immediately precedes the first
1045 /// `SendToBackend`.
1046 in_flight_flow: Option<FlowId>,
1047 /// Shadow of the manager's flow table: normalised client key → `FlowId`.
1048 /// Lets the shell resolve the owning flow for a `SendToBackend` on an
1049 /// established flow from the in-flight client source. Kept in lockstep with
1050 /// `OpenUpstream` / `CloseFlow`.
1051 client_key_to_flow: HashMap<SocketAddr, FlowId>,
1052 /// Reusable recv scratch buffer, sized to `max_rx_datagram_size`.
1053 recv_buf: Vec<u8>,
1054 /// The currently-armed `TIMER` handle, so a re-arm cancels the previous
1055 /// deadline instead of leaking timer-slab entries (the manager only emits a
1056 /// fresh `ArmTimer` when the deadline actually changes).
1057 timer_handle: Option<crate::timer::Timeout>,
1058}
1059
1060impl UdpListenerSession {
1061 #[allow(clippy::too_many_arguments)]
1062 pub fn new(
1063 listener: Rc<RefCell<UdpListener>>,
1064 manager: Rc<RefCell<UdpManager>>,
1065 backends: Rc<RefCell<BackendMap>>,
1066 registry: Registry,
1067 sessions: Rc<RefCell<SessionManager>>,
1068 listener_token: Token,
1069 ) -> UdpListenerSession {
1070 let (address, max_rx) = {
1071 let l = listener.borrow();
1072 (l.address, l.config.max_rx_datagram_size as usize)
1073 };
1074 UdpListenerSession {
1075 listener,
1076 manager,
1077 backends,
1078 registry,
1079 sessions,
1080 listener_token,
1081 address,
1082 upstream_sockets: HashMap::new(),
1083 upstream_write_queues: HashMap::new(),
1084 client_write_queue: WriteQueue::new(CLIENT_WRITE_QUEUE_CAP),
1085 upstream_to_flow: HashMap::new(),
1086 flow_to_upstream: HashMap::new(),
1087 flow_started: HashMap::new(),
1088 flow_endpoints: HashMap::new(),
1089 in_flight_client: None,
1090 in_flight_flow: None,
1091 client_key_to_flow: HashMap::new(),
1092 // Size the recv scratch to `max_rx + 1`, NOT `max_rx`: a UDP
1093 // `recv_from` truncates the datagram to the buffer length and
1094 // silently discards the tail. If the buffer were exactly `max_rx`,
1095 // an oversized datagram would arrive as a `max_rx`-byte payload —
1096 // indistinguishable from a legal one — and be forwarded truncated.
1097 // The extra byte lets the manager observe `len == max_rx + 1 >
1098 // max_rx` and drop it (`DropReason::Truncated`) instead.
1099 recv_buf: vec![0u8; max_rx.saturating_add(1).max(1)],
1100 timer_handle: None,
1101 }
1102 }
1103
1104 /// Resize the recv scratch buffer to `max_rx + 1` (the same `+ 1` sizing as
1105 /// [`new`](Self::new): the extra byte lets the manager observe an oversized
1106 /// datagram as `len == max_rx + 1 > max_rx` and drop it as `Truncated`
1107 /// rather than silently forwarding a kernel-truncated payload). Called from
1108 /// `UdpProxy::update_listener` when a config push changes the listener's
1109 /// `max_rx_datagram_size`. Resizing larger zero-fills the new tail; resizing
1110 /// smaller truncates the (idle, between-datagram) scratch — both are safe
1111 /// because the buffer holds no live datagram across calls.
1112 fn resize_recv_buf(&mut self, max_rx: usize) {
1113 self.recv_buf.resize(max_rx.saturating_add(1).max(1), 0u8);
1114 }
1115
1116 /// Normalise a client source the same way the active manager config keys
1117 /// flows (4-tuple when `affinity_with_port`, else source-IP with port 0).
1118 fn client_key(&self, src: SocketAddr) -> SocketAddr {
1119 let with_port = self.manager.borrow().affinity_with_port();
1120 if with_port {
1121 src
1122 } else {
1123 let mut s = src;
1124 s.set_port(0);
1125 s
1126 }
1127 }
1128
1129 /// Drain every datagram waiting on the listener socket into the manager.
1130 /// Edge-triggered epoll: loop `recv_from` to `WouldBlock`.
1131 fn ingest_client(&mut self, now: Instant) {
1132 // Pull the socket out behind a short borrow then operate on it.
1133 loop {
1134 let result = {
1135 let listener = self.listener.borrow();
1136 let Some(socket) = listener.socket.as_ref() else {
1137 return;
1138 };
1139 socket.recv_from(&mut self.recv_buf)
1140 };
1141 match result {
1142 Ok((len, src)) => {
1143 // Mark the in-flight client so `on_send_to_backend` can
1144 // resolve the owning flow's upstream socket for an
1145 // already-established flow (no `OpenUpstream` precedes it).
1146 self.in_flight_client = Some(src);
1147 self.in_flight_flow = None;
1148 let len = len.min(self.recv_buf.len());
1149 // Borrow the payload via a split so the mutable borrow of
1150 // `self.manager` does not alias `self.recv_buf`.
1151 let payload: &[u8] = &self.recv_buf[..len];
1152 // SAFETY-free: `handle_input` only reads the slice; the
1153 // borrow checker is satisfied because `recv_buf` and
1154 // `manager` are disjoint fields.
1155 let mgr = self.manager.clone();
1156 mgr.borrow_mut()
1157 .handle_input(ManagerInput::ClientDatagram { src, payload }, now);
1158 // Drain outputs after each datagram to bound queue growth.
1159 self.drain_outputs(now);
1160 self.in_flight_client = None;
1161 }
1162 Err(ref e) if e.kind() == ErrorKind::WouldBlock => break,
1163 Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
1164 Err(e) => {
1165 debug!(
1166 "{} recv_from error on UDP listener: {}",
1167 log_context!(self),
1168 e
1169 );
1170 break;
1171 }
1172 }
1173 }
1174 }
1175
1176 /// Drain datagrams waiting on one flow's connected upstream socket into the
1177 /// manager as `BackendDatagram`s.
1178 fn ingest_upstream(&mut self, upstream_token: Token, now: Instant) {
1179 let Some(&flow) = self.upstream_to_flow.get(&upstream_token) else {
1180 return;
1181 };
1182 loop {
1183 let result = {
1184 let Some(socket) = self.upstream_sockets.get(&upstream_token) else {
1185 return;
1186 };
1187 socket.recv(&mut self.recv_buf)
1188 };
1189 match result {
1190 Ok(len) => {
1191 let len = len.min(self.recv_buf.len());
1192 let payload: &[u8] = &self.recv_buf[..len];
1193 let mgr = self.manager.clone();
1194 mgr.borrow_mut()
1195 .handle_input(ManagerInput::BackendDatagram { flow, payload }, now);
1196 self.drain_outputs(now);
1197 }
1198 Err(ref e) if e.kind() == ErrorKind::WouldBlock => break,
1199 Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
1200 Err(e) => {
1201 debug!(
1202 "{} recv error on upstream socket: {}",
1203 log_context!(self),
1204 e
1205 );
1206 break;
1207 }
1208 }
1209 }
1210 }
1211
1212 /// Drain the manager's output queue, acting on each `Output`.
1213 fn drain_outputs(&mut self, now: Instant) {
1214 let mgr = self.manager.clone();
1215 loop {
1216 let out = mgr.borrow_mut().poll_output();
1217 let Some(out) = out else { break };
1218 match out {
1219 Output::SelectBackend { flow, cluster, key } => {
1220 self.on_select_backend(flow, &cluster, key, now)
1221 }
1222 Output::OpenUpstream { flow, backend } => self.on_open_upstream(flow, backend, now),
1223 Output::SendToBackend(transmit) => self.on_send_to_backend(transmit),
1224 Output::SendToClient(transmit) => self.on_send_to_client(transmit),
1225 Output::ArmTimer(deadline) => self.arm_timer(deadline, now),
1226 Output::Metric(ev) => Self::record_metric(ev),
1227 Output::CloseFlow(flow) => self.on_close_flow(flow),
1228 Output::Drop(reason) => Self::record_drop(reason),
1229 }
1230 }
1231 }
1232
1233 fn on_select_backend(&mut self, flow: FlowId, cluster: &str, key: u64, now: Instant) {
1234 let resolved = self
1235 .backends
1236 .borrow_mut()
1237 .backend_from_cluster_id_with_key(cluster, Some(key));
1238 match resolved {
1239 Ok((backend, addr)) => {
1240 self.manager.borrow_mut().handle_input(
1241 ManagerInput::BackendResolved {
1242 flow,
1243 backend,
1244 addr,
1245 },
1246 now,
1247 );
1248 }
1249 Err(e) => {
1250 debug!(
1251 "{} no backend for cluster {}: {}; aborting flow {}",
1252 log_context!(self),
1253 cluster,
1254 e,
1255 flow
1256 );
1257 incr!(names::udp::DROPPED_NO_BACKEND);
1258 // Abort the flow instead of leaving it parked AwaitingBackend:
1259 // the manager already counted it (FlowCreated, +1 gauge, slab +
1260 // admission slot), so without this it would squat a `max_flows`
1261 // slot for the full idle timeout while every later datagram is
1262 // dropped. `abort_flow` enqueues FlowEvicted + CloseFlow, which
1263 // the surrounding `drain_outputs` loop processes — freeing the
1264 // slot immediately and balancing the gauge via FlowEvicted.
1265 self.manager
1266 .borrow_mut()
1267 .abort_flow(flow, now, CloseReason::Aborted);
1268 }
1269 }
1270 }
1271
1272 fn on_open_upstream(&mut self, flow: FlowId, backend: SocketAddr, now: Instant) {
1273 let mut socket = match udp_connect(backend) {
1274 Ok(socket) => socket,
1275 Err(e) => {
1276 // EMFILE/ENFILE/connect refusal → shed this flow, never panic.
1277 warn!(
1278 "{} could not open upstream socket to {}: {}; shedding flow {}",
1279 log_context!(self),
1280 backend,
1281 e,
1282 flow
1283 );
1284 incr!(names::udp::FLOWS_SHED);
1285 // The manager already moved this flow toward Established and
1286 // counted it; abort it so the `max_flows` slot frees immediately
1287 // (FlowEvicted balances the gauge) instead of squatting until the
1288 // idle timeout. Keep the FLOWS_SHED metric above for the
1289 // EMFILE/refused case. `abort_flow` enqueues FlowEvicted +
1290 // CloseFlow for the surrounding `drain_outputs` loop.
1291 self.manager
1292 .borrow_mut()
1293 .abort_flow(flow, now, CloseReason::Aborted);
1294 return;
1295 }
1296 };
1297 // Multi-token pattern (template tcp.rs:1029-1053): a fresh slab slot
1298 // under the SAME listener-session Rc, registered READABLE so its
1299 // readiness reaches `Server::ready` → demuxed back to this session by
1300 // `update_readiness`. The flow-table cap (`max_flows`) already bounds
1301 // how many upstream sockets/slots can exist, so the slab cannot grow
1302 // unbounded here.
1303 let upstream_token = {
1304 let mut s = self.sessions.borrow_mut();
1305 let listener_session = s.slab[self.listener_token.0].clone();
1306 let entry = s.slab.vacant_entry();
1307 let token = Token(entry.key());
1308 entry.insert(listener_session);
1309 token
1310 };
1311 if let Err(e) = self
1312 .registry
1313 .register(&mut socket, upstream_token, Interest::READABLE)
1314 {
1315 error!(
1316 "{} could not register upstream socket: {}",
1317 log_context!(self),
1318 e
1319 );
1320 self.sessions.borrow_mut().slab.try_remove(upstream_token.0);
1321 // The flow is Established in the manager but has no usable upstream
1322 // socket: abort it so its `max_flows` slot frees now (FlowEvicted
1323 // balances the gauge) rather than squatting until idle timeout.
1324 self.manager
1325 .borrow_mut()
1326 .abort_flow(flow, now, CloseReason::Aborted);
1327 return;
1328 }
1329 self.upstream_sockets.insert(upstream_token, socket);
1330 self.upstream_to_flow.insert(upstream_token, flow);
1331 self.flow_to_upstream.insert(flow, upstream_token);
1332 // `flow_to_upstream` and `upstream_to_flow` are inverse maps: the token
1333 // in one points back to the flow in the other. A broken inverse would let
1334 // a backend reply demux to the wrong client (a NAT-return mismatch).
1335 debug_assert_eq!(
1336 self.upstream_to_flow.get(&upstream_token),
1337 Some(&flow),
1338 "upstream_to_flow must map the new token back to its flow"
1339 );
1340 debug_assert_eq!(
1341 self.flow_to_upstream.get(&flow),
1342 Some(&upstream_token),
1343 "flow_to_upstream must map the flow back to its upstream token"
1344 );
1345 self.flow_started.insert(flow, Instant::now());
1346 // The client source for this flow is the one currently in flight.
1347 let client = self.in_flight_client.unwrap_or(self.address);
1348 self.flow_endpoints.insert(flow, (client, Some(backend)));
1349 if let Some(src) = self.in_flight_client {
1350 let key = self.client_key(src);
1351 self.client_key_to_flow.insert(key, flow);
1352 // The shadow flow-table only ever holds live flows: the flow we just
1353 // mapped must have a live upstream token (it is the one we just
1354 // opened). Pairs the on-close drop in `on_close_flow`.
1355 debug_assert!(
1356 self.flow_to_upstream.contains_key(&flow),
1357 "client_key_to_flow points at flow {flow} with no live upstream token"
1358 );
1359 }
1360 // This flow's first SendToBackend (if any) follows immediately.
1361 self.in_flight_flow = Some(flow);
1362 }
1363
1364 fn on_send_to_backend(&mut self, transmit: crate::protocol::udp::Transmit) {
1365 // Resolve the owning flow precisely (NOT by `transmit.dst`, which two
1366 // flows to the same backend would alias — sending on the wrong
1367 // connected socket misroutes that backend's reply to the wrong client).
1368 // * new flow: `OpenUpstream{flow}` set `in_flight_flow` just before.
1369 // * established flow: resolve via the in-flight client source through
1370 // the shell-side `client_key -> flow` shadow of the flow table.
1371 let flow = self.in_flight_flow.or_else(|| {
1372 self.in_flight_client
1373 .map(|src| self.client_key(src))
1374 .and_then(|key| self.client_key_to_flow.get(&key).copied())
1375 });
1376 let token = flow.and_then(|f| self.flow_to_upstream.get(&f).copied());
1377 let Some(token) = token else {
1378 // No resolved flow / upstream socket: this is not a queue-full drop.
1379 incr!(names::udp::DROPPED_UNKNOWN_FLOW);
1380 return;
1381 };
1382 let Some(socket) = self.upstream_sockets.get(&token) else {
1383 // Socket already gone (flow closed mid-drain): unknown-flow, not
1384 // queue-full.
1385 incr!(names::udp::DROPPED_UNKNOWN_FLOW);
1386 return;
1387 };
1388 // If a queue is already backed up for this flow, preserve FIFO order —
1389 // do NOT jump the line with the fast-path `send`. Append (or drop on
1390 // overflow) and let the WRITABLE drain catch up.
1391 if let Some(q) = self.upstream_write_queues.get_mut(&token)
1392 && !q.is_empty()
1393 {
1394 if !q.push(transmit.dst, transmit.payload) {
1395 debug!("{} upstream write queue full, dropping", log_context!(self));
1396 incr!(names::udp::DROPPED_WQ_FULL);
1397 }
1398 return;
1399 }
1400 match socket.send(&transmit.payload) {
1401 Ok(_) => {}
1402 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
1403 // Kernel send buffer full: enqueue (bounded) and arm WRITABLE so
1404 // the next writable event drains it. Drop + metric only at cap.
1405 let q = self
1406 .upstream_write_queues
1407 .entry(token)
1408 .or_insert_with(|| WriteQueue::new(UPSTREAM_WRITE_QUEUE_CAP));
1409 if q.push(transmit.dst, transmit.payload) {
1410 self.arm_upstream_writable(token);
1411 } else {
1412 debug!("{} upstream write queue full, dropping", log_context!(self));
1413 incr!(names::udp::DROPPED_WQ_FULL);
1414 }
1415 }
1416 Err(e) => {
1417 debug!("{} upstream send error: {}", log_context!(self), e);
1418 // Hard send error (e.g. ECONNREFUSED), not a queue-full drop.
1419 incr!(names::udp::DROPPED_SEND_ERROR);
1420 }
1421 }
1422 }
1423
1424 /// Reregister a flow's connected upstream socket for `READABLE | WRITABLE`
1425 /// so a queued forward datagram gets a writable wake (the edge-triggered
1426 /// analog of `signal_pending_write`). Idempotent enough — mio coalesces a
1427 /// repeated interest set.
1428 fn arm_upstream_writable(&mut self, token: Token) {
1429 if let Some(socket) = self.upstream_sockets.get_mut(&token)
1430 && let Err(e) =
1431 self.registry
1432 .reregister(socket, token, Interest::READABLE | Interest::WRITABLE)
1433 {
1434 debug!(
1435 "{} could not arm WRITABLE on upstream socket: {}",
1436 log_context!(self),
1437 e
1438 );
1439 }
1440 }
1441
1442 /// Drop a flow's upstream socket back to `READABLE`-only once its write queue
1443 /// has fully drained, so an empty socket no longer wakes the loop on every
1444 /// writable edge (a permanently-WRITABLE UDP socket would busy-loop).
1445 fn disarm_upstream_writable(&mut self, token: Token) {
1446 if let Some(socket) = self.upstream_sockets.get_mut(&token)
1447 && let Err(e) = self.registry.reregister(socket, token, Interest::READABLE)
1448 {
1449 debug!(
1450 "{} could not disarm WRITABLE on upstream socket: {}",
1451 log_context!(self),
1452 e
1453 );
1454 }
1455 }
1456
1457 /// Drain a flow's upstream write queue on a writable event. Re-sends in FIFO
1458 /// order until `WouldBlock` or empty; on empty, drops WRITABLE interest.
1459 fn drain_upstream_queue(&mut self, token: Token) {
1460 let Some(mut queue) = self.upstream_write_queues.remove(&token) else {
1461 return;
1462 };
1463 let socket = self.upstream_sockets.get(&token);
1464 let Some(socket) = socket else {
1465 // Socket gone (flow closed mid-drain): discard the queue.
1466 return;
1467 };
1468 let emptied = queue.drain(|_dst, payload| match socket.send(payload) {
1469 Ok(_) => SendOutcome::Sent,
1470 Err(ref e) if e.kind() == ErrorKind::WouldBlock => SendOutcome::WouldBlock,
1471 Err(_) => SendOutcome::Dropped,
1472 });
1473 if emptied {
1474 self.disarm_upstream_writable(token);
1475 } else {
1476 // Still backed up: put the queue back and keep WRITABLE armed.
1477 self.upstream_write_queues.insert(token, queue);
1478 }
1479 }
1480
1481 fn on_send_to_client(&mut self, transmit: crate::protocol::udp::Transmit) {
1482 // Preserve FIFO: if the client-return queue is already backed up, append
1483 // (or drop on overflow) rather than jumping the line via the fast path.
1484 if !self.client_write_queue.is_empty() {
1485 if !self.client_write_queue.push(transmit.dst, transmit.payload) {
1486 debug!("{} client write queue full, dropping", log_context!(self));
1487 incr!(names::udp::DROPPED_WQ_FULL);
1488 }
1489 return;
1490 }
1491 let send_result = {
1492 let listener = self.listener.borrow();
1493 let Some(socket) = listener.socket.as_ref() else {
1494 return;
1495 };
1496 socket.send_to(&transmit.payload, transmit.dst)
1497 };
1498 match send_result {
1499 Ok(_) => {}
1500 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
1501 // Listener send buffer full: enqueue (bounded) + arm WRITABLE on
1502 // the listener so the next writable event drains it.
1503 if self.client_write_queue.push(transmit.dst, transmit.payload) {
1504 self.arm_client_writable();
1505 } else {
1506 debug!("{} client write queue full, dropping", log_context!(self));
1507 incr!(names::udp::DROPPED_WQ_FULL);
1508 }
1509 }
1510 Err(e) => {
1511 debug!("{} client send_to error: {}", log_context!(self), e);
1512 // Hard send_to error, not a queue-full drop.
1513 incr!(names::udp::DROPPED_SEND_ERROR);
1514 }
1515 }
1516 }
1517
1518 /// Reregister the listener socket for `READABLE | WRITABLE` so a queued
1519 /// client-return datagram gets a writable wake. The listener token routes
1520 /// the writable event back into `update_readiness`.
1521 fn arm_client_writable(&mut self) {
1522 let listener = self.listener.borrow();
1523 let fd = match listener.socket.as_ref() {
1524 Some(socket) => socket.as_raw_fd(),
1525 None => return,
1526 };
1527 if let Err(e) = self.registry.reregister(
1528 &mut SourceFd(&fd),
1529 self.listener_token,
1530 Interest::READABLE | Interest::WRITABLE,
1531 ) {
1532 debug!(
1533 "{} could not arm WRITABLE on listener socket: {}",
1534 log_context!(self),
1535 e
1536 );
1537 }
1538 }
1539
1540 /// Drop the listener socket back to `READABLE`-only once the client-return
1541 /// queue is empty (a permanently-WRITABLE listener would busy-loop).
1542 fn disarm_client_writable(&mut self) {
1543 let listener = self.listener.borrow();
1544 let fd = match listener.socket.as_ref() {
1545 Some(socket) => socket.as_raw_fd(),
1546 None => return,
1547 };
1548 if let Err(e) =
1549 self.registry
1550 .reregister(&mut SourceFd(&fd), self.listener_token, Interest::READABLE)
1551 {
1552 debug!(
1553 "{} could not disarm WRITABLE on listener socket: {}",
1554 log_context!(self),
1555 e
1556 );
1557 }
1558 }
1559
1560 /// Drain the client-return write queue on a listener writable event. Re-sends
1561 /// in FIFO order until `WouldBlock` or empty; on empty, drops WRITABLE.
1562 fn drain_client_queue(&mut self) {
1563 let mut queue = std::mem::replace(&mut self.client_write_queue, WriteQueue::new(0));
1564 let emptied = {
1565 let listener = self.listener.borrow();
1566 let Some(socket) = listener.socket.as_ref() else {
1567 // Listener socket gone: discard the queue and restore an empty one.
1568 self.client_write_queue = WriteQueue::new(CLIENT_WRITE_QUEUE_CAP);
1569 return;
1570 };
1571 queue.drain(|dst, payload| match socket.send_to(payload, *dst) {
1572 Ok(_) => SendOutcome::Sent,
1573 Err(ref e) if e.kind() == ErrorKind::WouldBlock => SendOutcome::WouldBlock,
1574 Err(_) => SendOutcome::Dropped,
1575 })
1576 };
1577 // Restore the (possibly still-backed-up) queue; preserve its capacity.
1578 queue.cap = CLIENT_WRITE_QUEUE_CAP;
1579 self.client_write_queue = queue;
1580 if emptied {
1581 self.disarm_client_writable();
1582 }
1583 }
1584
1585 /// Map the single manager-wide `ArmTimer(deadline)` onto the thread-local
1586 /// `TIMER`, keyed by the **listener token**: when it fires, the run loop
1587 /// calls `Server::timeout(listener_token)` → `session.timeout(token)` →
1588 /// `manager.handle_timeout(now)`.
1589 fn arm_timer(&mut self, deadline: Instant, now: Instant) {
1590 let delay = deadline.saturating_duration_since(now);
1591 TIMER.with(|timer| {
1592 let mut timer = timer.borrow_mut();
1593 // Cancel the previous deadline so re-arming does not leak timer-slab
1594 // entries. The manager only emits a fresh `ArmTimer` on a real
1595 // deadline change, so cancellations are rare.
1596 if let Some(old) = self.timer_handle.take() {
1597 let _ = timer.cancel_timeout(&old);
1598 }
1599 self.timer_handle = Some(timer.set_timeout(delay, self.listener_token));
1600 });
1601 }
1602
1603 fn on_close_flow(&mut self, flow: FlowId) {
1604 if let Some(token) = self.flow_to_upstream.remove(&flow) {
1605 if let Some(mut socket) = self.upstream_sockets.remove(&token)
1606 && let Err(e) = self.registry.deregister(&mut socket)
1607 {
1608 debug!("{} deregister upstream on close: {}", log_context!(self), e);
1609 }
1610 // Drop any queued (un-drained) forward datagrams with the socket so
1611 // the per-flow queue cannot leak; gauge correctness is preserved
1612 // because the queue holds bytes, not flow-count state.
1613 self.upstream_write_queues.remove(&token);
1614 self.upstream_to_flow.remove(&token);
1615 self.sessions.borrow_mut().slab.try_remove(token.0);
1616 // On close, all per-flow maps drop the flow together: neither
1617 // direction of the upstream-token map may still reference it.
1618 debug_assert!(
1619 !self.upstream_to_flow.values().any(|&f| f == flow),
1620 "on_close_flow left upstream_to_flow referencing closed flow {flow}"
1621 );
1622 debug_assert!(
1623 !self.flow_to_upstream.contains_key(&flow),
1624 "on_close_flow left flow_to_upstream entry for closed flow {flow}"
1625 );
1626 }
1627 if let Some(started) = self.flow_started.remove(&flow) {
1628 let duration = started.elapsed();
1629 time!(names::udp::FLOW_DURATION, duration.as_millis());
1630 }
1631 let (client, backend) = self
1632 .flow_endpoints
1633 .remove(&flow)
1634 .unwrap_or((self.address, None));
1635 // Drop the shadow flow-table entry if it still points at this flow.
1636 let key = self.client_key(client);
1637 if self.client_key_to_flow.get(&key) == Some(&flow) {
1638 self.client_key_to_flow.remove(&key);
1639 }
1640 // The shadow flow-table must no longer map THIS flow id. A surviving
1641 // entry would misroute a later established-flow `SendToBackend` onto a
1642 // freed upstream token.
1643 debug_assert!(
1644 !self.client_key_to_flow.values().any(|&f| f == flow),
1645 "on_close_flow left client_key_to_flow referencing closed flow {flow}"
1646 );
1647 info!("{} flow closed", log_flow_context!(flow, client, backend));
1648 }
1649
1650 fn record_metric(ev: MetricEvent) {
1651 match ev {
1652 MetricEvent::FlowCreated => {
1653 incr!(names::udp::FLOWS_CREATED);
1654 gauge_add!(names::udp::ACTIVE_FLOWS, 1);
1655 }
1656 MetricEvent::FlowEvicted => {
1657 incr!(names::udp::FLOWS_EVICTED);
1658 gauge_add!(names::udp::ACTIVE_FLOWS, -1);
1659 }
1660 MetricEvent::FlowShed => {
1661 incr!(names::udp::FLOWS_SHED);
1662 }
1663 MetricEvent::DatagramIn(bytes) => {
1664 incr!(names::udp::DATAGRAMS_IN);
1665 count!(names::udp::BYTES_IN, bytes as i64);
1666 }
1667 MetricEvent::DatagramOut(bytes) => {
1668 incr!(names::udp::DATAGRAMS_OUT);
1669 count!(names::udp::BYTES_OUT, bytes as i64);
1670 }
1671 MetricEvent::DatagramDropped(reason) => Self::record_drop(reason),
1672 }
1673 }
1674
1675 fn record_drop(reason: DropReason) {
1676 incr!(names::udp::DATAGRAMS_DROPPED);
1677 match reason {
1678 DropReason::Invalid => incr!(names::udp::DROPPED_INVALID),
1679 DropReason::Truncated => incr!(names::udp::DROPPED_TRUNCATED),
1680 DropReason::NoBackend => incr!(names::udp::DROPPED_NO_BACKEND),
1681 DropReason::Shed => incr!(names::udp::DROPPED_SHED),
1682 DropReason::UnknownFlow => incr!(names::udp::DROPPED_UNKNOWN_FLOW),
1683 }
1684 }
1685
1686 /// Tear down every active flow on this listener **through the manager**, so
1687 /// each close emits `FlowEvicted` + `CloseFlow` exactly once and the shell's
1688 /// normal [`on_close_flow`](Self::on_close_flow) handler frees the upstream
1689 /// socket + slab slot and decrements `udp.active_flows`. Used on soft/hard
1690 /// stop, listener remove, and listener deactivate so the worker reaches its
1691 /// `base_sessions_count` and exits promptly instead of waiting out every
1692 /// flow's idle timeout — and so the active-flows gauge does not leak by N
1693 /// (the bug the old direct-teardown path had: it cleared the shell maps
1694 /// without telling the manager, so `FlowEvicted` never fired).
1695 ///
1696 /// On a deactivate where the manager is *retained* (not dropped), this also
1697 /// resets the manager's flow table to empty, keeping manager and shell
1698 /// consistent. The listener socket and the listener-session slab slot are
1699 /// left intact (the listener slot is part of `base_sessions_count` and
1700 /// reclaimed when the worker exits); only the connectionless per-flow slots
1701 /// — which are NOT counted in `nb_connections` and so removed without
1702 /// `decr` — are freed.
1703 pub fn close_all_flows(&mut self, now: Instant) {
1704 // Drive teardown through the manager: it emits one `FlowEvicted` +
1705 // `CloseFlow` per live flow into its output queue. Draining those runs
1706 // `record_metric(FlowEvicted)` (the single gauge decrement) and
1707 // `on_close_flow` (frees socket + slab slot + shell maps) per flow.
1708 self.manager.borrow_mut().close_all(now);
1709 self.drain_outputs(now);
1710 // After `close_all`, the manager's flow table is empty and it armed no
1711 // new timer (`reschedule` emits `ArmTimer` only on a real deadline
1712 // change, and an empty table has no deadline). Cancel any residual shell
1713 // timer so it can't fire against a now-flowless listener.
1714 if let Some(handle) = self.timer_handle.take() {
1715 TIMER.with(|timer| {
1716 let _ = timer.borrow_mut().cancel_timeout(&handle);
1717 });
1718 }
1719 }
1720}
1721
1722impl ProxySession for UdpListenerSession {
1723 fn protocol(&self) -> Protocol {
1724 Protocol::UDPListen
1725 }
1726
1727 fn update_readiness(&mut self, token: Token, events: Ready) {
1728 // WRITABLE first: a previously-`WouldBlock` socket can now accept queued
1729 // egress. Drain before reading so the kernel send buffer has room before
1730 // this pass enqueues more (and so a writable-only event still drains).
1731 if events.is_writable() {
1732 if token == self.listener_token {
1733 self.drain_client_queue();
1734 } else if self.upstream_to_flow.contains_key(&token) {
1735 self.drain_upstream_queue(token);
1736 }
1737 }
1738 if !events.is_readable() {
1739 return;
1740 }
1741 let now = Instant::now();
1742 if token == self.listener_token {
1743 self.ingest_client(now);
1744 } else if self.upstream_to_flow.contains_key(&token) {
1745 self.ingest_upstream(token, now);
1746 }
1747 }
1748
1749 fn ready(&mut self, _session: Rc<RefCell<dyn ProxySession>>) -> SessionIsToBeClosed {
1750 // All work happens in `update_readiness` (it has the firing token; the
1751 // generic `ready()` does not). Never close the listener session here —
1752 // it lives for the listener's lifetime.
1753 false
1754 }
1755
1756 fn timeout(&mut self, token: Token) -> SessionIsToBeClosed {
1757 if token == self.listener_token {
1758 let now = Instant::now();
1759 self.manager.borrow_mut().handle_timeout(now);
1760 self.drain_outputs(now);
1761 // Re-arm: the manager emits a fresh ArmTimer via poll_output if a
1762 // flow is still scheduled (handled inside drain_outputs). Nothing
1763 // to do here. Never close the listener on a flow timeout.
1764 }
1765 false
1766 }
1767
1768 fn close(&mut self) {
1769 // Tear down any still-live flows THROUGH the manager (FlowEvicted +
1770 // CloseFlow per flow) so `udp.active_flows` balances to zero even if the
1771 // server reaps this listener session directly without a prior
1772 // proxy-driven `close_all_flows`. In the common path
1773 // (`close_all_flows` already ran) the manager has no live flows, so this
1774 // is a cheap no-op. Also cancels the residual idle timer.
1775 self.close_all_flows(Instant::now());
1776 // Any leftover egress queues / shell maps are dropped here (the per-flow
1777 // ones were already freed by the manager-driven close above; this only
1778 // resets the client-return queue, which is not flow-state).
1779 self.upstream_write_queues.clear();
1780 self.client_write_queue = WriteQueue::new(CLIENT_WRITE_QUEUE_CAP);
1781 self.upstream_to_flow.clear();
1782 self.flow_to_upstream.clear();
1783 // Deregister + drop the listener socket. Never shutdown(Both) — UDP has
1784 // no connection to shut down; just deregister + drop the fds.
1785 let mut listener = self.listener.borrow_mut();
1786 if let Some(socket) = listener.socket.as_ref() {
1787 let fd = socket.as_raw_fd();
1788 let _ = self.registry.deregister(&mut SourceFd(&fd));
1789 }
1790 listener.active = false;
1791 }
1792
1793 fn last_event(&self) -> Instant {
1794 // The listener session lives for the listener's lifetime — it is never a
1795 // zombie even when idle. Mirror `ListenSession::last_event` (which
1796 // returns `now`) so the `zombie_check` (which has no listener-protocol
1797 // exclusion) never reaps a quiet UDP listener. Per-flow idle reaping is
1798 // handled by the manager's timer wheel, not the zombie sweep.
1799 Instant::now()
1800 }
1801
1802 fn print_session(&self) {
1803 error!(
1804 "{} UDP listener session: {} active flows, {} upstream sockets",
1805 log_context!(self),
1806 self.manager.borrow().flow_count(),
1807 self.upstream_sockets.len(),
1808 );
1809 }
1810
1811 fn frontend_token(&self) -> Token {
1812 self.listener_token
1813 }
1814
1815 fn shutting_down(&mut self) -> SessionIsToBeClosed {
1816 // The listener session is a *listener*, not a connection: it was never
1817 // counted by `SessionManager::incr` (only accepted sessions are), so it
1818 // MUST NOT be routed through the connection-counted
1819 // `shut_down_sessions_by_frontend_tokens` path — that calls `decr`,
1820 // which underflows `nb_connections` and panics (`assert!(nb != 0)` at
1821 // `server.rs`). This mirrors `ListenSession::shutting_down` (which also
1822 // returns `false`). Soft-stop draining is owned by
1823 // `UdpProxy::notify(SoftStop)`: it flips every manager to `Drain` and
1824 // deregisters the listener sockets, so existing flows reach teardown and
1825 // no new flow is admitted. The slot is reclaimed when the worker exits.
1826 false
1827 }
1828
1829 fn cluster_id(&self) -> Option<String> {
1830 self.listener.borrow().cluster_id.clone()
1831 }
1832}
1833
1834#[allow(unused_imports)]
1835pub(crate) use {log_context, log_flow_context, log_module_context};
1836
1837#[cfg(test)]
1838mod tests {
1839 use super::*;
1840
1841 use std::cell::Cell;
1842
1843 #[test]
1844 fn effective_max_flows_explicit_value_is_used() {
1845 // An explicit value is honoured verbatim, ignoring the slab headroom
1846 // clamp (the operator opted in).
1847 assert_eq!(effective_max_flows(42, 0), 42);
1848 assert_eq!(effective_max_flows(42, 10), 42);
1849 }
1850
1851 #[test]
1852 fn effective_max_flows_auto_is_positive() {
1853 // 0 = auto: derives ~70% of RLIMIT_NOFILE, always >= 1.
1854 assert!(effective_max_flows(0, 0) >= 1);
1855 }
1856
1857 #[test]
1858 fn effective_max_flows_auto_is_clamped_to_slab_headroom() {
1859 // Auto derivation is capped at the slab headroom (max_connections) so a
1860 // UDP listener can't inflate the shared slab. A headroom of 0 disables
1861 // the clamp.
1862 assert!(effective_max_flows(0, 4) <= 4);
1863 assert!(effective_max_flows(0, 4) >= 1);
1864 }
1865
1866 #[test]
1867 fn clamp_max_rx_respects_buffer_size() {
1868 // The configured rx size is clamped to buffer_size; an unset (0)
1869 // buffer_size leaves it untouched.
1870 assert_eq!(clamp_max_rx(u32::MAX as usize, 16_384), 16_384);
1871 assert_eq!(clamp_max_rx(1_024, 16_384), 1_024);
1872 assert_eq!(clamp_max_rx(u32::MAX as usize, 0), u32::MAX as usize);
1873 }
1874
1875 fn addr(port: u16) -> SocketAddr {
1876 SocketAddr::from(([127, 0, 0, 1], port))
1877 }
1878
1879 /// A fake socket whose `send` outcome is scripted per call. Lets the
1880 /// `WriteQueue` state machine be exercised with zero real I/O: we feed a
1881 /// sequence of `SendOutcome`s and record what was sent in order.
1882 struct FakeSocket {
1883 /// Outcomes returned by successive `send` calls (front = next).
1884 script: RefCell<VecDeque<SendOutcome>>,
1885 /// Payloads that `Sent`/`Dropped` actually consumed, in order.
1886 consumed: RefCell<Vec<Vec<u8>>>,
1887 /// Default outcome once the script is exhausted.
1888 default: Cell<bool>, // true = Sent, false = WouldBlock
1889 }
1890
1891 impl FakeSocket {
1892 fn new(script: Vec<SendOutcome>, default_sent: bool) -> Self {
1893 FakeSocket {
1894 script: RefCell::new(script.into()),
1895 consumed: RefCell::new(Vec::new()),
1896 default: Cell::new(default_sent),
1897 }
1898 }
1899
1900 fn send(&self, payload: &[u8]) -> SendOutcome {
1901 let outcome = self.script.borrow_mut().pop_front().unwrap_or({
1902 if self.default.get() {
1903 SendOutcome::Sent
1904 } else {
1905 SendOutcome::WouldBlock
1906 }
1907 });
1908 if matches!(outcome, SendOutcome::Sent | SendOutcome::Dropped) {
1909 self.consumed.borrow_mut().push(payload.to_vec());
1910 }
1911 outcome
1912 }
1913 }
1914
1915 #[test]
1916 fn write_queue_push_until_full_then_drops() {
1917 let mut q = WriteQueue::new(2);
1918 assert!(q.is_empty());
1919 assert!(q.push(addr(1), vec![1]));
1920 assert!(q.push(addr(2), vec![2]));
1921 assert_eq!(q.len(), 2);
1922 // At capacity: the third push is rejected (caller drops + counts).
1923 assert!(!q.push(addr(3), vec![3]));
1924 assert_eq!(q.len(), 2);
1925 }
1926
1927 #[test]
1928 fn write_queue_drains_in_fifo_order_on_writable() {
1929 let mut q = WriteQueue::new(8);
1930 for i in 0..4u8 {
1931 assert!(q.push(addr(i as u16), vec![i]));
1932 }
1933 // All sends succeed: drain empties the queue, FIFO preserved.
1934 let sock = FakeSocket::new(vec![], true);
1935 let emptied = q.drain(|_dst, payload| sock.send(payload));
1936 assert!(emptied);
1937 assert!(q.is_empty());
1938 assert_eq!(
1939 *sock.consumed.borrow(),
1940 vec![vec![0u8], vec![1u8], vec![2u8], vec![3u8]]
1941 );
1942 }
1943
1944 #[test]
1945 fn write_queue_stops_on_wouldblock_and_resumes() {
1946 let mut q = WriteQueue::new(8);
1947 for i in 0..3u8 {
1948 assert!(q.push(addr(i as u16), vec![i]));
1949 }
1950 // First send ok, then WouldBlock: drain sends one and stops, leaving two.
1951 let sock = FakeSocket::new(vec![SendOutcome::Sent, SendOutcome::WouldBlock], false);
1952 let emptied = q.drain(|_dst, payload| sock.send(payload));
1953 assert!(!emptied);
1954 assert_eq!(q.len(), 2);
1955 assert_eq!(*sock.consumed.borrow(), vec![vec![0u8]]);
1956 // Front is still the second datagram (FIFO preserved across the stall).
1957 assert_eq!(q.queue.front().unwrap().1, vec![1u8]);
1958 // Second writable event: now everything goes through.
1959 let sock2 = FakeSocket::new(vec![], true);
1960 let emptied2 = q.drain(|_dst, payload| sock2.send(payload));
1961 assert!(emptied2);
1962 assert!(q.is_empty());
1963 assert_eq!(*sock2.consumed.borrow(), vec![vec![1u8], vec![2u8]]);
1964 }
1965
1966 #[test]
1967 fn write_queue_hard_error_drops_one_and_continues() {
1968 let mut q = WriteQueue::new(8);
1969 for i in 0..3u8 {
1970 assert!(q.push(addr(i as u16), vec![i]));
1971 }
1972 // Middle datagram hard-errors: it is popped + skipped, the rest proceed.
1973 let sock = FakeSocket::new(
1974 vec![SendOutcome::Sent, SendOutcome::Dropped, SendOutcome::Sent],
1975 true,
1976 );
1977 let emptied = q.drain(|_dst, payload| sock.send(payload));
1978 assert!(emptied);
1979 assert!(q.is_empty());
1980 // The dropped datagram (1) was consumed-for-accounting but the queue is
1981 // empty and the surviving datagrams (0, 2) went out in order.
1982 assert_eq!(
1983 *sock.consumed.borrow(),
1984 vec![vec![0u8], vec![1u8], vec![2u8]]
1985 );
1986 }
1987
1988 #[test]
1989 fn write_queue_empties_cleanly_when_already_empty() {
1990 let mut q = WriteQueue::new(4);
1991 let sock = FakeSocket::new(vec![], true);
1992 // Draining an empty queue is a no-op that reports emptied = true.
1993 let emptied = q.drain(|_dst, payload| sock.send(payload));
1994 assert!(emptied);
1995 assert!(q.is_empty());
1996 assert!(sock.consumed.borrow().is_empty());
1997 }
1998}