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}