1use std::{
2 io,
3 net::{SocketAddr, UdpSocket},
4 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
5};
6
7use irtt_proto::{
8 close::CloseRequest, decode_echo_reply, echo_packet_len, encode_close_request,
9 encode_echo_request, encode_open_request, flags, EchoReply, EchoRequest, OpenReply,
10 OpenRequest, Params, ServerFill, TimestampFields, PROTOCOL_VERSION,
11};
12
13use crate::{
14 config::{
15 ClientConfig, RecvBudget, RunMode, MAX_DSCP_CODEPOINT, MAX_SERVER_FILL_BYTES,
16 MAX_UDP_PAYLOAD_LENGTH,
17 },
18 error::ClientError,
19 event::{
20 ClientEvent, OneWayDelaySample, OpenOutcome, ReceivedStatsSample, RttSample, ServerTiming,
21 SignedDuration, WarningKind,
22 },
23 metadata::ReceiveMeta,
24 probe::{CompletedSet, PendingMap, PendingProbe, TimedOutMap},
25 receive::recv_datagram,
26 session::{negotiate_params, ActiveSession, ClientPhase, NegotiatedParams},
27 socket::{connect_udp_socket, resolve_remote, validate_open_timeouts},
28 socket_options::{apply_dscp_to_socket, clear_dscp_on_socket},
29 timing::ClientTimestamp,
30};
31
32const MAX_OPEN_PACKET_SIZE: usize = 512;
33const MIN_RECV_BUFFER_SIZE: usize = 2048;
34
35#[derive(Debug)]
42pub struct Client {
43 config: ClientConfig,
44 socket: UdpSocket,
45 remote: SocketAddr,
46 requested: Params,
47 negotiated: Option<NegotiatedParams>,
48 phase: ClientPhase,
49 session: Option<ActiveSession>,
50 recv_buffer: Vec<u8>,
51}
52
53impl Client {
54 pub fn connect(config: ClientConfig) -> Result<Self, ClientError> {
60 validate_open_timeouts(&config.open_timeouts)?;
61 if config.max_pending_probes == 0 {
62 return Err(ClientError::InvalidConfig {
63 reason: "max_pending_probes must be greater than zero".to_owned(),
64 });
65 }
66 if config.probe_timeout == Duration::ZERO {
67 return Err(ClientError::InvalidConfig {
68 reason: "probe_timeout must be greater than zero".to_owned(),
69 });
70 }
71 let remote = resolve_remote(&config)?;
72 let socket = connect_udp_socket(&config.socket_config, remote)?;
73 let requested = params_from_config(&config)?;
74
75 Ok(Self {
76 config,
77 socket,
78 remote,
79 requested,
80 negotiated: None,
81 phase: ClientPhase::Connected,
82 session: None,
83 recv_buffer: vec![0_u8; MIN_RECV_BUFFER_SIZE],
84 })
85 }
86
87 pub fn open(&mut self) -> Result<OpenOutcome, ClientError> {
93 match self.phase {
94 ClientPhase::Connected => {}
95 ClientPhase::Open { .. } => return Err(ClientError::AlreadyOpen),
96 ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
97 ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
98 }
99
100 let outcome = (|| -> Result<OpenOutcome, ClientError> {
101 let request = OpenRequest {
102 params: self.requested.clone(),
103 close: self.config.run_mode == RunMode::NoTest,
104 };
105
106 let packet = encode_open_request(&request, self.config.hmac_key.as_deref())?;
107 let mut buf = [0_u8; MAX_OPEN_PACKET_SIZE];
108
109 for timeout in &self.config.open_timeouts {
110 self.socket.set_read_timeout(Some(*timeout))?;
111 self.socket.send(&packet)?;
112
113 match self.socket.recv(&mut buf) {
114 Ok(size) => {
115 let reply = irtt_proto::decode_open_reply(
116 &buf[..size],
117 self.config.hmac_key.as_deref(),
118 )?;
119
120 return self.accept_open_reply(reply, ClientTimestamp::now());
121 }
122 Err(err)
123 if matches!(
124 err.kind(),
125 io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
126 ) => {}
127 Err(err) => return Err(ClientError::Socket(err)),
128 }
129 }
130
131 Err(ClientError::OpenTimeout)
132 })();
133
134 let restore = self
135 .socket
136 .set_read_timeout(self.config.socket_config.recv_timeout);
137
138 match (outcome, restore) {
139 (Ok(outcome), Ok(())) => Ok(outcome),
140 (Ok(_), Err(source)) => Err(ClientError::ReadTimeoutRestore { source }),
141 (Err(err), Ok(())) => Err(err),
142 (Err(err), Err(_)) => Err(err),
143 }
144 }
145
146 pub fn close(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
151 let token = match self.phase {
152 ClientPhase::Open { token } => token,
153 ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
154 ClientPhase::Connected | ClientPhase::NoTestCompleted => {
155 return Err(ClientError::NotOpen)
156 }
157 };
158
159 clear_dscp_on_socket(&self.socket, self.remote)?;
160 let packet =
161 encode_close_request(&CloseRequest { token }, self.config.hmac_key.as_deref())?;
162 self.socket.send(&packet)?;
163 self.phase = ClientPhase::Closed;
164 if let Some(session) = self.session.as_mut() {
165 session.timed_out.clear();
166 }
167 self.session = None;
168
169 Ok(vec![ClientEvent::SessionClosed {
170 remote: self.remote,
171 token,
172 at: ClientTimestamp::now(),
173 }])
174 }
175
176 pub fn next_send_deadline(&self) -> Option<Instant> {
179 let session = self.session.as_ref()?;
180 if session.sending_done {
181 return None;
182 }
183 Some(session.next_send_at)
184 }
185
186 pub fn probe_timeout(&self) -> Duration {
188 self.config.probe_timeout
189 }
190
191 pub fn send_probe(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
197 self.send_probe_inner(None)
198 }
199
200 pub fn recv_once(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
205 self.recv_once_inner(None)
206 }
207
208 fn send_probe_inner(
209 &mut self,
210 override_ts: Option<ClientTimestamp>,
211 ) -> Result<Vec<ClientEvent>, ClientError> {
212 let token = match self.phase {
213 ClientPhase::Open { token } => token,
214 ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
215 ClientPhase::Connected => return Err(ClientError::NotOpen),
216 ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
217 };
218
219 let session = self
220 .session
221 .as_mut()
222 .expect("session must exist when phase is Open");
223
224 if session.sending_done {
225 return Ok(vec![]);
226 }
227
228 let now = override_ts.unwrap_or_else(ClientTimestamp::now);
229
230 if let Some(end) = session.end_mono {
231 if now.mono >= end {
232 session.sending_done = true;
233 return Ok(vec![]);
234 }
235 }
236
237 session.pending.check_capacity()?;
238
239 let negotiated = self
240 .negotiated
241 .as_ref()
242 .expect("negotiated must exist when Open");
243
244 let wire_seq = session.next_wire_seq;
245 let scheduled_at = session.next_send_at;
246
247 let request = EchoRequest {
248 token,
249 sequence: wire_seq,
250 params: negotiated.params.clone(),
251 payload: vec![],
252 };
253 let packet = encode_echo_request(&request, self.config.hmac_key.as_deref())?;
254 let sent_at = override_ts.unwrap_or_else(ClientTimestamp::now);
255 let send_call_start = Instant::now();
256 let bytes = self.socket.send(&packet)?;
257 let send_call = send_call_start.elapsed();
258 let timer_error = instant_abs_diff(sent_at.mono, scheduled_at);
259
260 let session = self
261 .session
262 .as_mut()
263 .expect("session must exist when phase is Open");
264
265 let pending = PendingProbe {
266 wire_seq,
267 sent_at,
268 timeout_at: sent_at
269 .mono
270 .checked_add(self.config.probe_timeout)
271 .ok_or(ClientError::DurationOverflow)?,
272 };
273 session.pending.insert(pending)?;
274
275 session.next_wire_seq = session.next_wire_seq.wrapping_add(1);
276 session.packets_sent =
277 session
278 .packets_sent
279 .checked_add(1)
280 .ok_or(ClientError::CounterOverflow {
281 counter: "packets_sent",
282 })?;
283
284 let negotiated = self
285 .negotiated
286 .as_ref()
287 .expect("negotiated must exist when Open");
288 let interval_ns = u64::try_from(negotiated.params.interval_ns)
289 .expect("validated positive negotiated interval");
290 let session = self
291 .session
292 .as_mut()
293 .expect("session must exist when phase is Open");
294 session.next_send_at =
295 next_probe_deadline(session.start_mono, interval_ns, session.packets_sent)?;
296
297 if let Some(end) = session.end_mono {
298 if session.next_send_at >= end {
299 session.sending_done = true;
300 }
301 }
302
303 Ok(vec![ClientEvent::EchoSent {
304 seq: wire_seq,
305 remote: self.remote,
306 scheduled_at,
307 sent_at,
308 bytes,
309 send_call,
310 timer_error,
311 }])
312 }
313
314 fn recv_once_inner(
315 &mut self,
316 override_ts: Option<ClientTimestamp>,
317 ) -> Result<Vec<ClientEvent>, ClientError> {
318 match self.phase {
319 ClientPhase::Open { .. } => {}
320 ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
321 ClientPhase::Connected => return Err(ClientError::NotOpen),
322 ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
323 }
324
325 let datagram = match recv_datagram(&self.socket, &mut self.recv_buffer) {
326 Ok(datagram) => datagram,
327 Err(err)
328 if matches!(
329 err.kind(),
330 io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
331 ) =>
332 {
333 return Ok(vec![]);
334 }
335 Err(err) => return Err(ClientError::Socket(err)),
336 };
337
338 let now = override_ts.unwrap_or(datagram.received_at);
339 let packet_len = datagram.len;
340 let Some(reply) = self.decode_received_packet(&self.recv_buffer[..packet_len]) else {
341 return Ok(vec![ClientEvent::Warning {
342 kind: WarningKind::MalformedOrUnrelatedPacket,
343 message: "dropped malformed or unrelated packet".to_owned(),
344 at: now,
345 }]);
346 };
347 self.process_echo_reply(reply, packet_len, now, datagram.meta)
348 }
349
350 pub fn recv_available(&mut self, budget: RecvBudget) -> Result<Vec<ClientEvent>, ClientError> {
353 let mut all_events = Vec::new();
354 for _ in 0..budget.max_packets {
355 let events = self.recv_once()?;
356 if events.is_empty() {
357 break;
358 }
359 all_events.extend(events);
360 }
361 Ok(all_events)
362 }
363
364 pub fn poll_timeouts(&mut self) -> Result<Vec<ClientEvent>, ClientError> {
366 self.poll_timeouts_at(Instant::now())
367 }
368
369 pub fn poll_timeouts_at(&mut self, now: Instant) -> Result<Vec<ClientEvent>, ClientError> {
377 match self.phase {
378 ClientPhase::Open { .. } => {}
379 ClientPhase::Closed => return Err(ClientError::AlreadyClosed),
380 ClientPhase::Connected => return Err(ClientError::NotOpen),
381 ClientPhase::NoTestCompleted => return Err(ClientError::AlreadyCompleted),
382 }
383
384 let session = self
385 .session
386 .as_mut()
387 .expect("session must exist when phase is Open");
388
389 let expired = session.pending.drain_expired(now);
390 let mut events = Vec::with_capacity(expired.len());
391 for probe in expired {
392 events.push(ClientEvent::EchoLoss {
393 seq: probe.wire_seq,
394 sent_at: probe.sent_at,
395 timeout_at: probe.timeout_at,
396 });
397 session.timed_out.insert(probe);
398 }
399
400 Ok(events)
401 }
402
403 pub fn is_run_complete(&self) -> bool {
409 let Some(session) = self.session.as_ref() else {
410 return matches!(
411 self.phase,
412 ClientPhase::Closed | ClientPhase::NoTestCompleted
413 );
414 };
415 session.sending_done && session.pending.len() == 0
416 }
417
418 pub(crate) fn has_timed_out_metadata(&self) -> bool {
419 self.session
420 .as_ref()
421 .is_some_and(|session| session.timed_out.len() > 0)
422 }
423
424 pub(crate) fn packets_sent(&self) -> u64 {
425 self.session
426 .as_ref()
427 .map_or(0, |session| session.packets_sent)
428 }
429
430 fn decode_received_packet(&self, packet: &[u8]) -> Option<EchoReply> {
431 let negotiated = self
432 .negotiated
433 .as_ref()
434 .expect("negotiated must exist when Open");
435
436 decode_echo_reply(packet, &negotiated.params, self.config.hmac_key.as_deref()).ok()
437 }
438
439 fn process_echo_reply(
440 &mut self,
441 reply: EchoReply,
442 packet_len: usize,
443 now: ClientTimestamp,
444 meta: ReceiveMeta,
445 ) -> Result<Vec<ClientEvent>, ClientError> {
446 let token = match self.phase {
447 ClientPhase::Open { token } => token,
448 _ => unreachable!(),
449 };
450 if reply.token != token {
451 return Ok(vec![ClientEvent::Warning {
452 kind: WarningKind::WrongToken,
453 message: format!(
454 "dropped reply with wrong token: expected {token:#x}, got {:#x}",
455 reply.token
456 ),
457 at: now,
458 }]);
459 }
460
461 let session = self.session.as_mut().expect("session must exist when Open");
462
463 let wire_seq = reply.sequence;
464
465 if let Some(pending) = session.pending.remove(wire_seq) {
466 let rtt = compute_rtt(&pending.sent_at, &now, &reply.timestamps);
467 let server_timing = build_server_timing(&reply.timestamps);
468 let one_way = compute_one_way(&pending.sent_at, &now, &reply.timestamps);
469 let received_stats = build_received_stats(&reply);
470 let is_late = session
471 .highest_received_seq
472 .is_some_and(|h| sequence_is_before(wire_seq, h));
473 let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
474
475 update_highest_received(&mut session.highest_received_seq, wire_seq);
476 session.completed.insert(wire_seq);
477
478 let mut events = Vec::new();
479 if is_late {
480 events.push(ClientEvent::LateReply {
481 seq: wire_seq,
482 highest_seen,
483 remote: self.remote,
484 sent_at: Some(pending.sent_at),
485 received_at: now,
486 rtt: Some(rtt),
487 server_timing,
488 one_way,
489 received_stats,
490 bytes: packet_len,
491 packet_meta: meta.into(),
492 });
493 } else {
494 events.push(ClientEvent::EchoReply {
495 seq: wire_seq,
496 remote: self.remote,
497 sent_at: pending.sent_at,
498 received_at: now,
499 rtt,
500 server_timing,
501 one_way,
502 received_stats,
503 bytes: packet_len,
504 packet_meta: meta.into(),
505 });
506 }
507 if flags::has(reply.flags, flags::FLAG_CLOSE) {
508 self.close_from_peer(token, now, &mut events)?;
509 }
510 Ok(events)
511 } else if session.completed.contains(wire_seq) {
512 update_highest_received(&mut session.highest_received_seq, wire_seq);
513 Ok(vec![ClientEvent::DuplicateReply {
514 seq: wire_seq,
515 remote: self.remote,
516 received_at: now,
517 bytes: packet_len,
518 }])
519 } else if let Some(timed_out) = session.timed_out.remove(wire_seq) {
520 let rtt = compute_rtt(&timed_out.sent_at, &now, &reply.timestamps);
521 let server_timing = build_server_timing(&reply.timestamps);
522 let one_way = compute_one_way(&timed_out.sent_at, &now, &reply.timestamps);
523 let received_stats = build_received_stats(&reply);
524 let highest_seen = session.highest_received_seq.unwrap_or(wire_seq);
525 update_highest_received(&mut session.highest_received_seq, wire_seq);
526 session.completed.insert(wire_seq);
527
528 let mut events = vec![ClientEvent::LateReply {
529 seq: wire_seq,
530 highest_seen,
531 remote: self.remote,
532 sent_at: Some(timed_out.sent_at),
533 received_at: now,
534 rtt: Some(rtt),
535 server_timing,
536 one_way,
537 received_stats,
538 bytes: packet_len,
539 packet_meta: meta.into(),
540 }];
541 if flags::has(reply.flags, flags::FLAG_CLOSE) {
542 self.close_from_peer(token, now, &mut events)?;
543 }
544 Ok(events)
545 } else if session
546 .highest_received_seq
547 .is_some_and(|h| sequence_is_before(wire_seq, h))
548 {
549 Ok(vec![ClientEvent::LateReply {
550 seq: wire_seq,
551 highest_seen: session.highest_received_seq.unwrap(),
552 remote: self.remote,
553 sent_at: None,
554 received_at: now,
555 rtt: None,
556 server_timing: build_server_timing(&reply.timestamps),
557 one_way: None,
558 received_stats: build_received_stats(&reply),
559 bytes: packet_len,
560 packet_meta: meta.into(),
561 }])
562 } else {
563 Ok(vec![ClientEvent::Warning {
564 kind: WarningKind::UntrackedReply,
565 message: format!(
566 "dropped reply with untracked seq {wire_seq} (no pending or completed entry)"
567 ),
568 at: now,
569 }])
570 }
571 }
572
573 fn close_from_peer(
574 &mut self,
575 token: u64,
576 now: ClientTimestamp,
577 events: &mut Vec<ClientEvent>,
578 ) -> Result<(), ClientError> {
579 clear_dscp_on_socket(&self.socket, self.remote)?;
580 self.phase = ClientPhase::Closed;
581 if let Some(session) = self.session.as_mut() {
582 session.timed_out.clear();
583 }
584 self.session = None;
585 events.push(ClientEvent::SessionClosed {
586 remote: self.remote,
587 token,
588 at: now,
589 });
590 Ok(())
591 }
592
593 fn accept_open_reply(
594 &mut self,
595 reply: OpenReply,
596 now: ClientTimestamp,
597 ) -> Result<OpenOutcome, ClientError> {
598 if reply.params.protocol_version != PROTOCOL_VERSION {
599 return Err(ClientError::ProtocolVersionMismatch {
600 requested: PROTOCOL_VERSION,
601 received: reply.params.protocol_version,
602 });
603 }
604
605 let reply_is_close = flags::has(reply.flags, flags::FLAG_CLOSE);
606 match self.config.run_mode {
607 RunMode::Normal if reply_is_close => Err(ClientError::ServerRejected),
608 RunMode::Normal if reply.token == 0 => Err(ClientError::ZeroToken),
609 RunMode::Normal => self.accept_normal_open(reply, now),
610 RunMode::NoTest if !reply_is_close => Err(ClientError::UnexpectedNoTestReply),
611 RunMode::NoTest if reply.token != 0 => {
612 Err(ClientError::NonZeroNoTestToken { token: reply.token })
613 }
614 RunMode::NoTest => self.accept_no_test_open(reply, now),
615 }
616 }
617
618 fn accept_normal_open(
619 &mut self,
620 reply: OpenReply,
621 now: ClientTimestamp,
622 ) -> Result<OpenOutcome, ClientError> {
623 let token = reply.token;
624 let negotiated = negotiate_params(
625 &self.requested,
626 reply.params,
627 self.config.negotiation_policy,
628 )?;
629 let recv_buffer_size = recv_buffer_size(self.config.hmac_key.is_some(), Some(&negotiated))?;
630 let negotiated_dscp =
631 u8::try_from(negotiated.params.dscp).map_err(|_| ClientError::InvalidConfig {
632 reason: "negotiated dscp must be in range 0..=63".to_owned(),
633 })?;
634 apply_dscp_to_socket(&self.socket, self.remote, negotiated_dscp)?;
635 self.negotiated = Some(negotiated.clone());
636 self.recv_buffer.resize(recv_buffer_size, 0);
637 self.phase = ClientPhase::Open { token };
638
639 let end_mono = if negotiated.params.duration_ns > 0 {
640 Some(negotiated_end_mono(
641 now.mono,
642 negotiated.params.duration_ns,
643 )?)
644 } else {
645 None
646 };
647
648 self.session = Some(ActiveSession {
649 next_wire_seq: 0,
650 highest_received_seq: None,
651 packets_sent: 0,
652 start_mono: now.mono,
653 end_mono,
654 next_send_at: now.mono,
655 pending: PendingMap::new(self.config.max_pending_probes),
656 timed_out: TimedOutMap::new(self.config.max_pending_probes),
657 completed: CompletedSet::new(self.config.max_pending_probes),
658 sending_done: false,
659 });
660
661 let event = ClientEvent::SessionStarted {
662 remote: self.remote,
663 token,
664 negotiated: negotiated.clone(),
665 at: now,
666 };
667
668 Ok(OpenOutcome::Started {
669 remote: self.remote,
670 token,
671 negotiated,
672 event,
673 })
674 }
675
676 fn accept_no_test_open(
677 &mut self,
678 reply: OpenReply,
679 now: ClientTimestamp,
680 ) -> Result<OpenOutcome, ClientError> {
681 let negotiated = negotiate_params(
682 &self.requested,
683 reply.params,
684 self.config.negotiation_policy,
685 )?;
686 self.negotiated = Some(negotiated.clone());
687 self.phase = ClientPhase::NoTestCompleted;
688 let event = ClientEvent::NoTestCompleted {
689 remote: self.remote,
690 negotiated: negotiated.clone(),
691 at: now,
692 };
693 Ok(OpenOutcome::NoTestCompleted {
694 remote: self.remote,
695 negotiated,
696 event,
697 })
698 }
699}
700
701#[cfg(test)]
702impl Client {
703 fn send_probe_at(&mut self, ts: ClientTimestamp) -> Result<Vec<ClientEvent>, ClientError> {
704 self.send_probe_inner(Some(ts))
705 }
706}
707
708fn update_highest_received(highest_received_seq: &mut Option<u32>, wire_seq: u32) {
709 *highest_received_seq = Some(highest_received_seq.map_or(wire_seq, |h| {
710 if sequence_is_after(wire_seq, h) {
711 wire_seq
712 } else {
713 h
714 }
715 }));
716}
717
718fn next_probe_deadline(
719 start: Instant,
720 interval_ns: u64,
721 packets_sent: u64,
722) -> Result<Instant, ClientError> {
723 let offset_ns = interval_ns
724 .checked_mul(packets_sent)
725 .ok_or(ClientError::DurationOverflow)?;
726 start
727 .checked_add(Duration::from_nanos(offset_ns))
728 .ok_or(ClientError::DurationOverflow)
729}
730
731fn sequence_is_after(candidate: u32, current: u32) -> bool {
732 candidate != current && candidate.wrapping_sub(current) < (1 << 31)
733}
734
735fn sequence_is_before(candidate: u32, current: u32) -> bool {
736 current != candidate && current.wrapping_sub(candidate) < (1 << 31)
737}
738
739fn instant_abs_diff(left: Instant, right: Instant) -> Duration {
740 left.checked_duration_since(right)
741 .or_else(|| right.checked_duration_since(left))
742 .unwrap_or(Duration::ZERO)
743}
744
745pub(crate) fn recv_buffer_size(
746 has_hmac: bool,
747 negotiated: Option<&NegotiatedParams>,
748) -> Result<usize, ClientError> {
749 Ok(match negotiated {
750 Some(negotiated) => echo_packet_len(has_hmac, &negotiated.params)?
751 .saturating_add(1)
752 .max(MIN_RECV_BUFFER_SIZE),
753 None => MIN_RECV_BUFFER_SIZE,
754 })
755}
756
757fn compute_rtt(
758 sent_at: &ClientTimestamp,
759 received_at: &ClientTimestamp,
760 ts: &TimestampFields,
761) -> RttSample {
762 let raw = received_at
763 .mono
764 .checked_duration_since(sent_at.mono)
765 .unwrap_or(Duration::ZERO);
766
767 let server_processing = compute_server_processing(ts);
768
769 let adjusted = server_processing
770 .map(|sp| SignedDuration::from_nanos(duration_ns_i128(raw) - duration_ns_i128(sp)));
771 let effective = adjusted.unwrap_or_else(|| SignedDuration::from_duration(raw));
772
773 RttSample {
774 raw,
775 adjusted,
776 effective,
777 }
778}
779
780fn duration_ns_i128(duration: Duration) -> i128 {
781 i128::try_from(duration.as_nanos()).unwrap_or(i128::MAX)
782}
783
784fn compute_server_processing(ts: &TimestampFields) -> Option<Duration> {
785 if let (Some(recv_mono), Some(send_mono)) = (ts.recv_mono, ts.send_mono) {
786 let diff = send_mono.checked_sub(recv_mono)?;
787 return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
788 }
789 if let (Some(recv_wall), Some(send_wall)) = (ts.recv_wall, ts.send_wall) {
790 let diff = send_wall.checked_sub(recv_wall)?;
791 return Some(Duration::from_nanos(u64::try_from(diff).ok()?));
792 }
793 None
794}
795
796fn build_server_timing(ts: &TimestampFields) -> Option<ServerTiming> {
797 if ts.recv_wall.is_none()
798 && ts.recv_mono.is_none()
799 && ts.send_wall.is_none()
800 && ts.send_mono.is_none()
801 && ts.midpoint_wall.is_none()
802 && ts.midpoint_mono.is_none()
803 {
804 return None;
805 }
806 Some(ServerTiming {
807 receive_wall_ns: ts.recv_wall,
808 receive_mono_ns: ts.recv_mono,
809 send_wall_ns: ts.send_wall,
810 send_mono_ns: ts.send_mono,
811 midpoint_wall_ns: ts.midpoint_wall,
812 midpoint_mono_ns: ts.midpoint_mono,
813 processing: compute_server_processing(ts),
814 })
815}
816
817fn compute_one_way(
818 sent_at: &ClientTimestamp,
819 received_at: &ClientTimestamp,
820 ts: &TimestampFields,
821) -> Option<OneWayDelaySample> {
822 let server_recv_wall = ts.recv_wall.or(ts.midpoint_wall);
823 let server_send_wall = ts.send_wall.or(ts.midpoint_wall);
824
825 let client_send_ns = unix_epoch_ns_i64(sent_at.wall);
826 let client_recv_ns = unix_epoch_ns_i64(received_at.wall);
827
828 let c2s = server_recv_wall
829 .zip(client_send_ns)
830 .and_then(|(srv, cli)| srv.checked_sub(cli))
831 .map(|d| SignedDuration::from_nanos(i128::from(d)));
832 let s2c = client_recv_ns
833 .zip(server_send_wall)
834 .and_then(|(cli, srv)| cli.checked_sub(srv))
835 .map(|d| SignedDuration::from_nanos(i128::from(d)));
836
837 if c2s.is_none() && s2c.is_none() {
838 return None;
839 }
840
841 Some(OneWayDelaySample {
842 client_to_server: c2s,
843 server_to_client: s2c,
844 })
845}
846
847fn unix_epoch_ns_i64(time: SystemTime) -> Option<i64> {
848 time.duration_since(UNIX_EPOCH)
849 .ok()
850 .and_then(|duration| i64::try_from(duration.as_nanos()).ok())
851}
852
853fn build_received_stats(reply: &EchoReply) -> Option<ReceivedStatsSample> {
854 if reply.recv_count.is_none() && reply.recv_window.is_none() {
855 return None;
856 }
857 Some(ReceivedStatsSample {
858 count: reply.recv_count,
859 window: reply.recv_window,
860 })
861}
862
863fn params_from_config(config: &ClientConfig) -> Result<Params, ClientError> {
864 validate_protocol_config(config)?;
865 Ok(Params {
866 protocol_version: PROTOCOL_VERSION,
867 duration_ns: match config.duration {
868 Some(duration) => config_duration_to_ns("duration", duration)?,
869 None => 0,
870 },
871 interval_ns: config_duration_to_ns("interval", config.interval)?,
872 length: i64::from(config.length),
873 received_stats: config.received_stats,
874 stamp_at: config.stamp_at,
875 clock: config.clock,
876 dscp: i64::from(config.dscp),
877 server_fill: config.server_fill.clone().map(|value| ServerFill { value }),
878 })
879}
880
881fn validate_protocol_config(config: &ClientConfig) -> Result<(), ClientError> {
882 if config.duration == Some(Duration::ZERO) {
883 return Err(ClientError::InvalidConfig {
884 reason: "duration must be greater than zero; use None for continuous mode".to_owned(),
885 });
886 }
887 if config.interval == Duration::ZERO {
888 return Err(ClientError::InvalidConfig {
889 reason: "interval must be greater than zero".to_owned(),
890 });
891 }
892 if config.dscp > MAX_DSCP_CODEPOINT {
893 return Err(ClientError::InvalidConfig {
894 reason: format!("dscp must be <= {MAX_DSCP_CODEPOINT}"),
895 });
896 }
897 if config.length > MAX_UDP_PAYLOAD_LENGTH {
898 return Err(ClientError::InvalidConfig {
899 reason: format!("packet length must be <= {MAX_UDP_PAYLOAD_LENGTH}"),
900 });
901 }
902
903 if let Some(fill) = &config.server_fill {
904 let len = fill.len();
905 if len == 0 {
906 return Err(ClientError::InvalidConfig {
907 reason: "server_fill must not be empty".to_owned(),
908 });
909 }
910 if len > MAX_SERVER_FILL_BYTES {
911 return Err(ClientError::InvalidConfig {
912 reason: format!("server_fill must be <= {MAX_SERVER_FILL_BYTES} bytes, got {len}"),
913 });
914 }
915 }
916
917 Ok(())
918}
919
920fn config_duration_to_ns(field: &str, duration: Duration) -> Result<i64, ClientError> {
921 i64::try_from(duration.as_nanos()).map_err(|_| ClientError::InvalidConfig {
922 reason: format!("{field} is too large to encode as nanoseconds"),
923 })
924}
925
926fn negotiated_end_mono(start: Instant, duration_ns: i64) -> Result<Instant, ClientError> {
927 debug_assert!(duration_ns > 0);
928 let duration_ns = u64::try_from(duration_ns).expect("validated positive negotiated duration");
929 start
930 .checked_add(Duration::from_nanos(duration_ns))
931 .ok_or_else(|| ClientError::NegotiationRejected {
932 reason: "duration is too large to schedule".to_owned(),
933 })
934}
935
936#[cfg(test)]
937mod tests;