1use std::{cmp, net::SocketAddr};
2
3use identity_hash::IntMap;
4use thiserror::Error;
5use tracing::{debug, trace};
6
7use super::{
8 PathStats, SpaceKind,
9 mtud::MtuDiscovery,
10 pacing::Pacer,
11 spaces::{PacketNumberSpace, SentPacket},
12};
13use crate::{
14 ConnectionId, Duration, FourTuple, Instant, TIMER_GRANULARITY, TransportConfig,
15 TransportErrorCode, VarInt,
16 coding::{self, Decodable, Encodable},
17 congestion,
18 frame::ObservedAddr,
19};
20
21#[cfg(feature = "qlog")]
22use qlog::events::quic::RecoveryMetricsUpdated;
23
24#[cfg_attr(test, derive(test_strategy::Arbitrary))]
26#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
27pub struct PathId(pub(crate) u32);
28
29impl std::hash::Hash for PathId {
30 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
31 state.write_u32(self.0);
32 }
33}
34
35impl identity_hash::IdentityHashable for PathId {}
36
37impl Decodable for PathId {
38 fn decode<B: bytes::Buf>(r: &mut B) -> coding::Result<Self> {
39 let v = VarInt::decode(r)?;
40 let v = u32::try_from(v.0).map_err(|_| coding::UnexpectedEnd)?;
41 Ok(Self(v))
42 }
43}
44
45impl Encodable for PathId {
46 fn encode<B: bytes::BufMut>(&self, w: &mut B) {
47 VarInt(self.0.into()).encode(w)
48 }
49}
50
51impl PathId {
52 pub const MAX: Self = Self(u32::MAX);
54
55 pub const ZERO: Self = Self(0);
57
58 pub(crate) const fn size(&self) -> usize {
60 VarInt(self.0 as u64).size()
61 }
62
63 pub fn saturating_add(self, rhs: impl Into<Self>) -> Self {
66 let rhs = rhs.into();
67 let inner = self.0.saturating_add(rhs.0);
68 Self(inner)
69 }
70
71 pub fn saturating_sub(self, rhs: impl Into<Self>) -> Self {
74 let rhs = rhs.into();
75 let inner = self.0.saturating_sub(rhs.0);
76 Self(inner)
77 }
78
79 pub(crate) fn next(&self) -> Self {
81 self.saturating_add(Self(1))
82 }
83
84 pub(crate) fn as_u32(&self) -> u32 {
86 self.0
87 }
88}
89
90impl std::fmt::Display for PathId {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 self.0.fmt(f)
93 }
94}
95
96impl<T: Into<u32>> From<T> for PathId {
97 fn from(source: T) -> Self {
98 Self(source.into())
99 }
100}
101
102#[derive(Debug)]
109pub(super) struct PathState {
110 pub(super) data: PathData,
111 pub(super) prev: Option<(ConnectionId, PathData)>,
112}
113
114impl PathState {
115 pub(super) fn remove_in_flight(&mut self, packet: &SentPacket) {
117 for path_data in [&mut self.data]
119 .into_iter()
120 .chain(self.prev.as_mut().map(|(_, data)| data))
121 {
122 if path_data.remove_in_flight(packet) {
123 return;
124 }
125 }
126 }
127}
128
129#[derive(Debug)]
130pub(super) struct SentChallengeInfo {
131 pub(super) sent_instant: Instant,
133 pub(super) network_path: FourTuple,
135}
136
137#[derive(Debug)]
139pub(super) struct PathData {
140 pub(super) network_path: FourTuple,
141 pub(super) rtt: RttEstimator,
142 pub(super) sending_ecn: bool,
144 pub(super) congestion: Box<dyn congestion::Controller>,
146 pub(super) pacing: Pacer,
148 on_path_challenges_unconfirmed: IntMap<u64, SentChallengeInfo>,
150 off_path_challenges_unconfirmed: IntMap<u64, SentChallengeInfo>,
152 pub(super) pending_on_path_challenge: bool,
160 pub(super) path_responses: PathResponses,
162 pub(super) validated: bool,
167 pub(super) total_sent: u64,
169 pub(super) total_recvd: u64,
171 pub(super) mtud: MtuDiscovery,
173 pub(super) first_packet_after_rtt_sample: Option<(SpaceKind, u64)>,
177 pub(super) in_flight: InFlight,
181 pub(super) observed_addr_sent: bool,
184 pub(super) last_observed_addr_report: Option<ObservedAddr>,
186 pub(super) status: PathStatusState,
188 first_packet: Option<u64>,
195 pub(super) pto_count: u32,
197
198 pub(super) idle_timeout: Option<Duration>,
206 pub(super) keep_alive: Option<Duration>,
214
215 pub(super) open_status: OpenStatus,
226
227 pub(super) draining: bool,
234
235 #[cfg(feature = "qlog")]
237 recovery_metrics: RecoveryMetrics,
238
239 generation: u64,
241}
242
243impl PathData {
244 pub(super) fn new(
245 network_path: FourTuple,
246 allow_mtud: bool,
247 peer_max_udp_payload_size: Option<u16>,
248 generation: u64,
249 now: Instant,
250 config: &TransportConfig,
251 ) -> Self {
252 let congestion = config
253 .congestion_controller_factory
254 .clone()
255 .build(now, config.get_initial_mtu());
256 Self {
257 network_path,
258 rtt: RttEstimator::new(config.initial_rtt),
259 sending_ecn: true,
260 pacing: Pacer::new(
261 config.initial_rtt,
262 congestion.initial_window(),
263 config.get_initial_mtu(),
264 now,
265 ),
266 congestion,
267 on_path_challenges_unconfirmed: Default::default(),
268 off_path_challenges_unconfirmed: Default::default(),
269 pending_on_path_challenge: false,
270 path_responses: PathResponses::default(),
271 validated: false,
272 total_sent: 0,
273 total_recvd: 0,
274 mtud: config
275 .mtu_discovery_config
276 .as_ref()
277 .filter(|_| allow_mtud)
278 .map_or_else(
279 || MtuDiscovery::disabled(config.get_initial_mtu(), config.min_mtu),
280 |mtud_config| {
281 MtuDiscovery::new(
282 config.get_initial_mtu(),
283 config.min_mtu,
284 peer_max_udp_payload_size,
285 mtud_config.clone(),
286 )
287 },
288 ),
289 first_packet_after_rtt_sample: None,
290 in_flight: InFlight::new(),
291 observed_addr_sent: false,
292 last_observed_addr_report: None,
293 status: Default::default(),
294 first_packet: None,
295 pto_count: 0,
296 idle_timeout: config.default_path_max_idle_timeout,
297 keep_alive: config.default_path_keep_alive_interval,
298 open_status: OpenStatus::default(),
299 draining: false,
300 #[cfg(feature = "qlog")]
301 recovery_metrics: RecoveryMetrics::default(),
302 generation,
303 }
304 }
305
306 pub(super) fn from_previous(
310 network_path: FourTuple,
311 prev: &Self,
312 generation: u64,
313 now: Instant,
314 ) -> Self {
315 let congestion = prev.congestion.clone_box();
316 let smoothed_rtt = prev.rtt.get();
317 Self {
318 network_path,
319 rtt: prev.rtt,
320 pacing: Pacer::new(smoothed_rtt, congestion.window(), prev.current_mtu(), now),
321 sending_ecn: true,
322 congestion,
323 on_path_challenges_unconfirmed: Default::default(),
324 off_path_challenges_unconfirmed: Default::default(),
325 pending_on_path_challenge: false,
326 path_responses: PathResponses::default(),
327 validated: false,
328 total_sent: 0,
329 total_recvd: 0,
330 mtud: prev.mtud.clone(),
331 first_packet_after_rtt_sample: prev.first_packet_after_rtt_sample,
332 in_flight: InFlight::new(),
333 observed_addr_sent: false,
334 last_observed_addr_report: None,
335 status: prev.status.clone(),
336 first_packet: None,
337 pto_count: 0,
338 idle_timeout: prev.idle_timeout,
339 keep_alive: prev.keep_alive,
340 open_status: OpenStatus::default(),
341 draining: false,
342 #[cfg(feature = "qlog")]
343 recovery_metrics: prev.recovery_metrics.clone(),
344 generation,
345 }
346 }
347
348 pub(super) fn is_validating_path(&self) -> bool {
350 !self.on_path_challenges_unconfirmed.is_empty() || self.pending_on_path_challenge
351 }
352
353 pub(super) fn anti_amplification_blocked(&self, bytes_to_send: u64) -> bool {
356 !self.validated && self.total_recvd * 3 < self.total_sent + bytes_to_send
357 }
358
359 pub(super) fn current_mtu(&self) -> u16 {
361 self.mtud.current_mtu()
362 }
363
364 pub(super) fn sent(&mut self, pn: u64, packet: SentPacket, space: &mut PacketNumberSpace) {
366 self.in_flight.insert(&packet);
367 if self.first_packet.is_none() {
368 self.first_packet = Some(pn);
369 }
370 if let Some(forgotten) = space.sent(pn, packet) {
371 self.remove_in_flight(&forgotten);
372 }
373 }
374
375 pub(super) fn record_path_challenge_sent(
376 &mut self,
377 now: Instant,
378 token: u64,
379 network_path: FourTuple,
380 ) {
381 let info = SentChallengeInfo {
382 sent_instant: now,
383 network_path,
384 };
385 if network_path == self.network_path {
386 self.on_path_challenges_unconfirmed.insert(token, info);
387 } else {
388 self.off_path_challenges_unconfirmed.insert(token, info);
389 }
390 }
391
392 pub(super) fn remove_in_flight(&mut self, packet: &SentPacket) -> bool {
395 if packet.path_generation != self.generation {
396 return false;
397 }
398 self.in_flight.remove(packet);
399 true
400 }
401
402 pub(super) fn inc_total_sent(&mut self, inc: u64) {
404 self.total_sent = self.total_sent.saturating_add(inc);
405 if !self.validated {
406 trace!(
407 network_path = %self.network_path,
408 anti_amplification_budget = %(self.total_recvd * 3).saturating_sub(self.total_sent),
409 "anti amplification budget decreased"
410 );
411 }
412 }
413
414 pub(super) fn inc_total_recvd(&mut self, inc: u64) {
416 self.total_recvd = self.total_recvd.saturating_add(inc);
417 if !self.validated {
418 trace!(
419 network_path = %self.network_path,
420 anti_amplification_budget = %(self.total_recvd * 3).saturating_sub(self.total_sent),
421 "anti amplification budget increased"
422 );
423 }
424 }
425
426 pub(super) fn earliest_on_path_expiring_challenge(&self) -> Option<Instant> {
428 if self.on_path_challenges_unconfirmed.is_empty() {
429 return None;
430 }
431 let pto = self.rtt.pto_base();
432 self.on_path_challenges_unconfirmed
433 .values()
434 .map(|info| info.sent_instant + pto)
435 .min()
436 }
437
438 pub(super) fn on_path_response_received(
440 &mut self,
441 now: Instant,
442 token: u64,
443 network_path: FourTuple,
444 ) -> OnPathResponseReceived {
445 match self.on_path_challenges_unconfirmed.remove(&token) {
458 Some(info) if info.network_path.is_probably_same_path(&self.network_path) => {
462 self.network_path.update_local_if_same_remote(&network_path);
463 let sent_instant = info.sent_instant;
464 if !std::mem::replace(&mut self.validated, true) {
465 trace!("new path validated");
466 }
467 self.on_path_challenges_unconfirmed.clear();
469
470 self.pending_on_path_challenge = false;
471
472 let rtt = now.saturating_duration_since(sent_instant);
475 self.rtt.reset_initial_rtt(rtt);
476
477 let prev_status = std::mem::replace(&mut self.open_status, OpenStatus::Informed);
478 OnPathResponseReceived::OnPath {
479 was_open: prev_status == OpenStatus::Informed,
480 }
481 }
482 Some(info) => {
484 self.on_path_challenges_unconfirmed
488 .retain(|_token, i| i.network_path == self.network_path);
489
490 if !self.on_path_challenges_unconfirmed.is_empty() {
492 self.pending_on_path_challenge = true;
493 }
494 OnPathResponseReceived::Ignored {
495 sent_on: info.network_path,
496 current_path: self.network_path,
497 }
498 }
499 None => match self.off_path_challenges_unconfirmed.remove(&token) {
500 Some(info) => {
502 self.off_path_challenges_unconfirmed
505 .retain(|_token, i| i.network_path.remote != info.network_path.remote);
506 OnPathResponseReceived::OffPath
507 }
508 None => OnPathResponseReceived::Unknown,
510 },
511 }
512 }
513
514 pub(super) fn reset_on_path_challenges(&mut self) {
516 self.on_path_challenges_unconfirmed.clear();
517 self.pending_on_path_challenge = false;
518 }
519
520 #[cfg(feature = "qlog")]
521 pub(super) fn qlog_recovery_metrics(
522 &mut self,
523 path_id: PathId,
524 ) -> Option<RecoveryMetricsUpdated> {
525 let controller_metrics = self.congestion.metrics();
526
527 let metrics = RecoveryMetrics {
528 min_rtt: Some(self.rtt.min),
529 smoothed_rtt: Some(self.rtt.get()),
530 latest_rtt: Some(self.rtt.latest),
531 rtt_variance: Some(self.rtt.var),
532 pto_count: Some(self.pto_count),
533 bytes_in_flight: Some(self.in_flight.bytes),
534 packets_in_flight: Some(self.in_flight.ack_eliciting),
535
536 congestion_window: Some(controller_metrics.congestion_window),
537 ssthresh: controller_metrics.ssthresh,
538 pacing_rate: controller_metrics.pacing_rate,
539 };
540
541 let event = metrics.to_qlog_event(path_id, &self.recovery_metrics);
542 self.recovery_metrics = metrics;
543 event
544 }
545
546 pub(super) fn pacing_delay(&mut self, bytes_to_send: u64, now: Instant) -> Option<Instant> {
550 let smoothed_rtt = self.rtt.get();
551 self.pacing.delay(
552 smoothed_rtt,
553 bytes_to_send,
554 self.current_mtu(),
555 self.congestion.window(),
556 now,
557 )
558 }
559
560 #[must_use = "updated observed address must be reported to the application"]
564 pub(super) fn update_observed_addr_report(
565 &mut self,
566 observed: ObservedAddr,
567 ) -> Option<SocketAddr> {
568 match self.last_observed_addr_report.as_mut() {
569 Some(prev) => {
570 if prev.seq_no >= observed.seq_no {
571 None
573 } else if prev.ip == observed.ip && prev.port == observed.port {
574 prev.seq_no = observed.seq_no;
576 None
577 } else {
578 let addr = observed.socket_addr();
579 self.last_observed_addr_report = Some(observed);
580 Some(addr)
581 }
582 }
583 None => {
584 let addr = observed.socket_addr();
585 self.last_observed_addr_report = Some(observed);
586 Some(addr)
587 }
588 }
589 }
590
591 pub(crate) fn remote_status(&self) -> Option<PathStatus> {
592 self.status.remote_status.map(|(_seq, status)| status)
593 }
594
595 pub(crate) fn local_status(&self) -> PathStatus {
596 self.status.local_status
597 }
598
599 pub(super) fn generation(&self) -> u64 {
600 self.generation
601 }
602}
603
604pub(super) enum OnPathResponseReceived {
605 OnPath { was_open: bool },
607 OffPath,
609 Unknown,
611 Ignored {
613 sent_on: FourTuple,
614 current_path: FourTuple,
615 },
616}
617
618#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
619pub(super) enum OpenStatus {
620 #[default]
622 Pending,
623 Sent,
626 Informed,
628}
629
630#[cfg(feature = "qlog")]
634#[derive(Default, Clone, PartialEq, Debug)]
635#[non_exhaustive]
636struct RecoveryMetrics {
637 pub min_rtt: Option<Duration>,
638 pub smoothed_rtt: Option<Duration>,
639 pub latest_rtt: Option<Duration>,
640 pub rtt_variance: Option<Duration>,
641 pub pto_count: Option<u32>,
642 pub bytes_in_flight: Option<u64>,
643 pub packets_in_flight: Option<u64>,
644 pub congestion_window: Option<u64>,
645 pub ssthresh: Option<u64>,
646 pub pacing_rate: Option<u64>,
647}
648
649#[cfg(feature = "qlog")]
650impl RecoveryMetrics {
651 fn retain_updated(&self, previous: &Self) -> Self {
653 macro_rules! keep_if_changed {
654 ($name:ident) => {
655 if previous.$name == self.$name {
656 None
657 } else {
658 self.$name
659 }
660 };
661 }
662
663 Self {
664 min_rtt: keep_if_changed!(min_rtt),
665 smoothed_rtt: keep_if_changed!(smoothed_rtt),
666 latest_rtt: keep_if_changed!(latest_rtt),
667 rtt_variance: keep_if_changed!(rtt_variance),
668 pto_count: keep_if_changed!(pto_count),
669 bytes_in_flight: keep_if_changed!(bytes_in_flight),
670 packets_in_flight: keep_if_changed!(packets_in_flight),
671 congestion_window: keep_if_changed!(congestion_window),
672 ssthresh: keep_if_changed!(ssthresh),
673 pacing_rate: keep_if_changed!(pacing_rate),
674 }
675 }
676
677 fn to_qlog_event(&self, path_id: PathId, previous: &Self) -> Option<RecoveryMetricsUpdated> {
679 let updated = self.retain_updated(previous);
680
681 if updated == Self::default() {
682 return None;
683 }
684
685 Some(RecoveryMetricsUpdated {
686 min_rtt: updated.min_rtt.map(|rtt| rtt.as_secs_f32()),
687 smoothed_rtt: updated.smoothed_rtt.map(|rtt| rtt.as_secs_f32()),
688 latest_rtt: updated.latest_rtt.map(|rtt| rtt.as_secs_f32()),
689 rtt_variance: updated.rtt_variance.map(|rtt| rtt.as_secs_f32()),
690 pto_count: updated
691 .pto_count
692 .map(|count| count.try_into().unwrap_or(u16::MAX)),
693 bytes_in_flight: updated.bytes_in_flight,
694 packets_in_flight: updated.packets_in_flight,
695 congestion_window: updated.congestion_window,
696 ssthresh: updated.ssthresh,
697 pacing_rate: updated.pacing_rate,
698 path_id: Some(path_id.as_u32() as u64),
699 })
700 }
701}
702
703#[derive(Copy, Clone, Debug)]
705pub struct RttEstimator {
706 latest: Duration,
708 smoothed: Option<Duration>,
710 var: Duration,
712 min: Duration,
714}
715
716impl RttEstimator {
717 pub(super) fn new(initial_rtt: Duration) -> Self {
718 Self {
719 latest: initial_rtt,
720 smoothed: None,
721 var: initial_rtt / 2,
722 min: initial_rtt,
723 }
724 }
725
726 pub(crate) fn reset_initial_rtt(&mut self, initial_rtt: Duration) {
739 if self.smoothed.is_none() {
740 self.latest = initial_rtt;
741 self.var = initial_rtt / 2;
742 self.min = initial_rtt;
743 }
744 }
745
746 pub fn get(&self) -> Duration {
748 self.smoothed.unwrap_or(self.latest)
749 }
750
751 pub fn conservative(&self) -> Duration {
756 self.get().max(self.latest)
757 }
758
759 pub fn min(&self) -> Duration {
761 self.min
762 }
763
764 pub(crate) fn pto_base(&self) -> Duration {
766 self.get() + cmp::max(4 * self.var, TIMER_GRANULARITY)
767 }
768
769 pub(crate) fn update(&mut self, ack_delay: Duration, rtt: Duration) {
771 self.latest = rtt;
772 self.min = cmp::min(self.min, self.latest);
775 if let Some(smoothed) = self.smoothed {
777 let adjusted_rtt = if self.min + ack_delay <= self.latest {
778 self.latest - ack_delay
779 } else {
780 self.latest
781 };
782 let var_sample = smoothed.abs_diff(adjusted_rtt);
783 self.var = (3 * self.var + var_sample) / 4;
784 self.smoothed = Some((7 * smoothed + adjusted_rtt) / 8);
785 } else {
786 self.smoothed = Some(self.latest);
787 self.var = self.latest / 2;
788 self.min = self.latest;
789 }
790 }
791}
792
793#[derive(Default, Debug)]
794pub(crate) struct PathResponses {
795 pending: Vec<PathResponse>,
796}
797
798impl PathResponses {
799 pub(crate) fn push(&mut self, packet: u64, token: u64, network_path: FourTuple) {
800 const MAX_PATH_RESPONSES: usize = 16;
802 let response = PathResponse {
803 packet,
804 token,
805 network_path,
806 };
807 let existing = self
808 .pending
809 .iter_mut()
810 .find(|x| x.network_path.remote == network_path.remote);
811 if let Some(existing) = existing {
812 if existing.packet <= packet {
814 *existing = response;
815 }
816 return;
817 }
818 if self.pending.len() < MAX_PATH_RESPONSES {
819 self.pending.push(response);
820 } else {
821 trace!("ignoring excessive PATH_CHALLENGE");
824 }
825 }
826
827 pub(crate) fn pop_off_path(&mut self, network_path: FourTuple) -> Option<(u64, FourTuple)> {
828 let response = *self.pending.last()?;
829 if response.network_path == network_path {
833 return None;
836 }
837 self.pending.pop();
838 Some((response.token, response.network_path))
839 }
840
841 pub(crate) fn pop_on_path(&mut self, network_path: FourTuple) -> Option<u64> {
842 let response = *self.pending.last()?;
843 if response.network_path != network_path {
845 return None;
848 }
849 self.pending.pop();
850 Some(response.token)
851 }
852
853 pub(crate) fn is_empty(&self) -> bool {
854 self.pending.is_empty()
855 }
856}
857
858#[derive(Copy, Clone, Debug)]
859struct PathResponse {
860 packet: u64,
862 token: u64,
864 network_path: FourTuple,
866}
867
868#[derive(Debug)]
871pub(super) struct InFlight {
872 pub(super) bytes: u64,
877 pub(super) ack_eliciting: u64,
883}
884
885impl InFlight {
886 fn new() -> Self {
887 Self {
888 bytes: 0,
889 ack_eliciting: 0,
890 }
891 }
892
893 fn insert(&mut self, packet: &SentPacket) {
894 self.bytes += u64::from(packet.size);
895 self.ack_eliciting += u64::from(packet.ack_eliciting);
896 }
897
898 fn remove(&mut self, packet: &SentPacket) {
900 self.bytes -= u64::from(packet.size);
901 self.ack_eliciting -= u64::from(packet.ack_eliciting);
902 }
903}
904
905#[derive(Debug, Clone, Default)]
907pub(super) struct PathStatusState {
908 local_status: PathStatus,
910 local_seq: VarInt,
914 remote_status: Option<(VarInt, PathStatus)>,
916}
917
918impl PathStatusState {
919 pub(super) fn remote_update(&mut self, status: PathStatus, seq: VarInt) {
921 if self.remote_status.is_some_and(|(curr, _)| curr >= seq) {
922 return trace!(%seq, "ignoring path status update");
923 }
924
925 let prev = self.remote_status.replace((seq, status)).map(|(_, s)| s);
926 if prev != Some(status) {
927 debug!(?status, ?seq, "remote changed path status");
928 }
929 }
930
931 pub(super) fn local_update(&mut self, status: PathStatus) -> Option<PathStatus> {
935 if self.local_status == status {
936 return None;
937 }
938
939 self.local_seq = self.local_seq.saturating_add(1u8);
940 Some(std::mem::replace(&mut self.local_status, status))
941 }
942
943 pub(crate) fn seq(&self) -> VarInt {
944 self.local_seq
945 }
946}
947
948#[cfg_attr(test, derive(test_strategy::Arbitrary))]
953#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
954pub enum PathStatus {
955 #[default]
960 Available,
961 Backup,
966}
967
968#[derive(Debug, Clone, PartialEq, Eq)]
970pub enum PathEvent {
971 Opened {
973 id: PathId,
975 },
976 Abandoned {
980 id: PathId,
982 reason: PathAbandonReason,
984 },
985 Discarded {
989 id: PathId,
991 path_stats: PathStats,
995 },
996 RemoteStatus {
1002 id: PathId,
1004 status: PathStatus,
1006 },
1007 ObservedAddr {
1009 id: PathId,
1012 addr: SocketAddr,
1014 },
1015}
1016
1017#[derive(Debug, Clone, Eq, PartialEq)]
1019pub enum PathAbandonReason {
1020 ApplicationClosed {
1022 error_code: VarInt,
1024 },
1025 ValidationFailed,
1027 TimedOut,
1029 UnusableAfterNetworkChange,
1031 NatTraversalRoundEnded,
1033 RemoteAbandoned {
1035 error_code: VarInt,
1037 },
1038}
1039
1040impl PathAbandonReason {
1041 pub(crate) fn is_locally_initiated(&self) -> bool {
1043 !matches!(self, Self::RemoteAbandoned { .. })
1044 }
1045
1046 pub(crate) fn error_code(&self) -> TransportErrorCode {
1048 match self {
1049 Self::ApplicationClosed { error_code } => (*error_code).into(),
1050 Self::NatTraversalRoundEnded => TransportErrorCode::APPLICATION_ABANDON_PATH,
1051 Self::ValidationFailed | Self::TimedOut | Self::UnusableAfterNetworkChange => {
1052 TransportErrorCode::PATH_UNSTABLE_OR_POOR
1053 }
1054 Self::RemoteAbandoned { error_code } => (*error_code).into(),
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Error, Clone, PartialEq, Eq)]
1061pub enum SetPathStatusError {
1062 #[error("closed path")]
1064 ClosedPath,
1065 #[error("multipath not negotiated")]
1067 MultipathNotNegotiated,
1068}
1069
1070#[derive(Debug, Default, Error, Clone, PartialEq, Eq)]
1072#[error("closed path")]
1073pub struct ClosedPath {
1074 pub(super) _private: (),
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080
1081 #[test]
1082 fn test_path_id_saturating_add() {
1083 let large: PathId = u16::MAX.into();
1085 let next = u32::from(u16::MAX) + 1;
1086 assert_eq!(large.saturating_add(1u8), PathId::from(next));
1087
1088 assert_eq!(PathId::MAX.saturating_add(1u8), PathId::MAX)
1090 }
1091}