irtt_server/set.rs
1//! Service-level supervision of one or more independent [`Server`] listeners.
2//!
3//! [`Server`] is the one-listener primitive: one socket, one core, one session
4//! namespace, one sequential receive/send loop. A host that must answer on more
5//! than one address needs several of them, and something has to own their
6//! construction, their shutdown and each other's failures. That owner is
7//! [`ServerSet`], and it is deliberately not a protocol object: it holds no
8//! session state, no tokens and no clock, and nothing it does is visible on the
9//! wire.
10
11use std::{
12 collections::HashMap,
13 future::Future,
14 io,
15 net::{IpAddr, SocketAddr},
16};
17
18use socket2::{Domain, Protocol, SockAddr, Socket, Type};
19use thiserror::Error;
20use tokio::{
21 net::UdpSocket,
22 sync::watch,
23 task::{Id, JoinError, JoinSet},
24};
25
26use crate::{Server, ServerConfig, ServerRuntimeError};
27
28/// One or more independent UDP listeners run as a single service.
29///
30/// Every listener is a complete [`Server`]: its own socket, its own
31/// [`ServerCore`](crate::ServerCore), its own session table and its own tokens.
32/// Nothing is shared between them but the configuration they were built from
33/// and the supervision described below. A token issued by one listener is an
34/// unknown token at any other, which is what makes the listeners genuinely
35/// independent rather than one server wearing several addresses.
36///
37/// # Configuration is cloned, so bounds are per listener
38///
39/// The [`ServerConfig`] passed to [`ServerSet::bind`] is cloned into each
40/// listener, so every resource bound in it applies **per listener** and not to
41/// the process. A set of two listeners built from a configuration with
42/// `max_sessions` of 100 admits up to 100 sessions on each, 200 in total. There
43/// is no process-wide session cap.
44///
45/// # A set of one is an ordinary set
46///
47/// One listener is not a special case and gets no shortcut: it is bound,
48/// supervised and shut down exactly as a member of a larger set is. The server
49/// CLI runs every invocation through a `ServerSet` for that reason, so the
50/// single-listener case exercises this path continuously.
51///
52/// # Supervision
53///
54/// [`run`](ServerSet::run) takes one shutdown future and fans it out to every
55/// listener, then waits for all of them. A listener that fails, or that stops on
56/// its own before shutdown was asked for, shuts its siblings down and fails the
57/// whole set: a service configured for IPv4 and IPv6 must not quietly continue
58/// as IPv4 only. Every listener task is joined before `run` returns, so no task
59/// outlives it.
60#[derive(Debug)]
61pub struct ServerSet {
62 servers: Vec<Server>,
63 /// The addresses the listeners actually bound, in requested order and
64 /// positionally matching `servers`.
65 local_addrs: Vec<SocketAddr>,
66}
67
68impl ServerSet {
69 /// Binds every requested listener, or none of them.
70 ///
71 /// Requested order is preserved, and each listener is bound before any is
72 /// served: a later failure drops the sockets already opened and starts
73 /// nothing. `config` is cloned into every listener, so its bounds are per
74 /// listener.
75 ///
76 /// A port of `0` is resolved per listener — two such requests select two
77 /// unrelated ephemeral ports, not one shared one. Ask
78 /// [`local_addrs`](ServerSet::local_addrs) for what was actually bound.
79 ///
80 /// # Same-port IPv4 and IPv6
81 ///
82 /// Requesting a genuine IPv6 listener and an IPv4 one on the same explicit
83 /// port — `0.0.0.0:2112` with `[::]:2112`, say — is a normal configuration,
84 /// but on hosts whose IPv6 sockets are dual-stack by default the IPv6 bind
85 /// would claim the IPv4 port too and the second bind would fail. Such an
86 /// IPv6 listener is therefore created `IPV6_V6ONLY` before it is bound, so
87 /// the two coexist and each serves its own family. Bind order is not relied
88 /// on, and a listener requested on its own is bound exactly as
89 /// [`Server::bind`] would.
90 ///
91 /// An IPv4-mapped address such as `[::ffff:0.0.0.0]` is an IPv4 listener
92 /// here, not an IPv6 one, matching how the runtime already reads it.
93 ///
94 /// # Errors
95 ///
96 /// Returns [`ServerSetError::NoListeners`] if `addrs` is empty — a set
97 /// serving nothing has no meaning — and
98 /// [`ServerSetError::ListenerSetup`] naming the requested address if any
99 /// listener could not be bound or prepared.
100 pub async fn bind<I>(addrs: I, config: ServerConfig) -> Result<Self, ServerSetError>
101 where
102 I: IntoIterator<Item = SocketAddr>,
103 {
104 let requested: Vec<SocketAddr> = addrs.into_iter().collect();
105 if requested.is_empty() {
106 return Err(ServerSetError::NoListeners);
107 }
108
109 let mut servers = Vec::with_capacity(requested.len());
110 let mut local_addrs = Vec::with_capacity(requested.len());
111 for &addr in &requested {
112 let server = bind_listener(addr, config.clone(), needs_ipv6_only(addr, &requested))
113 .await
114 .map_err(|source| ServerSetError::ListenerSetup { addr, source })?;
115 let local_addr = server
116 .local_addr()
117 .map_err(|source| ServerSetError::ListenerSetup { addr, source })?;
118 servers.push(server);
119 local_addrs.push(local_addr);
120 }
121
122 Ok(Self {
123 servers,
124 local_addrs,
125 })
126 }
127
128 /// The addresses the listeners actually bound, in requested order.
129 ///
130 /// These are the resolved endpoints, so a requested port of `0` appears
131 /// here as the port the host selected.
132 #[must_use]
133 pub fn local_addrs(&self) -> &[SocketAddr] {
134 &self.local_addrs
135 }
136
137 /// Serves every listener until `shutdown` completes or one of them fails.
138 ///
139 /// Each listener runs in its own Tokio task and stays internally sequential
140 /// there, so the one-sender-per-socket ownership the reply traffic class
141 /// depends on is unchanged. The single `shutdown` future is fanned out
142 /// internally; nothing about how the caller produces it — a signal, a
143 /// channel, a timer — is this crate's business.
144 ///
145 /// The set consumes itself because each listener is moved into its task. A
146 /// caller that wants to bind and serve again binds a new set; the
147 /// lower-level [`Server`] keeps its reusable `&mut self` loop.
148 ///
149 /// # Errors
150 ///
151 /// A listener that fails, that returns before shutdown was requested, or
152 /// whose task did not complete shuts down its siblings; every remaining
153 /// listener is then drained and the first such failure is returned. An
154 /// external shutdown that completes first still returns a listener's error
155 /// if one appears while draining. `Ok(())` means every listener stopped
156 /// cleanly after being asked to.
157 pub async fn run<F>(self, shutdown: F) -> Result<(), ServerSetError>
158 where
159 F: Future<Output = ()>,
160 {
161 let (shutdown_tx, _) = watch::channel(false);
162 let mut listeners = JoinSet::new();
163 // A panicking task cannot report which listener it was, so the mapping
164 // is kept here rather than recovered from the task's return value.
165 let mut listener_addrs: HashMap<Id, SocketAddr> = HashMap::new();
166
167 for (mut server, addr) in self.servers.into_iter().zip(self.local_addrs) {
168 let mut shutdown_rx = shutdown_tx.subscribe();
169 let handle = listeners.spawn(async move {
170 let outcome = server
171 .run(async move {
172 // A closed channel means the set is being torn down and
173 // is as good a reason to stop as an explicit request.
174 let _ = shutdown_rx.wait_for(|requested| *requested).await;
175 })
176 .await;
177 (addr, outcome)
178 });
179 listener_addrs.insert(handle.id(), addr);
180 }
181
182 tokio::pin!(shutdown);
183 let mut shutting_down = false;
184 let mut failure: Option<ServerSetError> = None;
185
186 loop {
187 tokio::select! {
188 () = &mut shutdown, if !shutting_down => {
189 shutting_down = true;
190 let _ = shutdown_tx.send(true);
191 }
192 joined = listeners.join_next() => {
193 let Some(joined) = joined else { break };
194 let listener_failure = match joined {
195 // The only clean exit: it was asked to stop, and did.
196 Ok((_, Ok(()))) if shutting_down => None,
197 Ok((addr, Ok(()))) => Some(ServerSetError::ListenerStopped { addr }),
198 Ok((addr, Err(source))) => {
199 Some(ServerSetError::ListenerRun { addr, source })
200 }
201 Err(source) => Some(ServerSetError::ListenerTask {
202 addr: listener_addrs.get(&source.id()).copied(),
203 source,
204 }),
205 };
206 if let Some(listener_failure) = listener_failure {
207 failure.get_or_insert(listener_failure);
208 if !shutting_down {
209 shutting_down = true;
210 let _ = shutdown_tx.send(true);
211 }
212 }
213 }
214 }
215 }
216
217 failure.map_or(Ok(()), Err)
218 }
219}
220
221/// Whether `addr`'s address family has any local support on this host at
222/// all, independent of whether this particular address or port can be
223/// bound.
224///
225/// A [`ServerSetError::ListenerSetup`] failure can mean many different
226/// things — the port is already in use, permission is denied, this target
227/// has no safe wildcard reply-source path (see [`ServerRuntimeError`]) — and
228/// only one of them, the address family itself having no local support at
229/// all (for example IPv6 disabled on the host), is safe for a caller to work
230/// around by silently trying a different address instead. This checks only
231/// that, by actually binding the wildcard address of `addr`'s family on an
232/// ephemeral port: a bare socket of an unsupported family can still
233/// construct successfully on some systems (Linux administratively disables
234/// IPv6 per-interface, so `AF_INET6` sockets keep working while every bind
235/// fails with `EADDRNOTAVAIL`), so construction alone is not a reliable
236/// signal — only a real bind attempt is.
237#[must_use]
238pub fn address_family_available(addr: SocketAddr) -> bool {
239 let wildcard = if addr.is_ipv6() {
240 SocketAddr::from((std::net::Ipv6Addr::UNSPECIFIED, 0))
241 } else {
242 SocketAddr::from((std::net::Ipv4Addr::UNSPECIFIED, 0))
243 };
244 let domain = if addr.is_ipv6() {
245 Domain::IPV6
246 } else {
247 Domain::IPV4
248 };
249 let Ok(socket) = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)) else {
250 return false;
251 };
252 socket.bind(&SockAddr::from(wildcard)).is_ok()
253}
254
255/// Binds one listener, pre-configuring the socket where the bind itself depends
256/// on it.
257///
258/// The ordinary path is [`Server::bind`] unchanged. Only an IPv6 listener that
259/// has to leave an IPv4 sibling's port alone needs its own socket, because
260/// `IPV6_V6ONLY` can only be set before the bind it affects.
261async fn bind_listener(
262 addr: SocketAddr,
263 config: ServerConfig,
264 ipv6_only: bool,
265) -> Result<Server, ServerRuntimeError> {
266 if !ipv6_only {
267 return Server::bind(addr, config).await;
268 }
269
270 let socket =
271 bind_ipv6_only(addr).map_err(|source| ServerRuntimeError::Bind { addr, source })?;
272 Server::from_socket(socket, config)
273}
274
275/// Creates an IPv6-only UDP socket and binds it.
276///
277/// Everything past the bind — destination metadata for a wildcard listener,
278/// the core, traffic-class state — stays with [`Server::from_socket`]. This
279/// helper exists solely for the one option that must precede the bind.
280fn bind_ipv6_only(addr: SocketAddr) -> io::Result<UdpSocket> {
281 let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
282 socket.set_only_v6(true)?;
283 socket.set_nonblocking(true)?;
284 socket.bind(&SockAddr::from(addr))?;
285 UdpSocket::from_std(socket.into())
286}
287
288/// Whether this listener must be bound IPv6-only to leave an IPv4 sibling's
289/// port available.
290///
291/// Only an explicit port can collide: two ephemeral requests are resolved
292/// independently and never name the same port to begin with.
293fn needs_ipv6_only(addr: SocketAddr, requested: &[SocketAddr]) -> bool {
294 is_genuine_ipv6(addr)
295 && addr.port() != 0
296 && requested
297 .iter()
298 .any(|other| other.port() == addr.port() && serves_ipv4(*other))
299}
300
301/// Whether a bind address names an IPv6 endpoint in its own right.
302///
303/// An IPv4-mapped address is not one: `[::ffff:0.0.0.0]` is an IPv4 wildcard
304/// bind that the kernel reports back in IPv6 clothing, and the runtime already
305/// reads it as IPv4 for wildcard handling and for the traffic-class option. It
306/// must not be given `IPV6_V6ONLY` — that would refuse the very traffic it
307/// exists to serve — nor counted as the IPv6 half of a same-port pair.
308fn is_genuine_ipv6(addr: SocketAddr) -> bool {
309 addr.is_ipv6() && !serves_ipv4(addr)
310}
311
312/// Whether a bind address will carry IPv4 traffic, in either spelling.
313fn serves_ipv4(addr: SocketAddr) -> bool {
314 match addr.ip() {
315 IpAddr::V4(_) => true,
316 IpAddr::V6(address) => address.to_ipv4_mapped().is_some(),
317 }
318}
319
320/// Failure to construct or supervise a [`ServerSet`].
321#[derive(Debug, Error)]
322pub enum ServerSetError {
323 /// No listener address was requested.
324 #[error("a server set needs at least one listener address")]
325 NoListeners,
326 /// A requested listener could not be bound or prepared. Nothing was served:
327 /// any listener already bound for this set has been released.
328 #[error("could not set up listener {addr}: {source}")]
329 ListenerSetup {
330 addr: SocketAddr,
331 #[source]
332 source: ServerRuntimeError,
333 },
334 /// A running listener failed. Its siblings were shut down.
335 #[error("listener {addr} failed: {source}")]
336 ListenerRun {
337 addr: SocketAddr,
338 #[source]
339 source: ServerRuntimeError,
340 },
341 /// A listener returned before the set was asked to shut down. A set does
342 /// not continue with fewer listeners than it was configured for.
343 #[error("listener {addr} stopped before the server set was asked to shut down")]
344 ListenerStopped { addr: SocketAddr },
345 /// A listener task did not complete — it panicked or was aborted. The
346 /// address is absent only where the task could not be identified.
347 #[error(
348 "listener task{} did not complete: {source}",
349 .addr.map(|addr| format!(" for {addr}")).unwrap_or_default()
350 )]
351 ListenerTask {
352 addr: Option<SocketAddr>,
353 #[source]
354 source: JoinError,
355 },
356}
357
358#[cfg(test)]
359mod tests {
360 use std::time::Duration;
361
362 use super::*;
363 use crate::{token::TokenSource, ServerCore, ServerError};
364
365 /// Which listener of a requested set has to be bound IPv6-only.
366 ///
367 /// The interesting rows are the mapped ones. `[::ffff:0.0.0.0]` is an IPv4
368 /// wildcard bind wearing an IPv6 address, so it is neither a candidate for
369 /// `IPV6_V6ONLY` — which would refuse the traffic it exists to serve — nor
370 /// the IPv6 half of a same-port pair. A genuine IPv6 address paired with it
371 /// still is.
372 #[test]
373 fn only_a_genuine_ipv6_listener_sharing_an_explicit_port_is_bound_ipv6_only() {
374 for (addr, requested, ipv6_only, why) in [
375 (
376 "[::]:2112",
377 vec!["0.0.0.0:2112", "[::]:2112"],
378 true,
379 "the wildcard pair is the reason this exists",
380 ),
381 (
382 "[::1]:2112",
383 vec!["127.0.0.1:2112", "[::1]:2112"],
384 true,
385 "and so is the explicit loopback pair",
386 ),
387 (
388 "0.0.0.0:2112",
389 vec!["0.0.0.0:2112", "[::]:2112"],
390 false,
391 "the IPv4 half is bound exactly as it always was",
392 ),
393 (
394 "[::]:2112",
395 vec!["[::]:2112"],
396 false,
397 "an IPv6 listener alone keeps ordinary bind semantics",
398 ),
399 (
400 "[::]:2112",
401 vec!["0.0.0.0:2113", "[::]:2112"],
402 false,
403 "different ports cannot collide",
404 ),
405 (
406 "[::]:0",
407 vec!["0.0.0.0:0", "[::]:0"],
408 false,
409 "two ephemeral requests resolve independently",
410 ),
411 (
412 "[::ffff:0.0.0.0]:2112",
413 vec!["0.0.0.0:2112", "[::ffff:0.0.0.0]:2112"],
414 false,
415 "a mapped wildcard is an IPv4 listener, not an IPv6 one",
416 ),
417 (
418 "[::]:2112",
419 vec!["[::ffff:0.0.0.0]:2112", "[::]:2112"],
420 true,
421 "and it is the IPv4 half a genuine IPv6 listener must avoid",
422 ),
423 (
424 "[::ffff:127.0.0.1]:2112",
425 vec!["[::ffff:127.0.0.1]:2112", "[::1]:2112"],
426 false,
427 "a mapped host address is explicit IPv4 too",
428 ),
429 ] {
430 let requested: Vec<SocketAddr> = requested
431 .iter()
432 .map(|addr| addr.parse().expect("a test address must parse"))
433 .collect();
434 assert_eq!(
435 needs_ipv6_only(addr.parse().unwrap(), &requested),
436 ipv6_only,
437 "{addr} among {requested:?}: {why}"
438 );
439 }
440 }
441
442 #[tokio::test(flavor = "current_thread")]
443 async fn a_set_serving_nothing_is_refused() {
444 assert!(matches!(
445 ServerSet::bind([], ServerConfig::default()).await,
446 Err(ServerSetError::NoListeners)
447 ));
448 }
449
450 /// A token source that draws one value successfully and then fails every
451 /// later draw.
452 ///
453 /// This is the same seam the core's own tests already use to make token
454 /// allocation fail deterministically (see [`crate::token::TokenSource`]
455 /// and its `ScriptedTokens::failing`), specialized here so the *first*
456 /// open still succeeds: the listener built from it answers one real
457 /// exchange exactly like a healthy listener would, and only a second open
458 /// discovers the failure.
459 #[derive(Debug)]
460 struct FailAfterFirstToken(Option<u64>);
461
462 impl TokenSource for FailAfterFirstToken {
463 fn next_token(&mut self) -> Result<u64, ServerError> {
464 self.0.take().ok_or_else(|| ServerError::RandomSource {
465 reason: "test fault injection: exhausted after the first token".to_owned(),
466 })
467 }
468 }
469
470 const FIRST_TOKEN: u64 = 0xdead_beef_dead_beef;
471 const TEST_TIMEOUT: Duration = Duration::from_secs(5);
472 const REPLY_TIMEOUT: Duration = Duration::from_secs(1);
473
474 fn loopback(port: u16) -> SocketAddr {
475 SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, port))
476 }
477
478 fn requested_params() -> irtt_proto::Params {
479 irtt_proto::Params {
480 protocol_version: 1,
481 duration_ns: 10_000_000_000,
482 interval_ns: 100_000_000,
483 received_stats: irtt_proto::ReceivedStats::Both,
484 stamp_at: irtt_proto::StampAt::Both,
485 clock: irtt_proto::Clock::Both,
486 ..irtt_proto::Params::default()
487 }
488 }
489
490 async fn send_open(client: &UdpSocket, addr: SocketAddr) {
491 let request = irtt_proto::encode_request(
492 irtt_proto::RequestToEncode::Open {
493 params: &requested_params(),
494 no_test: false,
495 },
496 None,
497 )
498 .unwrap();
499 client.send_to(&request, addr).await.unwrap();
500 }
501
502 /// Sends one open request and returns its reply, or `None` if the
503 /// listener stayed silent within [`REPLY_TIMEOUT`].
504 async fn open(client: &UdpSocket, addr: SocketAddr) -> Option<irtt_proto::OpenReply> {
505 send_open(client, addr).await;
506 let mut buffer = [0u8; 4096];
507 match tokio::time::timeout(REPLY_TIMEOUT, client.recv_from(&mut buffer)).await {
508 Ok(received) => {
509 let (len, from) = received.unwrap();
510 assert_eq!(from, addr, "a reply came from the wrong listener");
511 Some(irtt_proto::decode_open_reply(&buffer[..len], None).unwrap())
512 }
513 Err(_) => None,
514 }
515 }
516
517 /// A fatal failure inside one listener, after every listener in the set
518 /// has already bound and answered real traffic, shuts its siblings down,
519 /// joins every task and fails the set — with no external shutdown ever
520 /// sent.
521 ///
522 /// The failure is injected the same way the core's own tests already
523 /// drive a token-allocation failure: a scripted
524 /// [`crate::token::TokenSource`] substituted for one listener's
525 /// [`crate::ServerCore`], through the test-only
526 /// [`Server::from_socket_with_core`]. That source draws one token
527 /// successfully, so the listener built from it answers a first open
528 /// exactly like its healthy sibling does — which is the startup/readiness
529 /// synchronization this test relies on: both are bound and actively
530 /// serving real traffic before anything is asked to fail, not merely
531 /// spawned. Only a *second* open against that listener draws its second
532 /// token, which the scripted source refuses, and that single draw is the
533 /// one injected fatal failure. Nothing about the socket, the receive loop
534 /// or the sibling listener is touched.
535 #[tokio::test(flavor = "current_thread")]
536 async fn a_mid_run_listener_failure_shuts_down_its_siblings_and_fails_the_set() {
537 let config = ServerConfig::default().with_min_send_interval(Duration::ZERO);
538
539 let failing_socket = UdpSocket::bind(loopback(0)).await.unwrap();
540 let failing_core = ServerCore::with_token_source(
541 config.clone(),
542 Box::new(FailAfterFirstToken(Some(FIRST_TOKEN))),
543 );
544 let failing = Server::from_socket_with_core(failing_socket, failing_core).unwrap();
545 let failing_addr = failing.local_addr().unwrap();
546
547 let healthy_socket = UdpSocket::bind(loopback(0)).await.unwrap();
548 let healthy = Server::from_socket(healthy_socket, config).unwrap();
549 let healthy_addr = healthy.local_addr().unwrap();
550
551 let set = ServerSet {
552 servers: vec![failing, healthy],
553 local_addrs: vec![failing_addr, healthy_addr],
554 };
555
556 // A shutdown future that never resolves: whatever stops the set has
557 // to come from the injected failure, not from this caller.
558 let run = tokio::spawn(set.run(std::future::pending::<()>()));
559
560 let client = UdpSocket::bind(loopback(0)).await.unwrap();
561
562 // Both listeners answer a first, ordinary open before anything fails.
563 let opened = open(&client, failing_addr)
564 .await
565 .expect("the failing listener is bound and serving before it fails");
566 assert_eq!(opened.token, FIRST_TOKEN);
567 assert!(
568 open(&client, healthy_addr).await.is_some(),
569 "the healthy listener is bound and serving too"
570 );
571
572 // The second open at the failing listener draws its second token,
573 // which the scripted source refuses. It terminates that listener
574 // before any reply could be built, so this exchange places no
575 // expectation on a reply.
576 send_open(&client, failing_addr).await;
577
578 let outcome = tokio::time::timeout(TEST_TIMEOUT, run)
579 .await
580 .expect("the set stopped on its own, with no external shutdown sent")
581 .expect("the supervising task did not panic");
582 match outcome {
583 Err(ServerSetError::ListenerRun { addr, source }) => {
584 assert_eq!(
585 addr, failing_addr,
586 "the failure names the listener that actually failed"
587 );
588 assert!(
589 matches!(
590 source,
591 ServerRuntimeError::Core(ServerError::RandomSource { .. })
592 ),
593 "unexpected failure source: {source}"
594 );
595 }
596 other => panic!("expected the injected listener failure, got {other:?}"),
597 }
598
599 // The sibling did not linger after the failure: its task was joined
600 // and its socket released, exactly like the listener that failed —
601 // proof neither task nor socket was leaked.
602 for addr in [healthy_addr, failing_addr] {
603 std::net::UdpSocket::bind(addr)
604 .unwrap_or_else(|error| panic!("{addr} was not released: {error}"));
605 }
606 }
607}