Skip to main content

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