Skip to main content

irtt_client/
async_client.rs

1use std::{
2    future::{poll_fn, Future},
3    io,
4    net::SocketAddr,
5    pin::Pin,
6    task::{Context, Poll},
7    time::{Duration, Instant},
8};
9
10use tokio::time;
11
12use crate::{
13    client::validate_datagram_length,
14    config::ClientConfig,
15    error::ClientError,
16    event::{ClientEvent, OpenOutcome},
17    receive::{drain_tx_timestamps, try_enable_tx_timestamping, try_recv_tokio_datagram},
18    session::machine::{
19        recv_buffer_size, OpenDatagramDisposition, PreparedOpenAcceptance, PreparedOpenRequest,
20        PreparedProbe, SessionMachine, TimeoutBatch, MAX_OPEN_PACKET_SIZE,
21    },
22    socket::{connect_tokio_udp_socket, resolve_remote_tokio},
23    socket_options::{apply_traffic_class_to_tokio_socket, clear_dscp_on_tokio_socket},
24    timing::ClientTimestamp,
25    SendProbeError, SendReceipt,
26};
27
28/// Maximum amount of immediately available opening work performed by one poll.
29///
30/// Opening may consume ignored UDP datagrams, roll over an expired attempt, or
31/// encounter readiness false positives without ever awaiting.  Bound that work
32/// so one managed target cannot retain the current-thread runtime.
33const OPEN_POLL_WORK_BUDGET: usize = 64;
34
35/// Maximum immediately available receive work performed by one
36/// [`AsyncClient::poll_recv`] poll.
37///
38/// A `WouldBlock` readiness false positive is naturally bounded: `try_io`
39/// clears the reactor's readiness on `WouldBlock`, so the next
40/// `poll_recv_ready` genuinely awaits a fresh edge. An `Interrupted`
41/// (`EINTR`) nonblocking `recvmsg` does not clear that readiness — the
42/// syscall was interrupted, not told there was nothing to read — so without
43/// an explicit bound a sustained signal delivery rate could otherwise retry
44/// in a tight loop on this executor thread. Bounding both cases the same way
45/// this crate already bounds open-attempt work keeps that impossible.
46const RECV_POLL_WORK_BUDGET: usize = 64;
47
48#[derive(Debug)]
49struct PreparedAsyncOpen {
50    machine: PreparedOpenAcceptance,
51    recv_buffer_len: Option<usize>,
52    negotiated_traffic_class: Option<u8>,
53}
54
55#[derive(Debug)]
56struct PreparedAsyncOpenFailure {
57    primary: ClientError,
58    machine: PreparedOpenAcceptance,
59}
60
61#[derive(Debug)]
62enum AsyncOpenCleanup {
63    Acceptance {
64        primary: ClientError,
65        packet: Option<Box<[u8]>>,
66    },
67    Adapter(Box<PreparedAsyncOpenFailure>),
68}
69
70impl AsyncOpenCleanup {
71    fn packet(&self) -> Option<&[u8]> {
72        match self {
73            Self::Acceptance { packet, .. } => packet.as_deref(),
74            Self::Adapter(failure) => failure.machine.cleanup_close_packet(),
75        }
76    }
77
78    fn into_primary(self) -> ClientError {
79        match self {
80            Self::Acceptance { primary, .. } => primary,
81            Self::Adapter(failure) => failure.primary,
82        }
83    }
84}
85
86/// Owned state for one asynchronous open transaction.
87///
88/// This state deliberately contains no reference to [`AsyncClient`], allowing
89/// a managed target to store the client and opening state side by side.
90#[derive(Debug)]
91pub(crate) struct AsyncOpenState {
92    attempt: usize,
93    deadline: Option<Instant>,
94    deadline_timer: Option<Pin<Box<time::Sleep>>>,
95    request_submitted: bool,
96    stop_after_current_attempt: bool,
97    stopped_after_current_attempt: bool,
98    buffer: [u8; MAX_OPEN_PACKET_SIZE],
99    cleanup: Option<AsyncOpenCleanup>,
100}
101
102impl AsyncOpenState {
103    pub(crate) fn new() -> Self {
104        Self {
105            attempt: 0,
106            deadline: None,
107            deadline_timer: None,
108            request_submitted: false,
109            stop_after_current_attempt: false,
110            stopped_after_current_attempt: false,
111            buffer: [0_u8; MAX_OPEN_PACKET_SIZE],
112            cleanup: None,
113        }
114    }
115
116    fn start_attempt(&mut self, timeout: Duration) -> Result<(), ClientError> {
117        let deadline = Instant::now()
118            .checked_add(timeout)
119            .ok_or(ClientError::DurationOverflow)?;
120        self.deadline = Some(deadline);
121        self.deadline_timer = Some(Box::pin(time::sleep_until(deadline.into())));
122        self.request_submitted = false;
123        Ok(())
124    }
125
126    fn finish_attempt(&mut self) {
127        self.attempt += 1;
128        self.deadline = None;
129        self.deadline_timer = None;
130        self.request_submitted = false;
131        self.stopped_after_current_attempt = self.stop_after_current_attempt;
132    }
133
134    pub(crate) fn has_in_flight_work(&self) -> bool {
135        self.request_submitted || self.cleanup.is_some()
136    }
137
138    pub(crate) fn request_stop_after_current_attempt(&mut self) {
139        self.stop_after_current_attempt = true;
140    }
141
142    pub(crate) fn stopped_after_current_attempt(&self) -> bool {
143        self.stopped_after_current_attempt
144    }
145
146    fn deadline(&self) -> Instant {
147        self.deadline
148            .expect("an open attempt has an active deadline")
149    }
150
151    fn deadline_timer(&mut self) -> &mut Pin<Box<time::Sleep>> {
152        self.deadline_timer
153            .as_mut()
154            .expect("an open attempt has an active deadline timer")
155    }
156}
157
158/// Low-level Tokio IRTT client for one connected UDP target.
159///
160/// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async
161/// methods are polled by the caller and require a current Tokio runtime with I/O
162/// and time enabled.
163///
164/// # Example
165///
166/// Use this low-level adapter when the application already owns Tokio and wants
167/// to drive the session lifecycle itself. This example type-checks without
168/// contacting a server.
169///
170/// ```no_run
171/// use irtt_client::{AsyncClient, ClientConfig};
172///
173/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
174/// let mut client = AsyncClient::connect("127.0.0.1:2112", ClientConfig::default()).await?;
175/// let outcome = client.open().await?;
176/// println!("opened: {outcome:?}");
177/// let sent = client.send_probe().await?;
178/// println!("sent: {sent:?}");
179/// let received = client.recv().await?;
180/// println!("received: {received:?}");
181/// let timed_out = client.poll_timeouts()?;
182/// println!("timeouts: {timed_out:?}");
183/// let closed = client.close().await?;
184/// println!("closed: {closed:?}");
185/// # Ok(())
186/// # }
187/// ```
188#[derive(Debug)]
189pub struct AsyncClient {
190    socket: tokio::net::UdpSocket,
191    machine: SessionMachine,
192    remote: SocketAddr,
193    recv_buffer: Vec<u8>,
194    applied_traffic_class: Option<u8>,
195    prepared_open: Option<PreparedOpenRequest>,
196    prepared_probe: Option<PreparedProbe>,
197    /// See `Client::tx_timestamping_enabled` in `client.rs` for the
198    /// invariant this tracks; it is the same adapter-owned capability flag.
199    tx_timestamping_enabled: bool,
200}
201
202impl AsyncClient {
203    /// Resolve the endpoint and construct one connected Tokio UDP
204    /// socket.
205    ///
206    /// Polling this future without a current Tokio runtime returns
207    /// [`ClientError::NoTokioRuntime`]. A runtime without enabled I/O or time
208    /// drivers is outside this type's runtime contract.
209    ///
210    /// The endpoint accepts a name or address; an omitted port defaults to 2112.
211    /// IPv6 literals may be bracketed or unbracketed with the default port.
212    /// The reusable config contains no endpoint identity.
213    pub async fn connect(
214        endpoint: impl AsRef<str>,
215        config: ClientConfig,
216    ) -> Result<Self, ClientError> {
217        tokio::runtime::Handle::try_current().map_err(|_| ClientError::NoTokioRuntime)?;
218        config.open.validate()?;
219        let remote = resolve_remote_tokio(endpoint.as_ref(), config.address_family).await?;
220        let machine = SessionMachine::new(config.clone(), remote)?;
221        let prepared_open = machine.prepare_open_request()?;
222        let socket = connect_tokio_udp_socket(&config.socket, remote, config.address_family)?;
223
224        Ok(Self {
225            socket,
226            machine,
227            remote,
228            recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
229            applied_traffic_class: None,
230            prepared_open: Some(prepared_open),
231            prepared_probe: None,
232            tx_timestamping_enabled: false,
233        })
234    }
235
236    /// Perform the IRTT open exchange.
237    ///
238    /// Each configured attempt uses one absolute deadline for one successful
239    /// request submission and all replies inspected during that attempt.
240    /// Malformed, unrelated, wrong-direction, and unauthenticated datagrams are
241    /// ignored without resending the request.
242    ///
243    /// Dropping this future before trusted acceptance leaves the local machine
244    /// connected with no negotiated adapter state. If it is dropped during
245    /// best-effort post-token cleanup, cleanup may remain incomplete because
246    /// this low-level client never detaches cleanup work.
247    pub async fn open(&mut self) -> Result<OpenOutcome, ClientError> {
248        let mut state = AsyncOpenState::new();
249        poll_fn(|cx| self.poll_open(&mut state, cx)).await
250    }
251
252    /// Send one echo probe now, committing protocol state only after acceptance.
253    /// The caller owns pacing and run duration.
254    ///
255    /// A prepared packet is retained across readiness false positives,
256    /// cancellation, and socket errors so a later call retries the same logical
257    /// probe without advancing state before kernel acceptance. Cancellation or
258    /// `WouldBlock` before submission is uncommitted. After acceptance there is
259    /// no suspension before infallible commitment, and the prepared probe is
260    /// cleared even if post-send processing returns [`SendProbeError::AfterCommit`].
261    /// That failure retains the authoritative receipt; earlier failures return
262    /// [`SendProbeError::NotCommitted`].
263    pub async fn send_probe(&mut self) -> Result<SendReceipt, SendProbeError> {
264        poll_fn(|cx| self.poll_send_probe(cx)).await
265    }
266
267    /// Await and classify one complete UDP datagram.
268    ///
269    /// State is validated before readiness is awaited. Readiness false positives
270    /// retry without changing protocol state. Authenticated peer-close events
271    /// remain authoritative even if best-effort DSCP cleanup fails.
272    pub async fn recv(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
273        poll_fn(|cx| self.poll_recv(cx)).await
274    }
275
276    /// Poll protocol timeouts using a newly captured monotonic timestamp.
277    pub fn poll_timeouts(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
278        self.poll_timeouts_at(Instant::now())
279    }
280
281    /// Poll protocol timeouts using the caller's monotonic timestamp.
282    pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
283        self.drain_tx_timestamps()?;
284        self.machine.poll_timeouts_at(now)
285    }
286
287    pub(crate) fn poll_timeouts_bounded_at(
288        &mut self,
289        now: Instant,
290        limit: usize,
291    ) -> Result<TimeoutBatch, ClientError> {
292        self.drain_tx_timestamps()?;
293        self.machine.poll_timeouts_bounded_at(now, limit)
294    }
295
296    /// Send the retained close packet and commit local close exactly once.
297    ///
298    /// DSCP is cleared only after write readiness and immediately before the
299    /// nonblocking send attempt. A `WouldBlock` result restores it before the
300    /// next await, so dropping this future at a suspension point cannot leave
301    /// an otherwise-open session with cleared DSCP.
302    pub async fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
303        poll_fn(|cx| self.poll_close(cx)).await
304    }
305
306    pub(crate) fn poll_open(
307        &mut self,
308        state: &mut AsyncOpenState,
309        cx: &mut Context<'_>,
310    ) -> Poll<Result<OpenOutcome, ClientError>> {
311        if self.prepared_open.is_none() {
312            if let Err(error) = self.machine.prepare_open_request() {
313                return Poll::Ready(Err(error));
314            }
315            unreachable!("connected async clients retain an open request");
316        }
317
318        for _ in 0..OPEN_POLL_WORK_BUDGET {
319            if state.cleanup.is_some() {
320                return self.poll_open_cleanup(state, cx);
321            }
322
323            let attempt_count = self.machine.config().open.timeouts.len();
324            if state.attempt >= attempt_count {
325                return Poll::Ready(Err(ClientError::OpenTimeout));
326            }
327            if state.stop_after_current_attempt && state.deadline.is_none() {
328                state.stopped_after_current_attempt = true;
329                return Poll::Ready(Err(ClientError::OpenTimeout));
330            }
331            if state.deadline.is_none() {
332                let timeout = self.machine.config().open.timeouts[state.attempt];
333                if let Err(error) = state.start_attempt(timeout) {
334                    return Poll::Ready(Err(error));
335                }
336            }
337
338            if !state.request_submitted {
339                match poll_writable_until(&self.socket, state.deadline_timer(), cx) {
340                    Poll::Pending => return Poll::Pending,
341                    Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
342                    Poll::Ready(Ok(false)) => {
343                        state.finish_attempt();
344                        continue;
345                    }
346                    Poll::Ready(Ok(true)) => {}
347                }
348
349                let request = self
350                    .prepared_open
351                    .as_ref()
352                    .expect("connected clients retain their prepared open request");
353                let send_result = self.socket.try_send(&request.bytes);
354                match send_result {
355                    Ok(bytes) => {
356                        state.request_submitted = true;
357                        if let Err(error) = validate_datagram_length(request.bytes.len(), bytes) {
358                            return Poll::Ready(Err(error));
359                        }
360                    }
361                    Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
362                    Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
363                }
364            }
365
366            match poll_readable_until(&self.socket, state.deadline_timer(), cx) {
367                Poll::Pending => return Poll::Pending,
368                Poll::Ready(Err(error)) => return Poll::Ready(Err(error)),
369                Poll::Ready(Ok(false)) => {
370                    state.finish_attempt();
371                    if state.stopped_after_current_attempt() {
372                        return Poll::Ready(Err(ClientError::OpenTimeout));
373                    }
374                    continue;
375                }
376                Poll::Ready(Ok(true)) => {}
377            }
378
379            let datagram = match try_recv_tokio_datagram(&self.socket, &mut state.buffer) {
380                Ok(datagram) => datagram,
381                // WouldBlock is a readiness false positive; Interrupted
382                // (EINTR) is a transient nonblocking-recvmsg interruption.
383                // Both retry through the surrounding `OPEN_POLL_WORK_BUDGET`-
384                // bounded loop above, which re-checks readiness before the
385                // next attempt and yields back to the executor once that
386                // budget is exhausted.
387                Err(error)
388                    if matches!(
389                        error.kind(),
390                        io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
391                    ) =>
392                {
393                    continue
394                }
395                Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
396            };
397            if datagram.received_at.mono > state.deadline() {
398                state.finish_attempt();
399                if state.stopped_after_current_attempt() {
400                    return Poll::Ready(Err(ClientError::OpenTimeout));
401                }
402                continue;
403            }
404
405            let reply = match self
406                .machine
407                .inspect_open_datagram(&state.buffer[..datagram.len])
408            {
409                Ok(OpenDatagramDisposition::Ignore) => continue,
410                Ok(OpenDatagramDisposition::Trusted(reply)) => reply,
411                Err(error) => return Poll::Ready(Err(error)),
412            };
413            let machine = match self
414                .machine
415                .prepare_open_acceptance(reply, datagram.received_at)
416            {
417                Ok(machine) => machine,
418                Err(failure) => {
419                    state.cleanup = Some(AsyncOpenCleanup::Acceptance {
420                        primary: failure.primary,
421                        packet: failure.cleanup_close,
422                    });
423                    continue;
424                }
425            };
426            let prepared = match self.prepare_async_open(machine) {
427                Ok(prepared) => prepared,
428                Err(failure) => {
429                    state.cleanup = Some(AsyncOpenCleanup::Adapter(failure));
430                    continue;
431                }
432            };
433            return Poll::Ready(Ok(self.commit_async_open(prepared)));
434        }
435
436        // We deliberately truncated known immediate opening work.  Arrange for
437        // another poll even if no new socket-readiness edge occurs.
438        cx.waker().wake_by_ref();
439        Poll::Pending
440    }
441
442    pub(crate) fn poll_send_probe(
443        &mut self,
444        cx: &mut Context<'_>,
445    ) -> Poll<Result<SendReceipt, SendProbeError>> {
446        if let Err(error) = self.machine.ensure_open() {
447            return Poll::Ready(Err(SendProbeError::NotCommitted(error)));
448        }
449        if self.prepared_probe.is_none() {
450            match self.machine.prepare_probe() {
451                Ok(prepared) => self.prepared_probe = Some(prepared),
452                Err(error) => return Poll::Ready(Err(SendProbeError::NotCommitted(error))),
453            }
454        }
455        loop {
456            match self.socket.poll_send_ready(cx) {
457                Poll::Pending => return Poll::Pending,
458                Poll::Ready(Ok(())) => {}
459                Poll::Ready(Err(error)) => {
460                    return Poll::Ready(Err(SendProbeError::NotCommitted(ClientError::Socket(
461                        error,
462                    ))));
463                }
464            }
465
466            let prepared = self
467                .prepared_probe
468                .as_ref()
469                .expect("prepared probe was retained across readiness");
470            let machine_preflight = match self.machine.preflight_probe_commit(prepared) {
471                Ok(preflight) => preflight,
472                Err(error) => return Poll::Ready(Err(SendProbeError::NotCommitted(error))),
473            };
474            let expected_bytes = prepared.bytes.len();
475
476            // Private pre-send anchor: finalizes all fallible commit work
477            // (timeout deadline arithmetic, kernel TX lower bound) before
478            // the send attempt. A WouldBlock retry samples a fresh one on
479            // the next loop iteration; only the successful attempt's anchor
480            // is retained (via `machine_commit`/`commit_probe_sent` below).
481            let send_anchor = ClientTimestamp::now();
482            let machine_commit = match self
483                .machine
484                .finalize_probe_commit(machine_preflight, send_anchor)
485            {
486                Ok(commit) => commit,
487                Err(error) => return Poll::Ready(Err(SendProbeError::NotCommitted(error))),
488            };
489            let send_call_start = Instant::now();
490            let send_result = self.socket.try_send(&prepared.bytes);
491            let bytes = match send_result {
492                Ok(bytes) => bytes,
493                // No measurement or machine commit: retry the transaction
494                // with a fresh send_anchor on the next loop iteration.
495                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
496                    self.machine.invalidate_kernel_tx_correlation();
497                    continue;
498                }
499                Err(error) => {
500                    self.machine.invalidate_kernel_tx_correlation();
501                    return Poll::Ready(Err(SendProbeError::NotCommitted(ClientError::Socket(
502                        error,
503                    ))));
504                }
505            };
506            let send_finished_at = Instant::now();
507
508            // Public post-send measurement, captured only after the
509            // successful send above. The subsequent machine commit
510            // is infallible.
511            let sent_at = ClientTimestamp::now();
512            let sent = self
513                .machine
514                .commit_probe_sent(machine_commit, sent_at, bytes);
515            self.prepared_probe = None;
516            let send_call = send_finished_at.saturating_duration_since(send_call_start);
517
518            let receipt = SendReceipt {
519                seq: sent.seq,
520                remote: self.remote,
521                sent_at: sent.sent_at,
522                bytes: sent.bytes,
523                send_call,
524            };
525            if let Err(source) = validate_datagram_length(expected_bytes, bytes) {
526                return Poll::Ready(Err(SendProbeError::AfterCommit {
527                    receipt,
528                    source: Box::new(source),
529                }));
530            }
531            if let Err(source) = self.drain_tx_timestamps() {
532                return Poll::Ready(Err(SendProbeError::AfterCommit {
533                    receipt,
534                    source: Box::new(source),
535                }));
536            }
537            return Poll::Ready(Ok(receipt));
538        }
539    }
540
541    pub(crate) fn poll_recv(
542        &mut self,
543        cx: &mut Context<'_>,
544    ) -> Poll<Result<Vec<ClientEvent>, ClientError>> {
545        if let Err(error) = self.machine.ensure_open() {
546            return Poll::Ready(Err(error));
547        }
548
549        for _ in 0..RECV_POLL_WORK_BUDGET {
550            match self.socket.poll_recv_ready(cx) {
551                Poll::Pending => return Poll::Pending,
552                Poll::Ready(Ok(())) => {}
553                Poll::Ready(Err(error)) => {
554                    return Poll::Ready(Err(ClientError::Socket(error)));
555                }
556            }
557
558            let datagram = match try_recv_tokio_datagram(&self.socket, &mut self.recv_buffer) {
559                Ok(datagram) => datagram,
560                // See `RECV_POLL_WORK_BUDGET`: both a readiness false
561                // positive and an interrupted nonblocking recvmsg retry
562                // within this bounded loop rather than failing the receive.
563                Err(error)
564                    if matches!(
565                        error.kind(),
566                        io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted
567                    ) =>
568                {
569                    continue
570                }
571                Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
572            };
573
574            // One final bounded drain before processing the reply, so a TX
575            // timestamp that raced the reply is still associated with its
576            // probe before that probe is looked up and possibly removed
577            // below.
578            if let Err(error) = self.drain_tx_timestamps() {
579                return Poll::Ready(Err(error));
580            }
581
582            let events = match self.machine.process_received_echo_packet(
583                &self.recv_buffer[..datagram.len],
584                datagram.received_at,
585                datagram.meta,
586            ) {
587                Ok(events) => events,
588                Err(error) => return Poll::Ready(Err(error)),
589            };
590            if self.machine.is_peer_closed() {
591                self.prepared_probe = None;
592                if clear_dscp_on_tokio_socket(&self.socket, self.remote).is_ok() {
593                    self.applied_traffic_class = None;
594                }
595            }
596            return Poll::Ready(Ok(events));
597        }
598
599        // Truncated known immediate receive work, exactly as `poll_open`
600        // does: arrange for another poll even if no new socket-readiness
601        // edge occurs, rather than spinning this executor thread further.
602        cx.waker().wake_by_ref();
603        Poll::Pending
604    }
605
606    pub(crate) fn poll_close(
607        &mut self,
608        cx: &mut Context<'_>,
609    ) -> Poll<Result<Vec<ClientEvent>, ClientError>> {
610        loop {
611            let prepared = match self.machine.prepare_close() {
612                Ok(prepared) => prepared,
613                Err(error) => return Poll::Ready(Err(error)),
614            };
615            let mut events = Vec::new();
616            if let Err(source) = events.try_reserve(1) {
617                return Poll::Ready(Err(ClientError::AllocationFailed {
618                    operation: "close event result",
619                    source,
620                }));
621            }
622            let previous_traffic_class = self.applied_traffic_class;
623            let expected_bytes = prepared.bytes.len();
624
625            match self.socket.poll_send_ready(cx) {
626                Poll::Pending => return Poll::Pending,
627                Poll::Ready(Ok(())) => {}
628                Poll::Ready(Err(error)) => {
629                    return Poll::Ready(Err(ClientError::Socket(error)));
630                }
631            }
632            if let Err(error) = clear_dscp_on_tokio_socket(&self.socket, self.remote) {
633                return Poll::Ready(Err(error));
634            }
635            let mut rollback =
636                DscpRollback::armed(&self.socket, self.remote, previous_traffic_class);
637            let send_result = self.socket.try_send(prepared.bytes);
638            let bytes = match send_result {
639                Ok(bytes) => bytes,
640                Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
641                    if let Err(error) = rollback.restore() {
642                        return Poll::Ready(Err(error));
643                    }
644
645                    continue;
646                }
647                Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
648            };
649
650            let close_sent_at = ClientTimestamp::now();
651
652            let event = self
653                .machine
654                .commit_local_close(prepared.commit, close_sent_at);
655            rollback.disarm();
656            self.prepared_probe = None;
657            self.applied_traffic_class = None;
658
659            if let Err(error) = validate_datagram_length(expected_bytes, bytes) {
660                return Poll::Ready(Err(error));
661            }
662            events.push(event);
663            return Poll::Ready(Ok(events));
664        }
665    }
666
667    /// Return the configured local probe timeout.
668    pub fn probe_timeout(&self) -> Duration {
669        self.machine.probe_timeout()
670    }
671
672    /// Accepted semantics and exact peer parameters of the currently live session.
673    ///
674    /// Returns `None` before Open, after Close, and after no-test completion.
675    pub fn negotiation(&self) -> Option<&crate::NegotiationResult> {
676        self.machine.negotiation()
677    }
678
679    /// Whether any sent probes still await a reply or timeout.
680    pub fn has_pending_probes(&self) -> bool {
681        !self.machine.pending_is_empty()
682    }
683
684    /// Return whether an authenticated peer close ended the session.
685    pub fn is_peer_closed(&self) -> bool {
686        self.machine.is_peer_closed()
687    }
688
689    pub(crate) fn remote_addr(&self) -> SocketAddr {
690        self.remote
691    }
692
693    pub(crate) fn packets_sent(&self) -> u64 {
694        self.machine.packets_sent()
695    }
696
697    /// Earliest timeout deadline among probes still awaiting a reply.
698    pub fn next_probe_timeout_deadline(&self) -> Option<Instant> {
699        self.machine.next_probe_timeout_deadline()
700    }
701
702    pub(crate) fn latest_probe_timeout_deadline(&self) -> Option<Instant> {
703        self.machine.latest_probe_timeout_deadline()
704    }
705
706    pub(crate) fn discard_prepared_probe(&mut self) {
707        self.prepared_probe = None;
708    }
709
710    fn prepare_async_open(
711        &mut self,
712        machine: PreparedOpenAcceptance,
713    ) -> Result<PreparedAsyncOpen, Box<PreparedAsyncOpenFailure>> {
714        let Some(negotiated) = machine.normal_negotiated() else {
715            return Ok(PreparedAsyncOpen {
716                machine,
717                recv_buffer_len: None,
718                negotiated_traffic_class: None,
719            });
720        };
721        let recv_buffer_len = match recv_buffer_size(self.machine.has_hmac(), Some(negotiated)) {
722            Ok(size) => size,
723            Err(primary) => {
724                return Err(Box::new(PreparedAsyncOpenFailure { primary, machine }));
725            }
726        };
727        let negotiated_traffic_class = negotiated.accepted.dscp << 2;
728        let additional = recv_buffer_len.saturating_sub(self.recv_buffer.len());
729        if let Err(source) = self.recv_buffer.try_reserve(additional) {
730            return Err(Box::new(PreparedAsyncOpenFailure {
731                primary: ClientError::AllocationFailed {
732                    operation: "negotiated receive buffer",
733                    source,
734                },
735                machine,
736            }));
737        }
738
739        let dscp_result = apply_traffic_class_to_tokio_socket(
740            &self.socket,
741            self.remote,
742            negotiated_traffic_class,
743        );
744        if let Err(primary) = dscp_result {
745            self.restore_dscp_best_effort(self.applied_traffic_class);
746            return Err(Box::new(PreparedAsyncOpenFailure { primary, machine }));
747        }
748
749        Ok(PreparedAsyncOpen {
750            machine,
751            recv_buffer_len: Some(recv_buffer_len),
752            negotiated_traffic_class: Some(negotiated_traffic_class),
753        })
754    }
755
756    fn commit_async_open(&mut self, prepared: PreparedAsyncOpen) -> OpenOutcome {
757        if let Some(recv_buffer_len) = prepared.recv_buffer_len {
758            self.recv_buffer.resize(recv_buffer_len, 0);
759        }
760        let outcome = self.machine.commit_open(prepared.machine);
761
762        self.applied_traffic_class = prepared.negotiated_traffic_class;
763        self.prepared_open = None;
764        self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
765        outcome
766    }
767
768    /// See `Client::drain_tx_timestamps` in `client.rs`: same bounded,
769    /// nonblocking, best-effort drain, adapted to the Tokio socket.
770    fn drain_tx_timestamps(&mut self) -> Result<(), ClientError> {
771        if !self.tx_timestamping_enabled {
772            return Ok(());
773        }
774        let machine = &mut self.machine;
775        drain_tx_timestamps(&self.socket, |id, timestamp| {
776            machine.record_kernel_tx_timestamp(id, timestamp);
777        })
778        .map_err(ClientError::Socket)
779    }
780
781    fn poll_open_cleanup(
782        &self,
783        state: &mut AsyncOpenState,
784        cx: &mut Context<'_>,
785    ) -> Poll<Result<OpenOutcome, ClientError>> {
786        let Some(packet) = state
787            .cleanup
788            .as_ref()
789            .expect("cleanup polling requires a retained primary error")
790            .packet()
791        else {
792            return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary()));
793        };
794        let send_result = self.socket.try_send(packet);
795        match send_result {
796            Ok(_) => return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary())),
797            Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
798            Err(_) => return Poll::Ready(Err(state.cleanup.take().unwrap().into_primary())),
799        }
800
801        match poll_writable_until(&self.socket, state.deadline_timer(), cx) {
802            Poll::Pending => Poll::Pending,
803            Poll::Ready(Ok(true)) => {
804                // Retain cleanup state and resume the send in a fresh poll.  A
805                // writable readiness false positive must not spin this future.
806                cx.waker().wake_by_ref();
807                Poll::Pending
808            }
809            Poll::Ready(Ok(false) | Err(_)) => {
810                Poll::Ready(Err(state.cleanup.take().unwrap().into_primary()))
811            }
812        }
813    }
814
815    fn restore_dscp_best_effort(&self, previous_traffic_class: Option<u8>) {
816        let _ = match previous_traffic_class {
817            Some(traffic_class) => {
818                apply_traffic_class_to_tokio_socket(&self.socket, self.remote, traffic_class)
819            }
820            None => clear_dscp_on_tokio_socket(&self.socket, self.remote),
821        };
822    }
823}
824
825struct DscpRollback<'a> {
826    socket: &'a tokio::net::UdpSocket,
827    remote: SocketAddr,
828    previous_traffic_class: Option<u8>,
829    armed: bool,
830}
831
832impl<'a> DscpRollback<'a> {
833    fn armed(
834        socket: &'a tokio::net::UdpSocket,
835        remote: SocketAddr,
836        previous_traffic_class: Option<u8>,
837    ) -> Self {
838        Self {
839            socket,
840            remote,
841            previous_traffic_class,
842            armed: true,
843        }
844    }
845
846    fn restore(&mut self) -> Result<(), ClientError> {
847        if !self.armed {
848            return Ok(());
849        }
850
851        match self.previous_traffic_class {
852            Some(traffic_class) => {
853                apply_traffic_class_to_tokio_socket(self.socket, self.remote, traffic_class)?
854            }
855            None => clear_dscp_on_tokio_socket(self.socket, self.remote)?,
856        }
857        self.armed = false;
858        Ok(())
859    }
860
861    fn disarm(&mut self) {
862        self.armed = false;
863    }
864}
865
866impl Drop for DscpRollback<'_> {
867    fn drop(&mut self) {
868        if self.armed {
869            let _ = self.restore();
870        }
871    }
872}
873
874fn poll_writable_until(
875    socket: &tokio::net::UdpSocket,
876    deadline: &mut Pin<Box<time::Sleep>>,
877    cx: &mut Context<'_>,
878) -> Poll<Result<bool, ClientError>> {
879    match socket.poll_send_ready(cx) {
880        Poll::Ready(Ok(())) => Poll::Ready(Ok(true)),
881        Poll::Ready(Err(error)) => Poll::Ready(Err(ClientError::Socket(error))),
882        Poll::Pending => match deadline.as_mut().poll(cx) {
883            Poll::Ready(()) => Poll::Ready(Ok(false)),
884            Poll::Pending => Poll::Pending,
885        },
886    }
887}
888
889fn poll_readable_until(
890    socket: &tokio::net::UdpSocket,
891    deadline: &mut Pin<Box<time::Sleep>>,
892    cx: &mut Context<'_>,
893) -> Poll<Result<bool, ClientError>> {
894    match socket.poll_recv_ready(cx) {
895        Poll::Ready(Ok(())) => Poll::Ready(Ok(true)),
896        Poll::Ready(Err(error)) => Poll::Ready(Err(ClientError::Socket(error))),
897        Poll::Pending => match deadline.as_mut().poll(cx) {
898            Poll::Ready(()) => Poll::Ready(Ok(false)),
899            Poll::Pending => Poll::Pending,
900        },
901    }
902}