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