1use std::{
2 io,
3 net::{SocketAddr, UdpSocket},
4 time::{Duration, Instant},
5};
6
7#[cfg(test)]
8use std::cell::Cell;
9
10#[cfg(test)]
11use irtt_proto::{flags, Params, TimestampFields, PROTOCOL_VERSION};
12
13use crate::{
14 config::{ClientConfig, RecvBudget},
15 error::ClientError,
16 event::{ClientEvent, OpenOutcome},
17 receive::{drain_tx_timestamps, recv_datagram, try_enable_tx_timestamping, ReceivedDatagram},
18 session::machine::{
19 recv_buffer_size, OpenDatagramDisposition, PreparedOpenAcceptance, ProbeSent,
20 SessionMachine, MAX_OPEN_PACKET_SIZE,
21 },
22 socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
23 socket_options::{apply_traffic_class_to_socket, clear_dscp_on_socket},
24 timing::ClientTimestamp,
25};
26
27pub(crate) mod schedule;
28
29use schedule::{instant_abs_diff, ProbeSchedule};
30
31#[derive(Debug, Clone, Copy)]
32enum ProbeScheduleMode {
33 CallerPaced,
34 #[cfg(test)]
35 Managed {
36 scheduled_at: Instant,
37 },
38}
39
40#[derive(Debug)]
41struct PreparedClientOpen {
42 machine: PreparedOpenAcceptance,
43 schedule: Option<ProbeSchedule>,
44 recv_buffer_len: Option<usize>,
45 negotiated_traffic_class: Option<u8>,
46 previous_traffic_class: Option<u8>,
47 post_open_recv_timeout: Option<Duration>,
48}
49
50#[derive(Debug)]
51struct PreparedClientOpenFailure {
52 primary: ClientError,
53 machine: PreparedOpenAcceptance,
54}
55
56#[cfg(test)]
57#[derive(Debug, Default)]
58struct ClientTestHooks {
59 fail_open_dscp: Cell<bool>,
60 fail_open_timeout_restore: Cell<bool>,
61 fail_close_event_reserve: Cell<bool>,
62 fail_close_dscp_clear: Cell<bool>,
63 fail_close_send: Cell<bool>,
64 close_reported_len: Cell<Option<usize>>,
65 close_send_attempts: Cell<usize>,
66 close_sent_at: Cell<Option<ClientTimestamp>>,
67 fail_cleanup_send: Cell<bool>,
68 recv_buffer_len_override: Cell<Option<usize>>,
69 fail_dscp_restore: Cell<bool>,
70 last_restored_read_timeout: Cell<Option<Duration>>,
71 prepared_active_session_before_dscp: Cell<bool>,
72 inject_recv_interrupted: Cell<usize>,
76}
77
78#[cfg(test)]
79#[derive(Debug, Clone, Copy)]
80pub(crate) struct ProbeSendTimestamps {
81 pub(crate) permission_at: Instant,
82 pub(crate) send_anchor: ClientTimestamp,
84 pub(crate) send_call_start: Instant,
85 pub(crate) send_finished_at: Instant,
86 pub(crate) sent_at: ClientTimestamp,
88}
89
90#[cfg(test)]
91use crate::{
92 session::machine::{
93 compute_one_way, compute_rtt, params_from_config, sequence_is_after, sequence_is_before,
94 unix_epoch_ns_i64, update_highest_received,
95 },
96 session::negotiate_params,
97 NegotiatedParams, RunMode, SignedDuration, WarningKind, MAX_UDP_PAYLOAD_LENGTH,
98};
99
100#[derive(Debug)]
132pub struct Client {
133 runtime: SessionMachine,
134 schedule: Option<ProbeSchedule>,
135 socket: UdpSocket,
136 remote: SocketAddr,
137 recv_buffer: Vec<u8>,
138 applied_traffic_class: Option<u8>,
139 tx_timestamping_enabled: bool,
146 #[cfg(test)]
147 test_hooks: ClientTestHooks,
148 #[cfg(test)]
149 probe_reported_len: Option<usize>,
150 #[cfg(test)]
151 probe_send_error: bool,
152 #[cfg(test)]
153 probe_send_timestamps: Option<ProbeSendTimestamps>,
154}
155
156impl Client {
157 pub fn connect(config: ClientConfig) -> Result<Self, ClientError> {
163 validate_open_timeouts(&config.open_timeouts)?;
164 let remote = resolve_remote(&config)?;
165 let runtime = SessionMachine::new(config.clone(), remote)?;
166 let socket = connect_udp_socket(&config.socket_config, remote)?;
167
168 Ok(Self {
169 runtime,
170 schedule: None,
171 socket,
172 remote,
173 recv_buffer: vec![0_u8; recv_buffer_size(false, None)?],
174 applied_traffic_class: None,
175 tx_timestamping_enabled: false,
176 #[cfg(test)]
177 test_hooks: ClientTestHooks::default(),
178 #[cfg(test)]
179 probe_reported_len: None,
180 #[cfg(test)]
181 probe_send_error: false,
182 #[cfg(test)]
183 probe_send_timestamps: None,
184 })
185 }
186
187 pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
204 let result = self.open_transaction();
205 if result.is_err() {
206 let _ =
207 self.restore_open_read_timeout(self.runtime.config().socket_config.recv_timeout);
208 }
209 result
210 }
211
212 pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
219 let prepared = self.runtime.prepare_close()?;
220 let mut events = Vec::new();
221 #[cfg(test)]
222 if self.test_hooks.fail_close_event_reserve.replace(false) {
223 events
224 .try_reserve(usize::MAX)
225 .map_err(|source| ClientError::AllocationFailed {
226 operation: "close event result",
227 source,
228 })?;
229 }
230 events
231 .try_reserve(1)
232 .map_err(|source| ClientError::AllocationFailed {
233 operation: "close event result",
234 source,
235 })?;
236 let previous_traffic_class = self.applied_traffic_class;
237 let expected_bytes = prepared.bytes.len();
238
239 self.clear_close_dscp()?;
240 let bytes = match self.send_close_datagram(prepared.bytes) {
241 Ok(bytes) => bytes,
242 Err(err) => {
243 self.restore_dscp_best_effort(previous_traffic_class);
244 return Err(ClientError::Socket(err));
245 }
246 };
247 #[cfg(not(test))]
248 let sent_at = ClientTimestamp::now();
249 #[cfg(test)]
250 let sent_at = self
251 .test_hooks
252 .close_sent_at
253 .take()
254 .unwrap_or_else(ClientTimestamp::now);
255 let event = self.runtime.commit_local_close(prepared.commit, sent_at);
256 self.schedule = None;
257 self.applied_traffic_class = None;
258
259 validate_datagram_length(expected_bytes, bytes)?;
260 events.push(event);
261 Ok(events)
262 }
263
264 pub fn next_send_deadline(&self) -> Option<Instant> {
267 if !self.runtime.is_open() {
268 return None;
269 }
270 self.schedule.as_ref()?.next_send_deadline()
271 }
272
273 pub fn probe_timeout(&self) -> Duration {
275 self.runtime.probe_timeout()
276 }
277
278 pub fn send_probe(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
284 self.send_probe_transaction(ProbeScheduleMode::CallerPaced)
285 }
286
287 pub fn recv_once(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
292 self.socket
293 .set_read_timeout(self.runtime.config().socket_config.recv_timeout)?;
294 self.recv_once_inner()
295 }
296
297 fn recv_datagram_once(&mut self) -> Result<ReceivedDatagram, io::Error> {
302 #[cfg(test)]
303 {
304 let remaining = self.test_hooks.inject_recv_interrupted.get();
305 if remaining > 0 {
306 self.test_hooks.inject_recv_interrupted.set(remaining - 1);
307 return Err(io::Error::from(io::ErrorKind::Interrupted));
308 }
309 }
310 recv_datagram(&self.socket, &mut self.recv_buffer)
311 }
312
313 fn recv_datagram_once_into(&self, buf: &mut [u8]) -> Result<ReceivedDatagram, io::Error> {
317 #[cfg(test)]
318 {
319 let remaining = self.test_hooks.inject_recv_interrupted.get();
320 if remaining > 0 {
321 self.test_hooks.inject_recv_interrupted.set(remaining - 1);
322 return Err(io::Error::from(io::ErrorKind::Interrupted));
323 }
324 }
325 recv_datagram(&self.socket, buf)
326 }
327
328 fn recv_once_inner(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
329 let Some(datagram) = self.recv_datagram_retrying_interrupted()? else {
330 return Ok(vec![]);
331 };
332
333 self.drain_tx_timestamps()?;
337
338 let events = self.runtime.process_received_echo_packet(
339 &self.recv_buffer[..datagram.len],
340 datagram.received_at,
341 datagram.meta,
342 )?;
343 if self.runtime.is_peer_closed() {
344 self.schedule = None;
345 if self.clear_close_dscp().is_ok() {
346 self.applied_traffic_class = None;
347 }
348 }
349 Ok(events)
350 }
351
352 fn recv_datagram_retrying_interrupted(
366 &mut self,
367 ) -> Result<Option<ReceivedDatagram>, ClientError> {
368 let configured_timeout = self.runtime.config().socket_config.recv_timeout;
369 let deadline = match configured_timeout {
370 Some(timeout) => Some(
371 Instant::now()
372 .checked_add(timeout)
373 .ok_or(ClientError::DurationOverflow)?,
374 ),
375 None => None,
376 };
377 let mut timeout_narrowed = false;
378
379 let result = loop {
380 match self.recv_datagram_once() {
381 Ok(datagram) => break Ok(Some(datagram)),
382 Err(err)
383 if matches!(
384 err.kind(),
385 io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
386 ) =>
387 {
388 break Ok(None);
389 }
390 Err(err) if err.kind() == io::ErrorKind::Interrupted => {
391 if let Some(deadline) = deadline {
392 let Some(remaining) = deadline.checked_duration_since(Instant::now())
393 else {
394 break Ok(None);
395 };
396 if remaining.is_zero() {
397 break Ok(None);
398 }
399 if let Err(err) = self.socket.set_read_timeout(Some(remaining)) {
400 break Err(ClientError::Socket(err));
401 }
402 timeout_narrowed = true;
403 }
404 }
405 Err(err) => break Err(ClientError::Socket(err)),
406 }
407 };
408
409 if timeout_narrowed {
410 if let Err(err) = self.socket.set_read_timeout(configured_timeout) {
411 return Err(ClientError::Socket(err));
412 }
413 }
414 result
415 }
416
417 pub fn recv_available(&mut self, budget: RecvBudget) -> Result<Vec<ClientEvent>, ClientError> {
420 self.socket
421 .set_read_timeout(self.runtime.config().socket_config.recv_timeout)?;
422 let mut all_events = Vec::new();
423 for _ in 0..budget.max_packets {
424 let events = self.recv_once_inner()?;
425 if events.is_empty() {
426 break;
427 }
428 all_events.extend(events);
429 if self.is_peer_closed() {
430 break;
431 }
432 }
433 Ok(all_events)
434 }
435
436 pub fn poll_timeouts(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
438 self.poll_timeouts_at(Instant::now())
439 }
440
441 pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
449 self.drain_tx_timestamps()?;
450 self.runtime.poll_timeouts_at(now)
451 }
452
453 pub fn is_run_complete(&self) -> bool {
459 self.runtime.is_terminal()
460 || self
461 .schedule
462 .as_ref()
463 .is_some_and(|schedule| schedule.is_finished() && self.runtime.pending_is_empty())
464 }
465
466 pub fn is_peer_closed(&self) -> bool {
473 self.runtime.is_peer_closed()
474 }
475
476 #[cfg(test)]
477 pub(crate) fn send_managed_probe(
478 &mut self,
479 scheduled_at: Instant,
480 ) -> Result<Vec<ClientEvent>, ClientError> {
481 self.send_probe_transaction(ProbeScheduleMode::Managed { scheduled_at })
482 }
483
484 fn send_probe_transaction(
485 &mut self,
486 mode: ProbeScheduleMode,
487 ) -> Result<Vec<ClientEvent>, ClientError> {
488 self.runtime.ensure_open()?;
489 #[cfg(test)]
490 let test_timestamps = self.probe_send_timestamps.take();
491 #[cfg(test)]
492 let fail_send = std::mem::take(&mut self.probe_send_error);
493 let remote = self.remote;
494 let (runtime, schedule, socket) = (&mut self.runtime, &mut self.schedule, &self.socket);
495 let schedule = schedule
496 .as_mut()
497 .expect("open sessions always have a probe schedule");
498 #[cfg(not(test))]
499 let permission_at = Instant::now();
500 #[cfg(test)]
501 let permission_at = test_timestamps
502 .map(|timestamps| timestamps.permission_at)
503 .unwrap_or_else(Instant::now);
504 if !schedule.permit_probe_at(permission_at) {
505 return Ok(Vec::new());
506 }
507
508 let Some(prepared) = runtime.prepare_probe()? else {
509 return Ok(Vec::new());
510 };
511 let machine_preflight = runtime.preflight_probe_commit(&prepared)?;
512 let schedule_commit = match mode {
513 ProbeScheduleMode::CallerPaced => {
514 schedule.preflight_caller_commit(machine_preflight.next_packets_sent)?
515 }
516 #[cfg(test)]
517 ProbeScheduleMode::Managed { scheduled_at } => {
518 schedule.preflight_managed_commit(scheduled_at, permission_at)?
519 }
520 };
521 let mut events = Vec::new();
522 events
523 .try_reserve(1)
524 .map_err(|source| ClientError::AllocationFailed {
525 operation: "probe event result",
526 source,
527 })?;
528
529 let expected_bytes = prepared.bytes.len();
530 let scheduled_at = schedule_commit.scheduled_at;
531 #[cfg(test)]
532 let reported_bytes = self.probe_reported_len.take();
533 #[cfg(not(test))]
538 let send_anchor = ClientTimestamp::now();
539 #[cfg(test)]
540 let send_anchor = test_timestamps
541 .map(|timestamps| timestamps.send_anchor)
542 .unwrap_or_else(ClientTimestamp::now);
543 let machine_commit = runtime.finalize_probe_commit(machine_preflight, send_anchor)?;
544 #[cfg(not(test))]
545 let send_call_start = Instant::now();
546 #[cfg(test)]
547 let send_call_start = test_timestamps
548 .map(|timestamps| timestamps.send_call_start)
549 .unwrap_or_else(Instant::now);
550 #[cfg(test)]
551 if fail_send {
552 runtime.invalidate_kernel_tx_correlation();
553 return Err(ClientError::Socket(io::Error::other(
554 "injected probe send failure",
555 )));
556 }
557 let bytes = match socket.send(&prepared.bytes) {
558 Ok(bytes) => bytes,
559 Err(error) => {
560 runtime.invalidate_kernel_tx_correlation();
561 return Err(ClientError::Socket(error));
562 }
563 };
564 #[cfg(not(test))]
565 let send_finished_at = Instant::now();
566 #[cfg(test)]
567 let send_finished_at = test_timestamps
568 .map(|timestamps| timestamps.send_finished_at)
569 .unwrap_or_else(Instant::now);
570 #[cfg(not(test))]
575 let sent_at = ClientTimestamp::now();
576 #[cfg(test)]
577 let sent_at = test_timestamps
578 .map(|timestamps| timestamps.sent_at)
579 .unwrap_or_else(ClientTimestamp::now);
580 let sent = runtime.commit_probe_sent(machine_commit, sent_at, bytes);
581 schedule.commit(schedule_commit);
582
583 let send_call = send_finished_at.saturating_duration_since(send_call_start);
584 let timer_error = instant_abs_diff(sent_at.mono, scheduled_at);
585 #[cfg(test)]
586 let bytes = reported_bytes.unwrap_or(bytes);
587 validate_datagram_length(expected_bytes, bytes)?;
588 self.drain_tx_timestamps()?;
589
590 events.push(echo_sent_event(
591 remote,
592 sent,
593 send_call,
594 scheduled_at,
595 timer_error,
596 ));
597 Ok(events)
598 }
599
600 fn open_transaction(&mut self) -> Result<OpenOutcome, ClientError> {
601 let request = self.runtime.prepare_open_request()?;
602 let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
603 let attempt_count = self.runtime.config().open_timeouts.len();
604
605 for attempt in 0..attempt_count {
606 let timeout = self.runtime.config().open_timeouts[attempt];
607 let deadline = Instant::now()
608 .checked_add(timeout)
609 .ok_or(ClientError::DurationOverflow)?;
610 self.socket.set_read_timeout(Some(timeout))?;
611 self.socket.send(&request.bytes)?;
612
613 loop {
614 let now = Instant::now();
615 let Some(remaining) = deadline.checked_duration_since(now) else {
616 break;
617 };
618 if remaining.is_zero() {
619 break;
620 }
621 self.socket.set_read_timeout(Some(remaining))?;
622
623 let datagram = match self.recv_datagram_once_into(&mut buf) {
624 Ok(datagram) => datagram,
625 Err(err)
634 if matches!(
635 err.kind(),
636 io::ErrorKind::WouldBlock
637 | io::ErrorKind::TimedOut
638 | io::ErrorKind::Interrupted
639 ) =>
640 {
641 if Instant::now() >= deadline {
642 break;
643 }
644 continue;
645 }
646 Err(err) => return Err(ClientError::Socket(err)),
647 };
648 if datagram.received_at.mono > deadline {
649 break;
650 }
651
652 let reply = match self.runtime.inspect_open_datagram(&buf[..datagram.len])? {
653 OpenDatagramDisposition::Ignore => continue,
654 OpenDatagramDisposition::Trusted(reply) => reply,
655 };
656 let machine = match self
657 .runtime
658 .prepare_open_acceptance(reply, datagram.received_at)
659 {
660 Ok(machine) => machine,
661 Err(failure) => {
662 self.send_cleanup_close_best_effort(failure.cleanup_close.as_deref());
663 return Err(failure.primary);
664 }
665 };
666 let prepared = match self.prepare_client_open(machine, datagram.received_at) {
667 Ok(prepared) => prepared,
668 Err(failure) => {
669 self.send_cleanup_close_best_effort(failure.machine.cleanup_close_packet());
670 return Err(failure.primary);
671 }
672 };
673 return self.apply_prepared_open(prepared);
674 }
675 }
676
677 Err(ClientError::OpenTimeout)
678 }
679
680 fn prepare_client_open(
681 &self,
682 machine: PreparedOpenAcceptance,
683 opened_at: ClientTimestamp,
684 ) -> Result<PreparedClientOpen, Box<PreparedClientOpenFailure>> {
685 let Some(negotiated) = machine.normal_negotiated() else {
686 return Ok(PreparedClientOpen {
687 machine,
688 schedule: None,
689 recv_buffer_len: None,
690 negotiated_traffic_class: None,
691 previous_traffic_class: self.applied_traffic_class,
692 post_open_recv_timeout: self.runtime.config().socket_config.recv_timeout,
693 });
694 };
695 let schedule = match ProbeSchedule::new(opened_at.mono, negotiated) {
696 Ok(schedule) => schedule,
697 Err(primary) => return Err(Box::new(PreparedClientOpenFailure { primary, machine })),
698 };
699 let recv_buffer_len = match recv_buffer_size(self.runtime.has_hmac(), Some(negotiated)) {
700 Ok(size) => size,
701 Err(primary) => return Err(Box::new(PreparedClientOpenFailure { primary, machine })),
702 };
703 #[cfg(test)]
704 let recv_buffer_len = self
705 .test_hooks
706 .recv_buffer_len_override
707 .get()
708 .unwrap_or(recv_buffer_len);
709 let negotiated_traffic_class = match u8::try_from(negotiated.params.dscp) {
710 Ok(traffic_class) => traffic_class,
711 Err(_) => {
712 return Err(Box::new(PreparedClientOpenFailure {
713 primary: ClientError::InvalidConfig {
714 reason: "negotiated dscp must be in range 0..=255".to_owned(),
715 },
716 machine,
717 }));
718 }
719 };
720
721 Ok(PreparedClientOpen {
722 machine,
723 schedule: Some(schedule),
724 recv_buffer_len: Some(recv_buffer_len),
725 negotiated_traffic_class: Some(negotiated_traffic_class),
726 previous_traffic_class: self.applied_traffic_class,
727 post_open_recv_timeout: self.runtime.config().socket_config.recv_timeout,
728 })
729 }
730
731 fn apply_prepared_open(
732 &mut self,
733 prepared: PreparedClientOpen,
734 ) -> Result<OpenOutcome, ClientError> {
735 let PreparedClientOpen {
736 machine,
737 schedule,
738 recv_buffer_len,
739 negotiated_traffic_class,
740 previous_traffic_class,
741 post_open_recv_timeout,
742 } = prepared;
743
744 if let (Some(recv_buffer_len), Some(negotiated_traffic_class)) =
745 (recv_buffer_len, negotiated_traffic_class)
746 {
747 let previous_len = self.recv_buffer.len();
748 let additional = recv_buffer_len.saturating_sub(previous_len);
749 if let Err(source) = self.recv_buffer.try_reserve(additional) {
750 self.send_cleanup_close_best_effort(machine.cleanup_close_packet());
751 return Err(ClientError::AllocationFailed {
752 operation: "negotiated receive buffer",
753 source,
754 });
755 }
756 self.recv_buffer.resize(recv_buffer_len, 0);
757
758 #[cfg(test)]
759 self.test_hooks
760 .prepared_active_session_before_dscp
761 .set(machine.has_prepared_active_session());
762 if let Err(primary) = self.apply_open_dscp(negotiated_traffic_class) {
763 self.recv_buffer.truncate(previous_len);
764 self.restore_dscp_best_effort(previous_traffic_class);
765 self.send_cleanup_close_best_effort(machine.cleanup_close_packet());
766 return Err(primary);
767 }
768 if let Err(source) = self.restore_open_read_timeout(post_open_recv_timeout) {
769 let primary = ClientError::ReadTimeoutRestore { source };
770 self.recv_buffer.truncate(previous_len);
771 self.restore_dscp_best_effort(previous_traffic_class);
772 self.send_cleanup_close_best_effort(machine.cleanup_close_packet());
773 return Err(primary);
774 }
775
776 let outcome = self.runtime.commit_open(machine);
777 self.schedule = schedule;
778 self.applied_traffic_class = Some(negotiated_traffic_class);
779 self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
780 Ok(outcome)
781 } else {
782 debug_assert!(schedule.is_none());
783 if let Err(source) = self.restore_open_read_timeout(post_open_recv_timeout) {
784 return Err(ClientError::ReadTimeoutRestore { source });
785 }
786 let outcome = self.runtime.commit_open(machine);
787 self.schedule = None;
788 self.applied_traffic_class = None;
789 self.tx_timestamping_enabled = try_enable_tx_timestamping(&self.socket);
790 Ok(outcome)
791 }
792 }
793
794 fn drain_tx_timestamps(&mut self) -> Result<(), ClientError> {
801 if !self.tx_timestamping_enabled {
802 return Ok(());
803 }
804 let runtime = &mut self.runtime;
805 drain_tx_timestamps(&self.socket, |id, timestamp| {
806 runtime.record_kernel_tx_timestamp(id, timestamp);
807 })
808 .map_err(ClientError::Socket)
809 }
810
811 fn restore_dscp_best_effort(&self, previous_traffic_class: Option<u8>) {
812 #[cfg(test)]
813 if self.test_hooks.fail_dscp_restore.replace(false) {
814 return;
815 }
816 let _ = match previous_traffic_class {
817 Some(traffic_class) => {
818 apply_traffic_class_to_socket(&self.socket, self.remote, traffic_class)
819 }
820 None => clear_dscp_on_socket(&self.socket, self.remote),
821 };
822 }
823
824 fn clear_close_dscp(&self) -> Result<(), ClientError> {
825 #[cfg(test)]
826 if self.test_hooks.fail_close_dscp_clear.replace(false) {
827 return Err(ClientError::SocketOption {
828 operation: "clear negotiated DSCP",
829 remote: self.remote,
830 source: io::Error::other("injected negotiated DSCP clear failure"),
831 });
832 }
833 clear_dscp_on_socket(&self.socket, self.remote)
834 }
835
836 fn send_cleanup_close_best_effort(&self, packet: Option<&[u8]>) {
837 if let Some(packet) = packet {
838 #[cfg(test)]
839 if self.test_hooks.fail_cleanup_send.replace(false) {
840 return;
841 }
842 let _ = self.socket.send(packet);
843 }
844 }
845
846 fn apply_open_dscp(&self, traffic_class: u8) -> Result<(), ClientError> {
847 #[cfg(test)]
848 if self.test_hooks.fail_open_dscp.replace(false) {
849 return Err(ClientError::SocketOption {
850 operation: "set negotiated DSCP",
851 remote: self.remote,
852 source: io::Error::other("injected negotiated DSCP failure"),
853 });
854 }
855 apply_traffic_class_to_socket(&self.socket, self.remote, traffic_class)
856 }
857
858 fn restore_open_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
859 #[cfg(test)]
860 self.test_hooks.last_restored_read_timeout.set(timeout);
861 #[cfg(test)]
862 if self.test_hooks.fail_open_timeout_restore.replace(false) {
863 return Err(io::Error::other(
864 "injected configured read timeout restoration failure",
865 ));
866 }
867 self.socket.set_read_timeout(timeout)
868 }
869
870 fn send_close_datagram(&self, packet: &[u8]) -> io::Result<usize> {
871 #[cfg(test)]
872 {
873 self.test_hooks
874 .close_send_attempts
875 .set(self.test_hooks.close_send_attempts.get() + 1);
876 if self.test_hooks.fail_close_send.replace(false) {
877 return Err(io::Error::other("injected close send failure"));
878 }
879 }
880 let bytes = self.socket.send(packet)?;
881 #[cfg(test)]
882 if let Some(reported) = self.test_hooks.close_reported_len.take() {
883 return Ok(reported);
884 }
885 Ok(bytes)
886 }
887}
888
889pub(crate) fn echo_sent_event(
890 remote: SocketAddr,
891 sent: ProbeSent,
892 send_call: Duration,
893 scheduled_at: Instant,
894 timer_error: Duration,
895) -> ClientEvent {
896 ClientEvent::EchoSent {
897 seq: sent.seq,
898 remote,
899 scheduled_at,
900 sent_at: sent.sent_at,
901 bytes: sent.bytes,
902 send_call,
903 timer_error,
904 }
905}
906
907pub(crate) fn validate_datagram_length(expected: usize, actual: usize) -> Result<(), ClientError> {
908 if actual == expected {
909 Ok(())
910 } else {
911 Err(ClientError::DatagramLengthMismatch { expected, actual })
912 }
913}
914
915#[cfg(test)]
916impl Client {
917 fn send_probe_at(
918 &mut self,
919 timestamps: ProbeSendTimestamps,
920 ) -> Result<Vec<ClientEvent>, ClientError> {
921 self.probe_send_timestamps = Some(timestamps);
922 self.send_probe()
923 }
924
925 fn send_managed_probe_at(
926 &mut self,
927 scheduled_at: Instant,
928 timestamps: ProbeSendTimestamps,
929 ) -> Result<Vec<ClientEvent>, ClientError> {
930 self.probe_send_timestamps = Some(timestamps);
931 self.send_managed_probe(scheduled_at)
932 }
933}
934
935#[cfg(test)]
936fn bounded_receive_timeout(
937 deadline: Option<Instant>,
938 configured_timeout: Option<Duration>,
939 max_wait: Duration,
940 now: Instant,
941) -> Option<Duration> {
942 if max_wait.is_zero() {
943 return None;
944 }
945 let mut timeout = configured_timeout.unwrap_or(max_wait).min(max_wait);
946 if let Some(deadline) = deadline {
947 let remaining = deadline.checked_duration_since(now)?;
948 if remaining.is_zero() {
949 return None;
950 }
951 timeout = timeout.min(remaining);
952 }
953 (!timeout.is_zero()).then_some(timeout)
954}
955
956#[cfg(test)]
957mod tests;