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#[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/// Binds one listener, pre-configuring the socket where the bind itself depends
222/// on it.
223///
224/// The ordinary path is [`Server::bind`] unchanged. Only an IPv6 listener that
225/// has to leave an IPv4 sibling's port alone needs its own socket, because
226/// `IPV6_V6ONLY` can only be set before the bind it affects.
227async fn bind_listener(
228    addr: SocketAddr,
229    config: ServerConfig,
230    ipv6_only: bool,
231) -> Result<Server, ServerRuntimeError> {
232    if !ipv6_only {
233        return Server::bind(addr, config).await;
234    }
235
236    let socket =
237        bind_ipv6_only(addr).map_err(|source| ServerRuntimeError::Bind { addr, source })?;
238    Server::from_socket(socket, config)
239}
240
241/// Creates an IPv6-only UDP socket and binds it.
242///
243/// Everything past the bind — destination metadata for a wildcard listener,
244/// the core, traffic-class state — stays with [`Server::from_socket`]. This
245/// helper exists solely for the one option that must precede the bind.
246fn bind_ipv6_only(addr: SocketAddr) -> io::Result<UdpSocket> {
247    let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP))?;
248    socket.set_only_v6(true)?;
249    socket.set_nonblocking(true)?;
250    socket.bind(&SockAddr::from(addr))?;
251    UdpSocket::from_std(socket.into())
252}
253
254/// Whether this listener must be bound IPv6-only to leave an IPv4 sibling's
255/// port available.
256///
257/// Only an explicit port can collide: two ephemeral requests are resolved
258/// independently and never name the same port to begin with.
259fn needs_ipv6_only(addr: SocketAddr, requested: &[SocketAddr]) -> bool {
260    is_genuine_ipv6(addr)
261        && addr.port() != 0
262        && requested
263            .iter()
264            .any(|other| other.port() == addr.port() && serves_ipv4(*other))
265}
266
267/// Whether a bind address names an IPv6 endpoint in its own right.
268///
269/// An IPv4-mapped address is not one: `[::ffff:0.0.0.0]` is an IPv4 wildcard
270/// bind that the kernel reports back in IPv6 clothing, and the runtime already
271/// reads it as IPv4 for wildcard handling and for the traffic-class option. It
272/// must not be given `IPV6_V6ONLY` — that would refuse the very traffic it
273/// exists to serve — nor counted as the IPv6 half of a same-port pair.
274fn is_genuine_ipv6(addr: SocketAddr) -> bool {
275    addr.is_ipv6() && !serves_ipv4(addr)
276}
277
278/// Whether a bind address will carry IPv4 traffic, in either spelling.
279fn serves_ipv4(addr: SocketAddr) -> bool {
280    match addr.ip() {
281        IpAddr::V4(_) => true,
282        IpAddr::V6(address) => address.to_ipv4_mapped().is_some(),
283    }
284}
285
286/// Failure to construct or supervise a [`ServerSet`].
287#[derive(Debug, Error)]
288pub enum ServerSetError {
289    /// No listener address was requested.
290    #[error("a server set needs at least one listener address")]
291    NoListeners,
292    /// A requested listener could not be bound or prepared. Nothing was served:
293    /// any listener already bound for this set has been released.
294    #[error("could not set up listener {addr}: {source}")]
295    ListenerSetup {
296        addr: SocketAddr,
297        #[source]
298        source: ServerRuntimeError,
299    },
300    /// A running listener failed. Its siblings were shut down.
301    #[error("listener {addr} failed: {source}")]
302    ListenerRun {
303        addr: SocketAddr,
304        #[source]
305        source: ServerRuntimeError,
306    },
307    /// A listener returned before the set was asked to shut down. A set does
308    /// not continue with fewer listeners than it was configured for.
309    #[error("listener {addr} stopped before the server set was asked to shut down")]
310    ListenerStopped { addr: SocketAddr },
311    /// A listener task did not complete — it panicked or was aborted. The
312    /// address is absent only where the task could not be identified.
313    #[error(
314        "listener task{} did not complete: {source}",
315        .addr.map(|addr| format!(" for {addr}")).unwrap_or_default()
316    )]
317    ListenerTask {
318        addr: Option<SocketAddr>,
319        #[source]
320        source: JoinError,
321    },
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    /// Which listener of a requested set has to be bound IPv6-only.
329    ///
330    /// The interesting rows are the mapped ones. `[::ffff:0.0.0.0]` is an IPv4
331    /// wildcard bind wearing an IPv6 address, so it is neither a candidate for
332    /// `IPV6_V6ONLY` — which would refuse the traffic it exists to serve — nor
333    /// the IPv6 half of a same-port pair. A genuine IPv6 address paired with it
334    /// still is.
335    #[test]
336    fn only_a_genuine_ipv6_listener_sharing_an_explicit_port_is_bound_ipv6_only() {
337        for (addr, requested, ipv6_only, why) in [
338            (
339                "[::]:2112",
340                vec!["0.0.0.0:2112", "[::]:2112"],
341                true,
342                "the wildcard pair is the reason this exists",
343            ),
344            (
345                "[::1]:2112",
346                vec!["127.0.0.1:2112", "[::1]:2112"],
347                true,
348                "and so is the explicit loopback pair",
349            ),
350            (
351                "0.0.0.0:2112",
352                vec!["0.0.0.0:2112", "[::]:2112"],
353                false,
354                "the IPv4 half is bound exactly as it always was",
355            ),
356            (
357                "[::]:2112",
358                vec!["[::]:2112"],
359                false,
360                "an IPv6 listener alone keeps ordinary bind semantics",
361            ),
362            (
363                "[::]:2112",
364                vec!["0.0.0.0:2113", "[::]:2112"],
365                false,
366                "different ports cannot collide",
367            ),
368            (
369                "[::]:0",
370                vec!["0.0.0.0:0", "[::]:0"],
371                false,
372                "two ephemeral requests resolve independently",
373            ),
374            (
375                "[::ffff:0.0.0.0]:2112",
376                vec!["0.0.0.0:2112", "[::ffff:0.0.0.0]:2112"],
377                false,
378                "a mapped wildcard is an IPv4 listener, not an IPv6 one",
379            ),
380            (
381                "[::]:2112",
382                vec!["[::ffff:0.0.0.0]:2112", "[::]:2112"],
383                true,
384                "and it is the IPv4 half a genuine IPv6 listener must avoid",
385            ),
386            (
387                "[::ffff:127.0.0.1]:2112",
388                vec!["[::ffff:127.0.0.1]:2112", "[::1]:2112"],
389                false,
390                "a mapped host address is explicit IPv4 too",
391            ),
392        ] {
393            let requested: Vec<SocketAddr> = requested
394                .iter()
395                .map(|addr| addr.parse().expect("a test address must parse"))
396                .collect();
397            assert_eq!(
398                needs_ipv6_only(addr.parse().unwrap(), &requested),
399                ipv6_only,
400                "{addr} among {requested:?}: {why}"
401            );
402        }
403    }
404
405    #[tokio::test(flavor = "current_thread")]
406    async fn a_set_serving_nothing_is_refused() {
407        assert!(matches!(
408            ServerSet::bind([], ServerConfig::default()).await,
409            Err(ServerSetError::NoListeners)
410        ));
411    }
412}