Skip to main content

irtt_server/
runtime.rs

1//! Tokio UDP orchestration around [`ServerCore`](crate::ServerCore).
2
3use std::{
4    future::Future,
5    io,
6    net::{IpAddr, SocketAddr},
7    time::Duration,
8};
9
10use socket2::SockRef;
11use thiserror::Error;
12use tokio::{net::UdpSocket, time::MissedTickBehavior};
13
14use crate::{
15    socket_io::{self, ReceivedDatagram},
16    socket_options::{marks_with_ipv4_option, set_reply_traffic_class},
17    ServerConfig, ServerCore, ServerError,
18};
19
20/// Fixed transport receive capacity.
21///
22/// Standard UDP payloads fit in this buffer; IPv6 jumbograms are not supported.
23/// This is allocation policy only: whether a received datagram may reach the
24/// protocol core — the conservative rejection of one filling the buffer
25/// included — belongs to [`socket_io::receive`].
26const RECEIVE_BUFFER_LEN: usize = 65_536;
27const MAINTENANCE_INTERVAL: Duration = Duration::from_secs(1);
28
29/// A Tokio UDP listener backed by one deterministic server core.
30///
31/// Processing is sequential: one task receives a datagram, lets the core
32/// process it, and sends the optional reply from the same socket. No
33/// per-datagram tasks or locks are involved.
34///
35/// **That sequential ownership is what makes the reply traffic class correct.**
36/// The class is a socket-wide setting, and every reply explicitly applies its
37/// own — zero included — immediately before its send, so the reply about to go
38/// out is always the one that set it. Nothing else sends from this socket, not
39/// even while a send is suspended, so no reply can inherit another session's
40/// marking. Adding a concurrent sender would break that and require per-packet
41/// control messages instead.
42///
43/// # Reply source address
44///
45/// A reply must leave from the exact address the request was sent to. An
46/// explicit-address listener gets that from the bind: it can send from nothing
47/// else. A wildcard listener (`0.0.0.0` or `[::]`) cannot, because the routing
48/// table would choose the source, so it asks the kernel for each request's
49/// local destination and sends that request's reply from it.
50///
51/// That per-packet path exists on Linux, macOS and FreeBSD. Elsewhere a
52/// wildcard bind is **refused** at construction — see
53/// [`ServerRuntimeError::WildcardSourceSelectionUnsupported`] — rather than
54/// served by a listener whose replies a client on a second address would
55/// silently discard. Explicit-address listeners are unaffected everywhere.
56#[derive(Debug)]
57pub struct Server {
58    socket: UdpSocket,
59    core: ServerCore,
60    recv_buffer: Vec<u8>,
61    /// The listener's own address family, which selects the IPv4 TOS or IPv6
62    /// Traffic Class option.
63    listener_is_ipv4: bool,
64    /// Whether this listener is wildcard-bound and must therefore recover each
65    /// request's local destination and reply from it.
66    ///
67    /// Decided once, from the bound address. It is never inferred later from a
68    /// peer: a listener that accepted a wildcard bind owes correct reply
69    /// sources to every request, not to the ones that look multi-homed.
70    select_reply_source: bool,
71    /// Whether this server has ever successfully applied a nonzero traffic
72    /// class to the socket.
73    ///
74    /// This is not a cache and never elides a call — the class is applied
75    /// before every send regardless. It is consulted only when applying one
76    /// *fails*, to tell "the socket may be carrying a marking of ours" from
77    /// "it cannot be carrying one".
78    socket_is_marked: bool,
79}
80
81impl Server {
82    /// Binds one UDP listener at `addr` and creates its independent session
83    /// namespace.
84    ///
85    /// A wildcard `addr` additionally configures reply source selection, and
86    /// fails where that is unavailable; see [`Server::from_socket`].
87    pub async fn bind(addr: SocketAddr, config: ServerConfig) -> Result<Self, ServerRuntimeError> {
88        let socket = UdpSocket::bind(addr)
89            .await
90            .map_err(|source| ServerRuntimeError::Bind { addr, source })?;
91        Self::from_socket(socket, config)
92    }
93
94    /// Wraps an already prepared Tokio UDP socket in a server.
95    ///
96    /// # Errors
97    ///
98    /// Construction is fallible because a wildcard listener has setup to do
99    /// before it may serve anything. It fails when the socket's bound address
100    /// cannot be queried — which is what says whether this is a wildcard
101    /// listener at all — and, for a wildcard socket, when this target has no
102    /// reply source selection or the kernel refuses to configure it.
103    ///
104    /// Failing here is the point. A wildcard listener that cannot recover a
105    /// request's local destination would start, run, and answer clients on a
106    /// second local address from an endpoint they never contacted, which they
107    /// discard as though the network had dropped it.
108    pub fn from_socket(
109        socket: UdpSocket,
110        config: ServerConfig,
111    ) -> Result<Self, ServerRuntimeError> {
112        Self::with_core(socket, ServerCore::new(config))
113    }
114
115    /// Test-only seam: builds a listener around an already-constructed core
116    /// instead of one this method would build from a [`ServerConfig`].
117    ///
118    /// This exists so a unit test can substitute a scripted
119    /// [`TokenSource`](crate::token::TokenSource) and make one listener in a
120    /// [`ServerSet`](crate::ServerSet) fail deterministically after it has
121    /// already bound and answered real traffic — the runtime otherwise offers
122    /// no way to make a core failure happen on command without OS-level
123    /// socket sabotage. Every other code path here — wildcard setup, kernel
124    /// timestamp configuration — is unchanged from [`Server::from_socket`].
125    #[cfg(test)]
126    pub(crate) fn from_socket_with_core(
127        socket: UdpSocket,
128        core: ServerCore,
129    ) -> Result<Self, ServerRuntimeError> {
130        Self::with_core(socket, core)
131    }
132
133    fn with_core(socket: UdpSocket, core: ServerCore) -> Result<Self, ServerRuntimeError> {
134        let addr = socket
135            .local_addr()
136            .map_err(|source| ServerRuntimeError::LocalAddr { source })?;
137        let select_reply_source = is_wildcard(addr);
138
139        if select_reply_source {
140            if !socket_io::SUPPORTED {
141                return Err(ServerRuntimeError::WildcardSourceSelectionUnsupported { addr });
142            }
143            socket_io::configure_destination_metadata(&socket, addr.is_ipv4())
144                .map_err(|source| ServerRuntimeError::SourceSelectionSetup { addr, source })?;
145        }
146
147        // Kernel receive timestamps, for every listener and strictly after the
148        // mandatory setup above so a refusal here can neither hide nor stand in
149        // for a wildcard listener's failure to secure its reply sources. This
150        // one is allowed to fail: it buys accuracy, not correctness, and a
151        // server that cannot have it is still a correct server.
152        let _ = socket_io::try_configure_kernel_rx_timestamp(&socket);
153
154        Ok(Self {
155            socket,
156            core,
157            recv_buffer: vec![0; RECEIVE_BUFFER_LEN],
158            listener_is_ipv4: marks_with_ipv4_option(addr),
159            select_reply_source,
160            socket_is_marked: false,
161        })
162    }
163
164    /// Applies one reply's raw traffic class to the listener socket, and
165    /// reports whether that reply may be sent.
166    ///
167    /// The class is applied before *every* send, and zero as deliberately as
168    /// any other value: a listener serves many sessions from one socket, so
169    /// skipping the call for an unmarked reply — an open reply, or a session
170    /// that negotiated nothing — would send it under whichever marking the
171    /// previous reply left behind.
172    ///
173    /// When the option cannot be applied, [`may_send_unappliable`] decides. A
174    /// marking this server asked for and did not get must never be replaced by
175    /// silently sending under the previous one, so those replies are dropped;
176    /// but a host that refuses the option outright — some Windows builds do not
177    /// support `IP_TOS`, and a few targets have no safe setter at all — must
178    /// still be able to run a server, and it can, because a socket this server
179    /// has never marked has no marking of ours to clear.
180    fn prepare_reply_traffic_class(&mut self, traffic_class: u8) -> bool {
181        match self.apply_reply_traffic_class(traffic_class) {
182            Ok(()) => {
183                self.socket_is_marked = traffic_class != 0;
184                true
185            }
186            Err(_) => may_send_unappliable(traffic_class, self.socket_is_marked),
187        }
188    }
189
190    fn apply_reply_traffic_class(&self, traffic_class: u8) -> io::Result<()> {
191        set_reply_traffic_class(
192            SockRef::from(&self.socket),
193            self.listener_is_ipv4,
194            traffic_class,
195        )
196    }
197
198    /// Returns the local endpoint selected for the listener.
199    pub fn local_addr(&self) -> Result<SocketAddr, ServerRuntimeError> {
200        self.socket
201            .local_addr()
202            .map_err(|source| ServerRuntimeError::LocalAddr { source })
203    }
204
205    /// Serves datagrams until `shutdown` completes.
206    ///
207    /// Graceful shutdown sends no session-close packets and leaves no hidden
208    /// receive task behind, even while a reply send is waiting for socket
209    /// writability. Per-packet send failures and short sends drop only that
210    /// reply, and so does a failure to apply a marking that reply needed —
211    /// preparing one packet is not a reason to stop serving every other
212    /// session, and the next reply makes its own independent attempt. Receive
213    /// failures and internal core failures terminate the loop with an error.
214    ///
215    /// A reply whose marking could not be applied is not sent instead: sending
216    /// it would put it on the wire under the previous reply's marking. An
217    /// *unmarked* reply on a socket this server has never marked is the one
218    /// exception, so a host that does not support the option at all — some
219    /// Windows builds do not support `IP_TOS` — still runs a working server.
220    ///
221    /// A wildcard listener drops a request whose local destination did not
222    /// arrive with it, before the core sees it. There is no fall back to a
223    /// routing-table source: the listener promised a correct reply source when
224    /// it accepted the bind, and a request it cannot answer correctly must not
225    /// move a session's receive, rate or lifetime state either.
226    pub async fn run<F>(&mut self, shutdown: F) -> Result<(), ServerRuntimeError>
227    where
228        F: Future<Output = ()>,
229    {
230        tokio::pin!(shutdown);
231        let mut maintenance = tokio::time::interval(MAINTENANCE_INTERVAL);
232        maintenance.set_missed_tick_behavior(MissedTickBehavior::Skip);
233        maintenance.tick().await;
234
235        loop {
236            tokio::select! {
237                _ = &mut shutdown => return Ok(()),
238                _ = maintenance.tick() => self.core.maintain(),
239                received = socket_io::receive(
240                    &self.socket,
241                    &mut self.recv_buffer,
242                    self.select_reply_source,
243                ) => {
244                    let received = match received {
245                        Ok(Some(received)) => received,
246                        Ok(None) => continue,
247                        Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
248                        Err(source) => return Err(ServerRuntimeError::Receive { source }),
249                    };
250                    let ReceivedDatagram {
251                        len,
252                        peer,
253                        reply_source,
254                        // Handed to the core with the datagram it describes, as
255                        // an explicit per-datagram input. The core samples its
256                        // own receive instant regardless and decides whether
257                        // this one is usable; nothing here judges it, and there
258                        // is no "next receive" state between the two.
259                        kernel_rx_timestamp,
260                    } = received;
261                    // `reply_source` stays on this side of the boundary: which
262                    // of the host's addresses a request arrived on is transport
263                    // state, not session policy.
264                    if let Some(reply) = self.core.handle_received_datagram(
265                        peer,
266                        &self.recv_buffer[..len],
267                        kernel_rx_timestamp,
268                    )? {
269                        if !self.prepare_reply_traffic_class(reply.traffic_class()) {
270                            continue;
271                        }
272                        let send = socket_io::send(
273                            &self.socket,
274                            reply.bytes(),
275                            peer,
276                            reply_source,
277                        );
278                        tokio::pin!(send);
279                        let sent = loop {
280                            tokio::select! {
281                                _ = &mut shutdown => return Ok(()),
282                                _ = maintenance.tick() => self.core.maintain(),
283                                sent = &mut send => break sent,
284                            }
285                        };
286                        if !matches!(sent, Ok(len) if len == reply.bytes().len()) {
287                            continue;
288                        }
289                    }
290                }
291            }
292        }
293    }
294}
295
296/// Whether a bound address names no particular local address, and so owes every
297/// reply an explicitly selected source.
298///
299/// `0.0.0.0` and `[::]` are the obvious forms. The third is the IPv4-mapped
300/// unspecified address, `[::ffff:0.0.0.0]`: Linux accepts it as an IPv4 wildcard
301/// bind and reports it back verbatim from `getsockname`, so asking
302/// `is_unspecified` alone would take a working wildcard listener for an explicit
303/// one and answer its requests from whatever source the routing table picked.
304/// (macOS normalizes that bind to `[::]` and never reaches the second test.)
305///
306/// A mapped address that names an actual host address — `[::ffff:127.0.0.1]` —
307/// is explicit, and stays on the plain path.
308fn is_wildcard(addr: SocketAddr) -> bool {
309    match addr.ip() {
310        IpAddr::V4(address) => address.is_unspecified(),
311        IpAddr::V6(address) => {
312            address.is_unspecified()
313                || address
314                    .to_ipv4_mapped()
315                    .is_some_and(|mapped| mapped.is_unspecified())
316        }
317    }
318}
319
320/// Whether a reply may still be sent after its traffic class could not be
321/// applied.
322///
323/// Only an unmarked reply on a socket this server has never successfully marked
324/// qualifies. Both halves matter: a reply that wanted a marking must not go out
325/// without one, and once this server has put a nonzero class on the socket, a
326/// failure to clear it means the socket may still be carrying it.
327///
328/// A socket handed to [`Server::from_socket`] pre-marked by its creator is
329/// outside what this can know; explicit marking is the server's own to manage
330/// from a fresh listener.
331fn may_send_unappliable(traffic_class: u8, socket_is_marked: bool) -> bool {
332    traffic_class == 0 && !socket_is_marked
333}
334
335/// Failure to create or run a Tokio UDP server.
336#[derive(Debug, Error)]
337pub enum ServerRuntimeError {
338    /// Binding the requested listener failed.
339    #[error("could not bind UDP listener at {addr}: {source}")]
340    Bind {
341        addr: SocketAddr,
342        #[source]
343        source: io::Error,
344    },
345    /// Querying the listener's selected local endpoint failed.
346    #[error("could not query UDP listener address: {source}")]
347    LocalAddr {
348        #[source]
349        source: io::Error,
350    },
351    /// A wildcard listener was requested on a target with no safe per-packet
352    /// reply source selection.
353    ///
354    /// The listener would answer from whichever local address the routing table
355    /// chose, which a client that contacted a different one discards.
356    #[error(
357        "wildcard listener {addr} cannot select its reply source address on this target: \
358         bind an explicit local address instead"
359    )]
360    WildcardSourceSelectionUnsupported { addr: SocketAddr },
361    /// Configuring destination-address metadata for a wildcard listener failed.
362    #[error(
363        "could not configure reply source-address selection for wildcard listener {addr}: \
364         {source}; bind an explicit local address instead"
365    )]
366    SourceSelectionSetup {
367        addr: SocketAddr,
368        #[source]
369        source: io::Error,
370    },
371    /// Receiving from the listener failed irrecoverably.
372    #[error("UDP receive failed: {source}")]
373    Receive {
374        #[source]
375        source: io::Error,
376    },
377    /// The deterministic protocol/session core failed internally.
378    #[error(transparent)]
379    Core(#[from] ServerError),
380}
381
382#[cfg(test)]
383mod tests {
384    use std::{net::Ipv4Addr, time::Duration};
385
386    use irtt_proto::{
387        decode_echo_reply, decode_open_reply, encode_request, Clock, Params, ReceivedStats,
388        RequestToEncode, StampAt,
389    };
390    use tokio::{net::UdpSocket, sync::oneshot, time::timeout};
391
392    use super::*;
393
394    #[tokio::test(flavor = "current_thread")]
395    async fn bind_reports_local_address_and_shutdown_stops_run() {
396        let requested = SocketAddr::from((Ipv4Addr::LOCALHOST, 0));
397        let mut server = Server::bind(requested, ServerConfig::default())
398            .await
399            .unwrap();
400        let bound = server.local_addr().unwrap();
401        assert_eq!(bound.ip(), requested.ip());
402        assert_ne!(bound.port(), 0);
403
404        let (shutdown_tx, shutdown_rx) = oneshot::channel();
405        shutdown_tx.send(()).unwrap();
406        server
407            .run(async {
408                let _ = shutdown_rx.await;
409            })
410            .await
411            .unwrap();
412    }
413
414    /// Construction is where a wildcard listener's promise is made or refused.
415    ///
416    /// Both outcomes are asserted from the same code, because which one a target
417    /// gets is exactly the thing under test: a target with a source-selection
418    /// path configures it and serves; one without refuses the bind rather than
419    /// starting a listener whose replies may come from an address no client
420    /// contacted. An explicit bind is unaffected either way.
421    #[tokio::test(flavor = "current_thread")]
422    async fn from_socket_settles_wildcard_source_selection_before_serving_anything() {
423        let explicit = UdpSocket::bind("127.0.0.1:0").await.unwrap();
424        assert!(Server::from_socket(explicit, ServerConfig::default()).is_ok());
425
426        let wildcard = UdpSocket::bind("0.0.0.0:0").await.unwrap();
427        let wildcard = Server::from_socket(wildcard, ServerConfig::default());
428        if socket_io::SUPPORTED {
429            let server = wildcard.expect("a supported target configures source selection");
430            assert!(server.select_reply_source);
431        } else {
432            assert!(matches!(
433                wildcard,
434                Err(ServerRuntimeError::WildcardSourceSelectionUnsupported { .. })
435            ));
436        }
437    }
438
439    #[tokio::test(flavor = "current_thread")]
440    async fn ipv4_open_echo_close_crosses_a_real_udp_socket() {
441        exercise_udp_path(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await;
442    }
443
444    #[tokio::test(flavor = "current_thread")]
445    async fn ipv6_open_echo_close_when_loopback_is_available() {
446        let addr = "[::1]:0".parse().unwrap();
447        let server = match Server::bind(addr, unthrottled()).await {
448            Ok(server) => server,
449            Err(ServerRuntimeError::Bind { source, .. })
450                if matches!(
451                    source.kind(),
452                    io::ErrorKind::AddrNotAvailable | io::ErrorKind::Unsupported
453                ) =>
454            {
455                return;
456            }
457            Err(error) => panic!("unexpected IPv6 loopback bind failure: {error}"),
458        };
459        exercise_bound_server(server).await;
460    }
461
462    async fn exercise_udp_path(addr: SocketAddr) {
463        let server = Server::bind(addr, unthrottled()).await.unwrap();
464        exercise_bound_server(server).await;
465    }
466
467    async fn exercise_bound_server(mut server: Server) {
468        let server_addr = server.local_addr().unwrap();
469        let client_bind = match server_addr {
470            SocketAddr::V4(_) => "127.0.0.1:0",
471            SocketAddr::V6(_) => "[::1]:0",
472        };
473        let client = UdpSocket::bind(client_bind).await.unwrap();
474        let (shutdown_tx, shutdown_rx) = oneshot::channel();
475        let server_task = tokio::spawn(async move {
476            server
477                .run(async {
478                    let _ = shutdown_rx.await;
479                })
480                .await
481        });
482
483        let requested = Params {
484            protocol_version: 1,
485            duration_ns: 1_000_000_000,
486            interval_ns: 100_000_000,
487            received_stats: ReceivedStats::Both,
488            stamp_at: StampAt::Both,
489            clock: Clock::Both,
490            ..Params::default()
491        };
492        let open = encode_request(
493            RequestToEncode::Open {
494                params: &requested,
495                no_test: false,
496            },
497            None,
498        )
499        .unwrap();
500        client.send_to(&open, server_addr).await.unwrap();
501
502        let mut buffer = vec![0; RECEIVE_BUFFER_LEN];
503        let (len, reply_source) = timeout(Duration::from_secs(1), client.recv_from(&mut buffer))
504            .await
505            .expect("OPEN reply timeout")
506            .unwrap();
507        assert_eq!(reply_source, server_addr);
508        let open_reply = decode_open_reply(&buffer[..len], None).unwrap();
509        assert_ne!(open_reply.token, 0);
510
511        let echo = encode_request(
512            RequestToEncode::Echo {
513                token: open_reply.token,
514                sequence: 0,
515                params: &open_reply.params,
516                payload: &[],
517            },
518            None,
519        )
520        .unwrap();
521        client.send_to(&echo, server_addr).await.unwrap();
522        let (len, reply_source) = timeout(Duration::from_secs(1), client.recv_from(&mut buffer))
523            .await
524            .expect("ECHO reply timeout")
525            .unwrap();
526        assert_eq!(reply_source, server_addr);
527        let echo_reply = decode_echo_reply(&buffer[..len], &open_reply.params, None).unwrap();
528        assert_eq!(echo_reply.token, open_reply.token);
529        assert_eq!(echo_reply.sequence, 0);
530        assert_eq!(echo_reply.recv_count, Some(1));
531        assert_eq!(echo_reply.recv_window, Some(1));
532
533        let close = encode_request(
534            RequestToEncode::Close {
535                token: open_reply.token,
536            },
537            None,
538        )
539        .unwrap();
540        client.send_to(&close, server_addr).await.unwrap();
541        client.send_to(&echo, server_addr).await.unwrap();
542        assert!(
543            timeout(Duration::from_millis(100), client.recv_from(&mut buffer))
544                .await
545                .is_err()
546        );
547
548        shutdown_tx.send(()).unwrap();
549        server_task.await.unwrap().unwrap();
550    }
551
552    fn unthrottled() -> ServerConfig {
553        ServerConfig::default().with_min_send_interval(Duration::ZERO)
554    }
555
556    /// Which bound addresses owe their replies a selected source.
557    ///
558    /// The IPv4-mapped unspecified address is the one that is not obvious, and
559    /// it is not hypothetical: Linux accepts `[::ffff:0.0.0.0]` as an IPv4
560    /// wildcard bind and reports it back unchanged, so missing it would leave a
561    /// working wildcard listener on the plain send path. A mapped address
562    /// naming a real host address is explicit and must stay there.
563    #[test]
564    fn a_bound_address_naming_no_particular_local_address_is_a_wildcard() {
565        for (addr, wildcard) in [
566            ("0.0.0.0:2112", true),
567            ("[::]:2112", true),
568            ("[::ffff:0.0.0.0]:2112", true),
569            ("127.0.0.1:2112", false),
570            ("192.0.2.10:2112", false),
571            ("[::1]:2112", false),
572            ("[::ffff:127.0.0.1]:2112", false),
573        ] {
574            assert_eq!(
575                is_wildcard(addr.parse().unwrap()),
576                wildcard,
577                "{addr} classified wrongly"
578            );
579        }
580    }
581
582    /// The rule that decides what an unappliable traffic class means, which no
583    /// normal interface can reach: it needs a host that refuses the socket
584    /// option, and the hosts these tests run on do not.
585    #[test]
586    fn only_an_unmarked_reply_on_a_never_marked_socket_survives_an_apply_failure() {
587        for (traffic_class, socket_is_marked, sendable, why) in [
588            (
589                0,
590                false,
591                true,
592                "no marking wanted, and none of ours to clear",
593            ),
594            (0, true, false, "our own marking may still be on the socket"),
595            (
596                0xb8,
597                false,
598                false,
599                "a marking that was wanted but not applied",
600            ),
601            (0xb8, true, false, "and likewise over a marking already set"),
602        ] {
603            assert_eq!(
604                may_send_unappliable(traffic_class, socket_is_marked),
605                sendable,
606                "{why}"
607            );
608        }
609    }
610}