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