Skip to main content

irtt_server/
core.rs

1use std::{
2    collections::HashMap,
3    net::SocketAddr,
4    time::{Duration, SystemTime},
5};
6
7use irtt_proto::{
8    decode_request, echo_packet_len, encode_echo_reply, encode_open_reply, verify_packet_hmac,
9    Clock, DecodedParams, DecodedRequestKind, EchoReply, OpenReply, Params, StampAt,
10    TimestampFields, FLAG_CLOSE, FLAG_OPEN, FLAG_REPLY,
11};
12
13use crate::{
14    clock::{saturating_ns, wall_ns_of, ClockSample, ClockSource, SystemClock},
15    config::ServerConfig,
16    error::ServerError,
17    fill::{echo_payload_len, negotiate_server_fill},
18    negotiate::negotiate_params,
19    session::{RateDecision, Session},
20    token::{OsTokenSource, TokenSource, TOKEN_ATTEMPTS},
21};
22
23/// The deterministic protocol and session engine of the server.
24///
25/// `ServerCore` owns admission, negotiation, the session table and reply
26/// construction. It performs no I/O: a caller hands it a source endpoint and
27/// the bytes of one received datagram, and gets back the bytes to send to that
28/// endpoint, or nothing. Everything it does is a pure function of its state and
29/// that input, except for drawing session tokens and sampling the clock an echo
30/// reply's timestamps come from — both of which are private injected seams, so
31/// the whole engine stays deterministically testable.
32///
33/// The Tokio runtime wraps it directly:
34///
35/// ```text
36/// recv_from -> core.handle_datagram(peer, packet) -> Some(reply) -> send_to
37///                                                 -> None        -> send nothing
38/// ```
39///
40/// A reply is an [`OutboundDatagram`]: the bytes to send plus the transport
41/// policy that sending them requires. The core is where session policy lives, so
42/// it hands that policy out with the packet rather than leaving the runtime to
43/// rediscover it from the session table or from the packet it is about to send.
44///
45/// It is public as a low-level engine for a pre-1.0 crate, not as a promise of
46/// runtime independence. The server is intentionally Tokio-native; this is not
47/// a transport abstraction, and there is no blocking or alternate-runtime
48/// counterpart.
49///
50/// # Example
51///
52/// The core can be driven directly with a normally encoded open request. It
53/// returns a reply to send and records the new session without performing I/O:
54///
55/// ```
56/// use std::net::{Ipv4Addr, SocketAddr};
57///
58/// use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
59/// use irtt_server::{ServerConfig, ServerCore};
60///
61/// let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 40_000));
62/// let mut core = ServerCore::new(ServerConfig::default());
63/// let params = Params::with_protocol_defaults();
64/// let request = encode_request(
65///     RequestToEncode::Open { params: &params, no_test: false },
66///     None,
67/// )
68/// .unwrap();
69///
70/// let reply = core.handle_datagram(peer, &request).unwrap().unwrap();
71/// let open = decode_open_reply(reply.bytes(), None).unwrap();
72/// assert_ne!(open.token, 0);
73/// assert_eq!(core.session_count(), 1);
74/// ```
75///
76/// # Scope
77///
78/// The core implements open handling, session creation and negotiation,
79/// normal echo processing, per-session rate limiting, idle expiry,
80/// maximum-duration close and client-initiated close. It decides the traffic
81/// class each reply is to be sent with; actually applying it to a socket is the
82/// runtime's job. It also settles each session's server fill and generates the
83/// payload bytes of every echo reply. Incoming authenticated requests enforce logical
84/// expiry exactly; the runtime also calls a private maintenance hook to reclaim
85/// idle sessions when no traffic arrives.
86#[derive(Debug)]
87pub struct ServerCore {
88    config: ServerConfig,
89    sessions: HashMap<u64, Session>,
90    tokens: Box<dyn TokenSource>,
91    clock: Box<dyn ClockSource>,
92}
93
94impl ServerCore {
95    /// Creates a server core drawing session tokens from the operating system's
96    /// random source and timestamps from the system clock.
97    #[must_use]
98    pub fn new(config: ServerConfig) -> Self {
99        Self::with_token_source(config, Box::new(OsTokenSource))
100    }
101
102    /// A core with a chosen token source and the production clock, which is
103    /// what tests that care about session identity but not about timestamp
104    /// values want.
105    pub(crate) fn with_token_source(config: ServerConfig, tokens: Box<dyn TokenSource>) -> Self {
106        Self::with_sources(config, tokens, Box::new(SystemClock::new()))
107    }
108
109    /// A core with both nondeterministic sources chosen.
110    pub(crate) fn with_sources(
111        config: ServerConfig,
112        tokens: Box<dyn TokenSource>,
113        clock: Box<dyn ClockSource>,
114    ) -> Self {
115        Self {
116            config,
117            sessions: HashMap::new(),
118            tokens,
119            clock,
120        }
121    }
122
123    /// This core's configuration.
124    #[must_use]
125    pub fn config(&self) -> &ServerConfig {
126        &self.config
127    }
128
129    /// The number of live sessions.
130    ///
131    /// This is the observable a caller genuinely needs — how much session state
132    /// the server is holding against its bound. The sessions themselves stay
133    /// crate-private until their shape settles.
134    #[must_use]
135    pub fn session_count(&self) -> usize {
136        self.sessions.len()
137    }
138
139    /// Looks up a live session by token.
140    ///
141    /// This is a lookup by token alone and is therefore *not* an admission
142    /// check: a request is bound to a session only when the token matches and
143    /// it arrived from the session's exact endpoint. Callers pair it with an
144    /// endpoint comparison, as close handling does.
145    pub(crate) fn session(&self, token: u64) -> Option<&Session> {
146        self.sessions.get(&token)
147    }
148
149    /// Handles one received datagram, returning the reply to send, if any.
150    ///
151    /// A reply is an [`OutboundDatagram`]: the encoded packet and the raw
152    /// traffic class it must be sent with. Open and no-test replies are always
153    /// unmarked, and an echo reply — including one carrying the
154    /// maximum-duration close — takes its session's negotiated class. The
155    /// runtime applies that class to the socket before sending; nothing here
156    /// touches a socket, and a class this core hands out is a request, not a
157    /// promise that the host can honor it.
158    ///
159    /// `Ok(None)` means "answer nothing". The protocol defines no error reply,
160    /// no reset and no NACK, so every rejection is a silent discard and a
161    /// client distinguishes "rejected" from "lost" only by timing out. A
162    /// datagram is discarded when it is structurally invalid, carries the reply
163    /// flag, disagrees with this server's authentication configuration, fails
164    /// authentication, carries a malformed or out-of-range open parameter
165    /// payload, negotiates to parameters the server could not execute, would
166    /// exceed the session bound, is longer than the configured maximum packet
167    /// length, or names no live session from the endpoint that opened it. None
168    /// of that mutates any live session.
169    ///
170    /// `Ok(None)` is not only a rejection, though. A close request that does
171    /// release its session is answered with nothing as well, because protocol
172    /// version 1 defines no acknowledgement for one — and so is an echo that
173    /// found its session's rate allowance spent, which advances no statistic
174    /// but does keep the session alive.
175    ///
176    /// Sessions whose idle deadline has passed are released before the request
177    /// is dispatched, so a request may find the session it names already gone,
178    /// and one request may release an unrelated session. Expiry is never
179    /// signalled; a client sees only silence.
180    ///
181    /// Authentication is checked before open parameters are decoded and before
182    /// any session is looked up. That ordering is deliberate: a packet that
183    /// fails authentication is discarded without parsing untrusted parameter
184    /// data or consulting the session table, and no reply behavior may reveal
185    /// which stage rejected it.
186    ///
187    /// Every instant this reports is one the core sampled itself: a caller hands
188    /// over bytes and an endpoint, not timings. The Tokio runtime, which does
189    /// have transport metadata, reaches the same implementation through a
190    /// crate-private entry point that may add a kernel-observed arrival time;
191    /// this path behaves exactly as it does with none.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`ServerError`] only for an internal failure — the random source
196    /// failing, no unique token being found within the retry budget, or a
197    /// server-generated reply failing to encode. Rejected input is never an
198    /// error.
199    pub fn handle_datagram(
200        &mut self,
201        peer: SocketAddr,
202        packet: &[u8],
203    ) -> Result<Option<OutboundDatagram>, ServerError> {
204        self.process(peer, packet, None)
205    }
206
207    /// Handles one received datagram together with what the transport observed
208    /// about its arrival.
209    ///
210    /// This is the entry point the Tokio runtime uses, and the only difference
211    /// from [`handle_datagram`](Self::handle_datagram) is `kernel_rx_timestamp`:
212    /// a wall-clock arrival instant the kernel recorded for this datagram, where
213    /// the listener could obtain one. Both paths run the same implementation,
214    /// and `None` reproduces the public path exactly.
215    ///
216    /// The observation is measurement input and nothing else. It may become the
217    /// receive wall value of an echo reply that negotiated one, and it can never
218    /// reach lifecycle, rate or deadline policy, the monotonic domain, or a
219    /// midpoint — those stay on the core's own paired userspace sample. An
220    /// implausible or unrepresentable value is not a reason to reject the
221    /// datagram; it simply is not used. See
222    /// [`preferred_receive_wall_ns`].
223    ///
224    /// It is crate-private on purpose: transport metadata reaches the core by
225    /// being passed with the datagram it describes, and there is no second
226    /// public API for it.
227    ///
228    /// # Errors
229    ///
230    /// As [`handle_datagram`](Self::handle_datagram).
231    pub(crate) fn handle_received_datagram(
232        &mut self,
233        peer: SocketAddr,
234        packet: &[u8],
235        kernel_rx_timestamp: Option<SystemTime>,
236    ) -> Result<Option<OutboundDatagram>, ServerError> {
237        self.process(peer, packet, kernel_rx_timestamp)
238    }
239
240    /// The one implementation behind both receive entry points.
241    fn process(
242        &mut self,
243        peer: SocketAddr,
244        packet: &[u8],
245        kernel_rx_timestamp: Option<SystemTime>,
246    ) -> Result<Option<OutboundDatagram>, ServerError> {
247        let Ok(request) = decode_request(packet) else {
248            return Ok(None);
249        };
250        // An echo's arrival instant is sampled the moment the datagram is known
251        // to be one, so that everything the reply's receive timestamp should
252        // bracket — authentication, the session lookup, the receive-state
253        // transition and encoding — happens after it. A datagram rejected later
254        // has consumed one clock sample and nothing else; sampling is not
255        // protocol state, and it is a strictly better trade than looking a
256        // session up before authentication to discover whether it even wanted
257        // timestamps.
258        let kind = self.classify(&request.kind, packet.len(), kernel_rx_timestamp);
259
260        // The HMAC flag must agree with this server's configuration in both
261        // directions of mismatch, and the MAC itself is verified separately.
262        if request.hmac_present != self.config.hmac_key().is_some() {
263            return Ok(None);
264        }
265        if let Some(key) = self.config.hmac_key() {
266            if verify_packet_hmac(key, packet).is_err() {
267                return Ok(None);
268            }
269        }
270
271        // One monotonic instant per authenticated, structurally valid request,
272        // which is the request's whole lifecycle time: what expiry is judged
273        // against, what a new session's deadlines start from, and what the rate
274        // limiter and maximum-duration deadline read. An echo already has one —
275        // the instant its reply will report as the receive time — and taking a
276        // second would let a reply's timestamps disagree with the policy
277        // decisions made about the very same datagram.
278        let now_ns = match kind {
279            ClassifiedKind::Echo(ref echo) => echo.received_at.mono_ns,
280            _ => self.clock.sample().mono_ns,
281        };
282        self.release_expired_sessions(now_ns);
283
284        match kind {
285            ClassifiedKind::Open { no_test, params } => {
286                self.handle_open(peer, no_test, params, now_ns)
287            }
288            ClassifiedKind::Close { token } => {
289                self.handle_close(peer, token);
290                Ok(None)
291            }
292            ClassifiedKind::Echo(echo) => self.handle_echo(peer, echo),
293        }
294    }
295
296    /// Reclaims sessions whose idle deadline has passed.
297    ///
298    /// The Tokio runtime calls this for physical background reclamation. Every
299    /// authenticated structurally valid datagram still performs the same sweep
300    /// before dispatch, so the timer cadence never weakens logical expiry.
301    pub(crate) fn maintain(&mut self) {
302        let now_ns = self.clock.sample().mono_ns;
303        self.release_expired_sessions(now_ns);
304    }
305
306    /// Drops every session whose idle deadline has passed, as of `now_ns`.
307    ///
308    /// Expiry is *logical* — a session is expired the instant its deadline
309    /// passes. Authenticated, structurally valid requests run this sweep before
310    /// dispatch, while the runtime's maintenance timer also invokes it for
311    /// physical reclamation when no traffic arrives. The table remains bounded,
312    /// and a stale session cannot deny capacity to a live one.
313    ///
314    /// The sweep is global and runs before the request is dispatched, so a
315    /// request touching one session can release another. That is deliberate:
316    /// expiry is a resource decision about the table, not a side effect of what
317    /// the current datagram happens to name. It also means a *no-test* open —
318    /// which creates nothing of its own — reclaims expired sessions like any
319    /// other open, where the reference server was observed not to. Both are
320    /// upstream policy the clean specification declines to require, and a
321    /// client cannot tell the difference: expiry is never signalled.
322    fn release_expired_sessions(&mut self, now_ns: i64) {
323        let idle_timeout_ns = saturating_ns(self.config.idle_timeout());
324        self.sessions
325            .retain(|_, session| !session.is_idle_expired(now_ns, idle_timeout_ns));
326    }
327
328    /// Pairs a structurally decoded request with the clock sample an echo
329    /// needs, leaving every other kind untouched.
330    ///
331    /// An echo also settles here which wall instant its reply may report as the
332    /// arrival time, because that is the moment both candidates are in hand.
333    /// `kernel_rx_timestamp` is ignored for every other kind: an open or a close
334    /// emits no receive timestamp, so having observed one changes nothing about
335    /// it.
336    fn classify<'a>(
337        &mut self,
338        kind: &DecodedRequestKind<'a>,
339        datagram_len: usize,
340        kernel_rx_timestamp: Option<SystemTime>,
341    ) -> ClassifiedKind<'a> {
342        match *kind {
343            DecodedRequestKind::Open { no_test, params } => {
344                ClassifiedKind::Open { no_test, params }
345            }
346            DecodedRequestKind::Close { token } => ClassifiedKind::Close { token },
347            // Everything after the sequence number is opaque, so the tail is
348            // read no further than its length.
349            DecodedRequestKind::Echo {
350                token, sequence, ..
351            } => {
352                let received_at = self.clock.sample();
353                ClassifiedKind::Echo(EchoRequest {
354                    token,
355                    sequence,
356                    datagram_len,
357                    received_at,
358                    receive_wall_ns: preferred_receive_wall_ns(kernel_rx_timestamp, received_at),
359                })
360            }
361        }
362    }
363
364    /// Answers an echo request that names one of this server's sessions.
365    ///
366    /// Admission runs before any state moves, and each step is a silent
367    /// discard: the datagram must not be longer than
368    /// [`max_packet_length`](ServerConfig::max_packet_length), its token must
369    /// name a live session, and it must have arrived from that session's own
370    /// endpoint. Authentication was already settled by the caller, as was idle
371    /// expiry — a request reaching here found its session still alive. A
372    /// request failing any of these leaves the receive state — of this session
373    /// and of every other — exactly where it was.
374    ///
375    /// Only the token and the sequence number mean anything. The request's own
376    /// length is judged against the configured maximum and then forgotten: it
377    /// does not have to equal the negotiated length, and it never sizes the
378    /// reply, which is laid out and sized from the session's negotiated
379    /// parameters alone.
380    ///
381    /// **Rate allowance is judged after admission and before the
382    /// maximum-duration deadline.** That order is the observed one and it
383    /// matters: a post-deadline echo with no allowance is simply dropped, and
384    /// the close it would have carried is not lost but handed to the next echo
385    /// that is served. A rate-limited request refreshes the session's idle
386    /// deadline — the one drop class the clean evidence records doing so — and
387    /// changes nothing else.
388    ///
389    /// **The maximum-duration close is an ordinary reply with the Close flag
390    /// added**, never a standalone packet: protocol version 1 has no other
391    /// in-band mechanism. It reports the triggering request in its statistics
392    /// exactly as any reply would, and the session is released once it has
393    /// encoded, so every later request is dropped as an unknown token.
394    ///
395    /// Every state transition is computed but not committed until the reply has
396    /// been encoded, so a reply this server failed to build cannot leave a
397    /// session claiming to have answered a request it never did — or, for a
398    /// close, leave the session released with no reply carrying the flag.
399    fn handle_echo(
400        &mut self,
401        peer: SocketAddr,
402        request: EchoRequest,
403    ) -> Result<Option<OutboundDatagram>, ServerError> {
404        // Resource policy, applied to the length actually received. This is
405        // `irtt-rs` policy rather than upstream-mechanism emulation: there is
406        // no interface-MTU effective length here, just the datagram the caller
407        // handed over. The comparison is strict — a request of exactly the
408        // maximum is served.
409        if request.datagram_len > self.config.max_packet_length() {
410            return Ok(None);
411        }
412        let Some(session) = self.sessions.get_mut(&request.token) else {
413            return Ok(None);
414        };
415        if !same_endpoint(session.peer(), peer) {
416            return Ok(None);
417        }
418
419        let now_ns = request.received_at.mono_ns;
420        let next_rate = match session.rate_state().charge(now_ns) {
421            RateDecision::Limited { next } => {
422                // Silence, and no statistic moves — but the session lives on,
423                // and its idle deadline moves with it.
424                let next_lifetime = session.lifetime_state().refreshed(now_ns);
425                session.commit_rate_state(next);
426                session.commit_lifetime_state(next_lifetime);
427                return Ok(None);
428            }
429            RateDecision::Allowed { next } => next,
430        };
431
432        // Only a session that has already served an echo has a deadline at all,
433        // so this can never fire on the request that starts the test.
434        let closing = self.config.max_test_duration().is_some_and(|maximum| {
435            session
436                .lifetime_state()
437                .max_duration_reached(now_ns, saturating_ns(maximum))
438        });
439
440        let next = session.receive_state().accepted(request.sequence);
441        let next_lifetime = session.lifetime_state().served(now_ns);
442        // The departure side, sampled as late as the core can: after admission
443        // and the transition, immediately before the reply is built. There is
444        // no socket here, so this is the core's departure instant rather than a
445        // kernel transmission time; the runtime slice does not need to rewrite
446        // it.
447        let sent_at = self.clock.sample();
448        let params = session.params();
449        // Fill bytes are prepared before the reply is built, so they go through
450        // the encoder's normal path and the packet's MAC covers them like every
451        // other byte. Nothing mutates a payload after encoding.
452        let payload_len = echo_payload_len(self.config.hmac_key().is_some(), params);
453        // Read before the session can be released below: a maximum-duration
454        // close is an ordinary echo reply and carries the session's marking like
455        // any other.
456        let traffic_class = transport_traffic_class(params);
457        let reply = EchoReply {
458            // Open must be clear or upstream clients abort. Close is set only
459            // for the maximum-duration limit, which is the sole trigger this
460            // server has. The authentication flag and field are the encoder's,
461            // decided by the key it is given — a close-flagged reply is
462            // authenticated by exactly the same path as any other.
463            flags: FLAG_REPLY | if closing { FLAG_CLOSE } else { 0 },
464            // Both are copied through unchanged: no token is allocated for an
465            // echo, and no sequence number is normalized or range-checked.
466            token: request.token,
467            sequence: request.sequence,
468            recv_count: params.received_stats.has_count().then_some(next.count),
469            recv_window: params.received_stats.has_window().then_some(next.window),
470            timestamps: timestamp_fields(
471                params,
472                request.received_at,
473                sent_at,
474                request.receive_wall_ns,
475            ),
476            // The session's negotiated fill, generated fresh for this reply and
477            // sized from the layout rather than from `params.length`. A `none`
478            // session contributes no bytes and the encoder's zero-fill stands,
479            // which is what keeps a no-fill reply from disclosing residue.
480            // Nothing from the request reaches here: its payload is opaque, and
481            // ServerFill describes the server's own bytes.
482            payload: session.fill().payload(payload_len),
483        };
484        let packet = encode_echo_reply(&reply, params, self.config.hmac_key())
485            .map_err(|source| ServerError::ReplyEncoding { source })?;
486
487        // The last fallible step is behind us, so the transitions can be made.
488        if closing {
489            // The reply already carries the state this request produced, so
490            // there is nothing worth committing into a session about to go. A
491            // failure above would have left it live and untouched instead.
492            self.sessions.remove(&request.token);
493        } else {
494            session.commit_rate_state(next_rate);
495            session.commit_receive_state(next);
496            session.commit_lifetime_state(next_lifetime);
497        }
498        Ok(Some(OutboundDatagram::new(packet, traffic_class)))
499    }
500
501    /// Negotiates an open request and, unless it is a no-test one, creates the
502    /// session it asks for.
503    ///
504    /// `now_ns` is the request's lifecycle instant, which becomes the new
505    /// session's idle origin: a session starts aging the moment it exists, not
506    /// when it first carries an echo, so one whose client never returns is
507    /// still reclaimed. Expired sessions were already swept, so the capacity
508    /// bound below is judged against sessions that are genuinely live.
509    fn handle_open(
510        &mut self,
511        peer: SocketAddr,
512        no_test: bool,
513        encoded_params: &[u8],
514        now_ns: i64,
515    ) -> Result<Option<OutboundDatagram>, ServerError> {
516        // The parameter decoder already rejects a truncated tag or value, a
517        // varint overflow, an out-of-range stats/stamp-at/clock enum, and a
518        // server-fill value that is oversized, short-buffered or not UTF-8.
519        let Ok(decoded) = Params::decode_with_presence(encoded_params) else {
520            return Ok(None);
521        };
522        if !open_params_are_admissible(&decoded) {
523            return Ok(None);
524        }
525        let mut params = negotiate_params(decoded.params, &self.config);
526        // Settled here, once per open, so echo processing never parses a
527        // descriptor. It returns what the session will actually fill with,
528        // which an absent or empty descriptor deliberately does not describe,
529        // and restricts `params` only where an explicit descriptor could not be
530        // honored. A no-test open runs it for the negotiated value alone and
531        // drops the mode, having no session to keep it in.
532        let fill = negotiate_server_fill(&mut params);
533        // Deliberately after negotiation, and before either open path: what has
534        // to be executable is the effective session, not the request.
535        if !negotiated_params_are_admissible(&params, &self.config) {
536            return Ok(None);
537        }
538
539        if no_test {
540            // No-test validates parameters without running a test: same
541            // negotiation, a zero token, and no state at all. It stays
542            // serviceable at capacity precisely because it creates nothing.
543            let reply = OpenReply {
544                flags: FLAG_OPEN | FLAG_REPLY | FLAG_CLOSE,
545                token: 0,
546                params,
547            };
548            return self
549                .encode_reply(&reply)
550                .map(|packet| Some(OutboundDatagram::unmarked(packet)));
551        }
552
553        if self.sessions.len() >= self.config.max_sessions() {
554            return Ok(None);
555        }
556
557        // Every fallible step runs before the table is touched, so an internal
558        // failure cannot leave a half-created session behind.
559        let token = self.allocate_token()?;
560        let reply = OpenReply {
561            flags: FLAG_OPEN | FLAG_REPLY,
562            token,
563            params,
564        };
565        let packet = self.encode_reply(&reply)?;
566
567        // The reply is encoded before its params move into the session, so
568        // nothing is cloned to keep both.
569        self.sessions.insert(
570            token,
571            Session::new(peer, reply.params, fill, &self.config, now_ns),
572        );
573        Ok(Some(OutboundDatagram::unmarked(packet)))
574    }
575
576    /// Releases the session a close request names, if that request owns one.
577    ///
578    /// A close is bound to a session only when the token matches *and* the
579    /// datagram arrived from that session's exact endpoint, so holding a token
580    /// is not by itself authority to tear the session down: a close from a
581    /// foreign endpoint leaves it live, and one close cannot take down a
582    /// sibling session that the same peer opened separately. A token that is
583    /// unknown, already closed, or zero — zero is reserved for no-test replies
584    /// and never issued — matches nothing and leaves the table untouched.
585    ///
586    /// Nothing is sent either way, which is why this returns no reply to
587    /// encode. Protocol version 1 defines no acknowledgement for a client
588    /// close, so a delivered close is indistinguishable to the client from a
589    /// lost one, and the freed capacity is immediately reusable because the
590    /// session table is the only record of it.
591    fn handle_close(&mut self, peer: SocketAddr, token: u64) {
592        let owns_session = self
593            .session(token)
594            .is_some_and(|session| same_endpoint(session.peer(), peer));
595        if owns_session {
596            self.sessions.remove(&token);
597        }
598    }
599
600    /// Encodes a reply this server constructed. Any failure here is internal.
601    fn encode_reply(&self, reply: &OpenReply) -> Result<Vec<u8>, ServerError> {
602        encode_open_reply(reply, self.config.hmac_key())
603            .map_err(|source| ServerError::ReplyEncoding { source })
604    }
605
606    /// Draws a token that is non-zero and unique among live sessions.
607    ///
608    /// Zero is reserved for no-test replies and is never issued to a session. A
609    /// value colliding with a live token is discarded rather than allowed to
610    /// overwrite that session. Both cases redraw, up to [`TOKEN_ATTEMPTS`].
611    fn allocate_token(&mut self) -> Result<u64, ServerError> {
612        for _ in 0..TOKEN_ATTEMPTS {
613            let token = self.tokens.next_token()?;
614            if token != 0 && !self.sessions.contains_key(&token) {
615                return Ok(token);
616            }
617        }
618        Err(ServerError::TokenExhausted {
619            attempts: TOKEN_ATTEMPTS,
620        })
621    }
622}
623
624/// One reply the core prepared, together with the transport policy it must be
625/// sent under.
626///
627/// The bytes alone are not a complete instruction. A session negotiates a
628/// traffic class, and only the core knows which session a reply belongs to, so
629/// the class travels out with the packet instead of the runtime decoding the
630/// outgoing datagram again or keeping a second copy of session state.
631///
632/// [`traffic_class`](Self::traffic_class) is the **raw 8-bit IPv4 TOS / IPv6
633/// Traffic Class byte**, not a six-bit codepoint: the codepoint is its upper six
634/// bits and the low two are ECN. Zero is a value like any other and means
635/// unmarked — it is deliberately not `Option<u8>`, because a runtime sharing one
636/// socket between sessions has to apply zero as explicitly as it applies
637/// anything else, or an unmarked reply inherits the previous one's marking.
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub struct OutboundDatagram {
640    bytes: Vec<u8>,
641    traffic_class: u8,
642}
643
644impl OutboundDatagram {
645    /// A reply to send with an explicit raw traffic-class byte.
646    fn new(bytes: Vec<u8>, traffic_class: u8) -> Self {
647        Self {
648            bytes,
649            traffic_class,
650        }
651    }
652
653    /// A reply to send unmarked.
654    fn unmarked(bytes: Vec<u8>) -> Self {
655        Self::new(bytes, 0)
656    }
657
658    /// The encoded reply.
659    #[must_use]
660    pub fn bytes(&self) -> &[u8] {
661        &self.bytes
662    }
663
664    /// The raw IPv4 TOS / IPv6 Traffic Class byte this reply is to be sent
665    /// with. Zero means unmarked.
666    #[must_use]
667    pub fn traffic_class(&self) -> u8 {
668        self.traffic_class
669    }
670
671    /// Consumes the datagram, yielding its encoded reply.
672    #[must_use]
673    pub fn into_bytes(self) -> Vec<u8> {
674        self.bytes
675    }
676}
677
678impl AsRef<[u8]> for OutboundDatagram {
679    fn as_ref(&self) -> &[u8] {
680        self.bytes()
681    }
682}
683
684/// The raw traffic-class byte a session's negotiated DSCP parameter asks for.
685///
686/// The wire parameter is an `i64` and this server deliberately accepts values
687/// outside `0..=255` during negotiation, returning and storing them unchanged.
688/// A value that is not a byte cannot be a TOS / Traffic Class byte, so the
689/// transport treats it as unmarked. That is **`irtt-rs` policy, not observed
690/// behavior**: the clean evidence records the reference host's handling of
691/// negative and oversized values as platform-specific and explicitly not a
692/// compatibility requirement, so there is nothing to reproduce. Zero is chosen
693/// over `as`-style truncation or two's-complement wrapping because neither
694/// −1 → 255 nor 256 → 0 is a marking the client asked for, and over refusing the
695/// session because a malformed-but-accepted open should stay usable.
696fn transport_traffic_class(params: &Params) -> u8 {
697    u8::try_from(params.dscp).unwrap_or(0)
698}
699
700/// A structurally decoded request kind, with an echo's arrival instant already
701/// sampled.
702///
703/// This mirrors [`DecodedRequestKind`] rather than replacing it: the decoder
704/// classifies the packet, and this records what the core had to capture at that
705/// moment. Keeping the two apart is what lets the clock be read before
706/// authentication without any handler having to ask whether a sample exists.
707enum ClassifiedKind<'a> {
708    Open { no_test: bool, params: &'a [u8] },
709    Close { token: u64 },
710    Echo(EchoRequest),
711}
712
713/// The whole of what an echo request means to this server.
714///
715/// Everything after the sequence number is opaque, so nothing beyond these
716/// fields is carried forward. `datagram_len` is the length the datagram
717/// actually arrived with, which the configured maximum judges; it is not
718/// required to match the negotiated length, and it never sizes the reply.
719///
720/// The two timing fields are deliberately separate and are not interchangeable.
721/// `received_at` is the core's own paired userspace instant and is the sole
722/// authority for everything about this request that is a same-host question:
723/// idle expiry, rate allowance, the maximum-duration deadline, and the
724/// monotonic timestamps a reply may carry. `receive_wall_ns` is only the
725/// wall-clock arrival value a reply may *report*, which is the kernel's reading
726/// where the transport observed a usable one. Keeping it out of `ClockSample`
727/// is the point: a kernel wall reading has no monotonic counterpart, and pairing
728/// it with `received_at.mono_ns` would claim two different physical instants
729/// were one sample.
730#[derive(Debug, Clone, Copy)]
731struct EchoRequest {
732    token: u64,
733    sequence: u32,
734    datagram_len: usize,
735    received_at: ClockSample,
736    receive_wall_ns: i64,
737}
738
739/// Largest lag behind the core's own receive sample that a kernel receive
740/// timestamp may show and still be used as a measurement endpoint.
741///
742/// This is a sanity guard rather than an expected kernel-to-userspace wakeup
743/// latency, which is orders of magnitude smaller. A reading this far behind the
744/// server's own sample most likely reflects a realtime clock discontinuity or
745/// otherwise unusable metadata, and falling back to a value this server knows it
746/// read is better than publishing a bad cross-host timing endpoint. The client
747/// applies the same bound to its own receive metadata.
748pub(crate) const MAX_KERNEL_RX_LAG: Duration = Duration::from_secs(1);
749
750/// The wall instant an echo reply should report as the request's arrival time.
751///
752/// A kernel receive timestamp is taken before the socket queue and the runtime's
753/// wakeup, so preferring it removes that delay from the upstream one-way delay a
754/// client computes. It is used only when it is plausible *for this datagram*: it
755/// must convert into the wire's signed-nanosecond vocabulary, it cannot be later
756/// than the sample that observed the same datagram, and it may not lag that
757/// sample by more than [`MAX_KERNEL_RX_LAG`]. The bound is inclusive.
758///
759/// Anything else falls back to the userspace wall reading. A failed check is
760/// never a reason to drop the datagram, refuse a reply or disturb session state:
761/// implausible metadata means the server measures as it did before the kernel
762/// offered any.
763///
764/// The comparison goes through `i128` because the difference of two `i64`
765/// nanosecond values can exceed `i64`.
766pub(crate) fn preferred_receive_wall_ns(
767    kernel_rx_timestamp: Option<SystemTime>,
768    received_at: ClockSample,
769) -> i64 {
770    let userspace_wall_ns = received_at.wall_ns;
771    let Some(kernel_wall_ns) = kernel_rx_timestamp.and_then(wall_ns_of) else {
772        return userspace_wall_ns;
773    };
774    let lag_ns = i128::from(userspace_wall_ns) - i128::from(kernel_wall_ns);
775    if (0..=i128::from(saturating_ns(MAX_KERNEL_RX_LAG))).contains(&lag_ns) {
776        kernel_wall_ns
777    } else {
778        userspace_wall_ns
779    }
780}
781
782/// Builds the timestamp fields a session negotiated, and only those.
783///
784/// [`StampAt`] selects which instants are reported and [`Clock`] selects which
785/// domains, exactly as the reply layout does, so a field this returns as `Some`
786/// is a field the encoder has a slot for. A midpoint is the per-domain mean of
787/// the two instants.
788///
789/// A single-clock midpoint therefore produces exactly one midpoint field. That
790/// is the conforming representation; upstream 0.9.1's habit of emitting both
791/// midpoint fields regardless of the negotiated clock is a defect this server
792/// deliberately does not reproduce. `irtt-proto` still *decodes* that form, so
793/// interoperating with a peer that sends it is unaffected.
794///
795/// [`Clock::Unspecified`] selects no domain and so produces no field. It cannot
796/// be reached with a non-none `StampAt` on an acknowledged session, because
797/// that combination is refused at open; nothing here needs to repair it.
798///
799/// The pair is ordered before anything is built from it, because a reply's
800/// receive instant must not be later than its send instant and the wall clock
801/// can be stepped backwards between the two readings. See
802/// [`ClockSample::not_after`]; the correction is confined to this one reply.
803///
804/// `receive_wall_ns` is the wall instant a reported arrival time should use —
805/// [`preferred_receive_wall_ns`], so the kernel's reading where there was a
806/// usable one and the userspace sample's own otherwise. It reaches exactly two
807/// fields, `recv_wall` under [`StampAt::Receive`] and under [`StampAt::Both`],
808/// and it is held to the send reading by the same rule the paired sample is.
809/// Everything else keeps the paired userspace samples:
810///
811/// - **Monotonic never moves.** A kernel wall reading has no monotonic
812///   counterpart, and inventing one would be precision the kernel never
813///   reported. So under [`Clock::Both`] a `recv_wall`/`recv_mono` pair may
814///   describe two different physical instants. That is a deliberate wire
815///   policy — each field is honest about the domain it names, and nothing
816///   computes across the two — and it is why the core does not model them as
817///   one sample.
818/// - **A midpoint is untouched.** It is the mean of the receive and send
819///   instants, so substituting only the wall receive endpoint would move
820///   `midpoint_wall` earlier by half the kernel-to-userspace delay: apparent
821///   upstream delay would improve by exactly as much as apparent downstream
822///   delay worsened, `midpoint_wall` and `midpoint_mono` would stop describing
823///   the same midpoint, and no server residency interval is recoverable from a
824///   midpoint to make up for it.
825/// - **[`StampAt::Send`] and [`StampAt::None`] report no arrival time at all**,
826///   so an observation cannot reach them.
827///
828/// One consequence is intended and worth stating: under [`Clock::Wall`] with
829/// [`StampAt::Both`], the interval a client computes from the two reported
830/// instants now spans from the kernel's arrival reading rather than from the
831/// server's wakeup, so it includes the kernel-to-userspace portion of the
832/// server's residency. Under [`Clock::Both`] the monotonic pair still yields the
833/// unchanged userspace interval.
834fn timestamp_fields(
835    params: &Params,
836    received: ClockSample,
837    sent: ClockSample,
838    receive_wall_ns: i64,
839) -> TimestampFields {
840    let received = received.not_after(sent);
841    // The reported arrival wall value obeys the same ordering rule, and needs
842    // its own application of it: it may not have come from `received`, so a
843    // backward wall step between the kernel's reading and the send sample would
844    // otherwise leave the reply claiming it was received after it was sent.
845    let receive_wall_ns = receive_wall_ns.min(sent.wall_ns);
846    let per_clock = |sample: ClockSample| {
847        (
848            params.clock.has_wall().then_some(sample.wall_ns),
849            params.clock.has_mono().then_some(sample.mono_ns),
850        )
851    };
852    let arrival = (
853        params.clock.has_wall().then_some(receive_wall_ns),
854        params.clock.has_mono().then_some(received.mono_ns),
855    );
856    let absent = (None, None);
857    let (recv, midpoint, send) = match params.stamp_at {
858        StampAt::None => (absent, absent, absent),
859        StampAt::Receive => (arrival, absent, absent),
860        StampAt::Send => (absent, absent, per_clock(sent)),
861        StampAt::Both => (arrival, absent, per_clock(sent)),
862        StampAt::Midpoint => (absent, per_clock(received.midpoint(sent)), absent),
863    };
864    TimestampFields {
865        recv_wall: recv.0,
866        recv_mono: recv.1,
867        midpoint_wall: midpoint.0,
868        midpoint_mono: midpoint.1,
869        send_wall: send.0,
870        send_mono: send.1,
871    }
872}
873
874/// Whether two source endpoints identify the same session endpoint.
875///
876/// The address family, the IP address and the UDP source port are established
877/// identity components: a request from a different port, address or family is
878/// dropped and leaves its session live.
879///
880/// The remaining two fields of [`SocketAddrV6`] are decided here rather than
881/// observed, which is why this is not simply `==`.
882///
883/// The flow label is excluded. It identifies no endpoint — it is routing
884/// metadata a sender or forwarding path may legitimately vary within one flow,
885/// and a receiver reports it only when asked to — so admitting it could strand
886/// a session its own client can then never close. It grants nothing in
887/// exchange: the token, address, port and scope are what an off-path attacker
888/// would have to guess.
889///
890/// The scope identifier is included, and that is **`irtt-rs` policy, not
891/// verified compatibility behavior**. The server specification lists the IPv6
892/// zone as an identity component, but its remaining-unknowns section records
893/// that multi-zone behavior could not be tested on a single-host platform, so
894/// no evidence settles how a reference server compares scoped addresses. Two
895/// peers reachable at the same link-local address in different zones are
896/// genuinely different peers, so treating the zone as identity is the
897/// conservative reading: the failure it prevents is one peer closing another's
898/// session, while the failure it risks is a scoped client having to reopen.
899/// Revisit if evidence ever settles the question.
900///
901/// [`SocketAddrV6`]: std::net::SocketAddrV6
902fn same_endpoint(session: SocketAddr, peer: SocketAddr) -> bool {
903    match (session, peer) {
904        (SocketAddr::V6(session), SocketAddr::V6(peer)) => {
905            session.ip() == peer.ip()
906                && session.port() == peer.port()
907                && session.scope_id() == peer.scope_id()
908        }
909        (session, peer) => session == peer,
910    }
911}
912
913/// Applies the open parameter rules the decoder cannot express.
914///
915/// This judges the *request*, before negotiation, which is why it takes the
916/// decoded form with its presence flags. Whether the resulting session is
917/// coherent is a separate question, asked after negotiation by
918/// [`negotiated_params_are_admissible`].
919///
920/// Duration and Interval are accepted when absent, taking the wire default
921/// zero, and rejected when explicitly present and not positive. The presence
922/// distinction is why the decoder reports it: testing the value alone would
923/// wrongly reject an open that simply omitted the tag, which includes the empty
924/// parameter payload.
925///
926/// Every other known parameter is already validated or deliberately
927/// unrestricted here. Protocol version is accepted at any value and rewritten
928/// during negotiation. Length and DSCP are accepted as decoded, including zero,
929/// negative and out-of-byte-range values; a DSCP the socket could never carry
930/// is negotiated unchanged and transported unmarked instead of being refused
931/// (see [`transport_traffic_class`]), and a server that disallows DSCP
932/// negotiates zero rather than rejecting the request. Unknown tags were ignored
933/// by the decoder
934/// and are not reflected in the reply.
935fn open_params_are_admissible(decoded: &DecodedParams) -> bool {
936    let admissible = |present: bool, value: i64| !present || value > 0;
937    admissible(decoded.presence.duration_ns, decoded.params.duration_ns)
938        && admissible(decoded.presence.interval_ns, decoded.params.interval_ns)
939}
940
941/// Whether the *effective* parameters describe a session this server could
942/// actually run.
943///
944/// This is the last gate before an open is acknowledged, and it asks a
945/// different question from [`open_params_are_admissible`]: not whether the
946/// request was well formed, but whether the parameters negotiation settled on
947/// are coherent. It therefore runs on the negotiated params, after
948/// [`negotiate_params`], and covers the no-test path too — a no-test open
949/// exists to tell a client what the session would be, so it must not report a
950/// session that could not exist.
951///
952/// Two rules so far.
953///
954/// **Selecting timestamps requires a specified clock.** A non-none `stamp_at`
955/// with [`Clock::Unspecified`] asks for timestamp fields from no clock at all,
956/// which lays out no timestamp field and leaves the session with a request the
957/// server cannot honor.
958///
959/// [`Clock::Unspecified`] means the Clock tag was absent — an explicit zero is
960/// already out of range for the decoder — so this cannot be reached by a
961/// conforming client, which sends a clock whenever it selects timestamps. The
962/// combination is silently rejected rather than repaired: synthesizing a clock
963/// would answer with a session the client never asked for, and rewriting
964/// `stamp_at` to none would silently drop the measurement it did ask for.
965/// Neither is the server's to decide.
966///
967/// **The session's echo datagram must fit
968/// [`max_packet_length`](ServerConfig::max_packet_length).** Negotiation already
969/// reduced an oversized Length, but that only bounds what the *parameter* asks
970/// for: the mandatory field block grows with the received statistics, the
971/// timestamps and — by 16 bytes — authentication, and can exceed the configured
972/// maximum on its own. A session whose smallest compliant reply is already too
973/// large is one the server could never answer an echo for, so it is refused at
974/// open rather than acknowledged and then found unserviceable.
975///
976/// The size comes from [`echo_packet_len`], the one packet-size authority, so
977/// this cannot drift from what an echo reply actually encodes. Its
978/// unrepresentable-length error is a rejection here, not a [`ServerError`]: a
979/// pathological requested length is remote input, not an internal failure. The
980/// default policy makes it unreachable for an acknowledged session anyway, since
981/// negotiation has already reduced the length to a representable maximum.
982///
983/// The check is deliberately *after* negotiation, which is what makes a
984/// configured timestamp allowance of
985/// [`TimestampAllowance::None`](crate::TimestampAllowance::None) accept this
986/// otherwise refused request: its effective `stamp_at` is [`StampAt::None`], so
987/// the absent clock selects nothing that was going to be reported and the
988/// session is executable exactly as negotiated.
989///
990/// [`ServerError`]: crate::ServerError
991fn negotiated_params_are_admissible(params: &Params, config: &ServerConfig) -> bool {
992    if params.stamp_at != StampAt::None && params.clock == Clock::Unspecified {
993        return false;
994    }
995    echo_packet_len(config.hmac_key().is_some(), params)
996        .is_ok_and(|len| len <= config.max_packet_length())
997}