aboutsummaryrefslogtreecommitdiff
path: root/src/path.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/path.rs')
-rw-r--r--src/path.rs1069
1 files changed, 1069 insertions, 0 deletions
diff --git a/src/path.rs b/src/path.rs
new file mode 100644
index 0000000..ea59659
--- /dev/null
+++ b/src/path.rs
@@ -0,0 +1,1069 @@
+// Copyright (C) 2022, Cloudflare, Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+use std::time;
+
+use std::collections::BTreeMap;
+use std::collections::VecDeque;
+use std::net::SocketAddr;
+
+use slab::Slab;
+
+use crate::Error;
+use crate::Result;
+
+use crate::recovery;
+use crate::recovery::HandshakeStatus;
+
+/// The different states of the path validation.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub enum PathState {
+ /// The path failed its validation.
+ Failed,
+
+ /// The path exists, but no path validation has been performed.
+ Unknown,
+
+ /// The path is under validation.
+ Validating,
+
+ /// The remote address has been validated, but not the path MTU.
+ ValidatingMTU,
+
+ /// The path has been validated.
+ Validated,
+}
+
+impl PathState {
+ #[cfg(feature = "ffi")]
+ pub fn to_c(self) -> libc::ssize_t {
+ match self {
+ PathState::Failed => -1,
+ PathState::Unknown => 0,
+ PathState::Validating => 1,
+ PathState::ValidatingMTU => 2,
+ PathState::Validated => 3,
+ }
+ }
+}
+
+/// A path-specific event.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum PathEvent {
+ /// A new network path (local address, peer address) has been seen on a
+ /// received packet. Note that this event is only triggered for servers, as
+ /// the client is responsible from initiating new paths. The application may
+ /// then probe this new path, if desired.
+ New(SocketAddr, SocketAddr),
+
+ /// The related network path between local `SocketAddr` and peer
+ /// `SocketAddr` has been validated.
+ Validated(SocketAddr, SocketAddr),
+
+ /// The related network path between local `SocketAddr` and peer
+ /// `SocketAddr` failed to be validated. This network path will not be used
+ /// anymore, unless the application requests probing this path again.
+ FailedValidation(SocketAddr, SocketAddr),
+
+ /// The related network path between local `SocketAddr` and peer
+ /// `SocketAddr` has been closed and is now unusable on this connection.
+ Closed(SocketAddr, SocketAddr),
+
+ /// The stack observes that the Source Connection ID with the given sequence
+ /// number, initially used by the peer over the first pair of `SocketAddr`s,
+ /// is now reused over the second pair of `SocketAddr`s.
+ ReusedSourceConnectionId(
+ u64,
+ (SocketAddr, SocketAddr),
+ (SocketAddr, SocketAddr),
+ ),
+
+ /// The connection observed that the peer migrated over the network path
+ /// denoted by the pair of `SocketAddr`, i.e., non-probing packets have been
+ /// received on this network path. This is a server side only event.
+ ///
+ /// Note that this event is only raised if the path has been validated.
+ PeerMigrated(SocketAddr, SocketAddr),
+}
+
+/// A network path on which QUIC packets can be sent.
+#[derive(Debug)]
+pub struct Path {
+ /// The local address.
+ local_addr: SocketAddr,
+
+ /// The remote address.
+ peer_addr: SocketAddr,
+
+ /// Source CID sequence number used over that path.
+ pub active_scid_seq: Option<u64>,
+
+ /// Destination CID sequence number used over that path.
+ pub active_dcid_seq: Option<u64>,
+
+ /// The current validation state of the path.
+ state: PathState,
+
+ /// Is this path used to send non-probing packets.
+ active: bool,
+
+ /// Loss recovery and congestion control state.
+ pub recovery: recovery::Recovery,
+
+ /// Pending challenge data with the size of the packet containing them and
+ /// when they were sent.
+ in_flight_challenges: VecDeque<([u8; 8], usize, time::Instant)>,
+
+ /// The maximum challenge size that got acknowledged.
+ max_challenge_size: usize,
+
+ /// Number of consecutive (spaced by at least 1 RTT) probing packets lost.
+ probing_lost: usize,
+
+ /// Last instant when a probing packet got lost.
+ last_probe_lost_time: Option<time::Instant>,
+
+ /// Received challenge data.
+ received_challenges: VecDeque<[u8; 8]>,
+
+ /// Number of packets sent on this path.
+ pub sent_count: usize,
+
+ /// Number of packets received on this path.
+ pub recv_count: usize,
+
+ /// Total number of packets sent with data retransmitted from this path.
+ pub retrans_count: usize,
+
+ /// Total number of sent bytes over this path.
+ pub sent_bytes: u64,
+
+ /// Total number of bytes received over this path.
+ pub recv_bytes: u64,
+
+ /// Total number of bytes retransmitted from this path.
+ /// This counts only STREAM and CRYPTO data.
+ pub stream_retrans_bytes: u64,
+
+ /// Total number of bytes the server can send before the peer's address
+ /// is verified.
+ pub max_send_bytes: usize,
+
+ /// Whether the peer's address has been verified.
+ pub verified_peer_address: bool,
+
+ /// Whether the peer has verified our address.
+ pub peer_verified_local_address: bool,
+
+ /// Does it requires sending PATH_CHALLENGE?
+ challenge_requested: bool,
+
+ /// Whether the failure of this path was notified.
+ failure_notified: bool,
+
+ /// Whether the connection tries to migrate to this path, but it still needs
+ /// to be validated.
+ migrating: bool,
+
+ /// Whether or not we should force eliciting of an ACK (e.g. via PING frame)
+ pub needs_ack_eliciting: bool,
+}
+
+impl Path {
+ /// Create a new Path instance with the provided addresses, the remaining of
+ /// the fields being set to their default value.
+ pub fn new(
+ local_addr: SocketAddr, peer_addr: SocketAddr,
+ recovery_config: &recovery::RecoveryConfig, is_initial: bool,
+ ) -> Self {
+ let (state, active_scid_seq, active_dcid_seq) = if is_initial {
+ (PathState::Validated, Some(0), Some(0))
+ } else {
+ (PathState::Unknown, None, None)
+ };
+
+ Self {
+ local_addr,
+ peer_addr,
+ active_scid_seq,
+ active_dcid_seq,
+ state,
+ active: false,
+ recovery: recovery::Recovery::new_with_config(recovery_config),
+ in_flight_challenges: VecDeque::new(),
+ max_challenge_size: 0,
+ probing_lost: 0,
+ last_probe_lost_time: None,
+ received_challenges: VecDeque::new(),
+ sent_count: 0,
+ recv_count: 0,
+ retrans_count: 0,
+ sent_bytes: 0,
+ recv_bytes: 0,
+ stream_retrans_bytes: 0,
+ max_send_bytes: 0,
+ verified_peer_address: false,
+ peer_verified_local_address: false,
+ challenge_requested: false,
+ failure_notified: false,
+ migrating: false,
+ needs_ack_eliciting: false,
+ }
+ }
+
+ /// Returns the local address on which this path operates.
+ #[inline]
+ pub fn local_addr(&self) -> SocketAddr {
+ self.local_addr
+ }
+
+ /// Returns the peer address on which this path operates.
+ #[inline]
+ pub fn peer_addr(&self) -> SocketAddr {
+ self.peer_addr
+ }
+
+ /// Returns whether the path is working (i.e., not failed).
+ #[inline]
+ fn working(&self) -> bool {
+ self.state > PathState::Failed
+ }
+
+ /// Returns whether the path is active.
+ #[inline]
+ pub fn active(&self) -> bool {
+ self.active && self.working() && self.active_dcid_seq.is_some()
+ }
+
+ /// Returns whether the path can be used to send non-probing packets.
+ #[inline]
+ pub fn usable(&self) -> bool {
+ self.active() ||
+ (self.state == PathState::Validated &&
+ self.active_dcid_seq.is_some())
+ }
+
+ /// Returns whether the path is unused.
+ #[inline]
+ fn unused(&self) -> bool {
+ // FIXME: we should check that there is nothing in the sent queue.
+ !self.active() && self.active_dcid_seq.is_none()
+ }
+
+ /// Returns whether the path requires sending a probing packet.
+ #[inline]
+ pub fn probing_required(&self) -> bool {
+ !self.received_challenges.is_empty() || self.validation_requested()
+ }
+
+ /// Promotes the path to the provided state only if the new state is greater
+ /// than the current one.
+ fn promote_to(&mut self, state: PathState) {
+ if self.state < state {
+ self.state = state;
+ }
+ }
+
+ /// Returns whether the path is validated.
+ #[inline]
+ pub fn validated(&self) -> bool {
+ self.state == PathState::Validated
+ }
+
+ /// Returns whether this path failed its validation.
+ #[inline]
+ fn validation_failed(&self) -> bool {
+ self.state == PathState::Failed
+ }
+
+ // Returns whether this path is under path validation process.
+ #[inline]
+ pub fn under_validation(&self) -> bool {
+ matches!(self.state, PathState::Validating | PathState::ValidatingMTU)
+ }
+
+ /// Requests path validation.
+ #[inline]
+ pub fn request_validation(&mut self) {
+ self.challenge_requested = true;
+ }
+
+ /// Returns whether a validation is requested.
+ #[inline]
+ pub fn validation_requested(&self) -> bool {
+ self.challenge_requested
+ }
+
+ pub fn on_challenge_sent(&mut self) {
+ self.promote_to(PathState::Validating);
+ self.challenge_requested = false;
+ }
+
+ /// Handles the sending of PATH_CHALLENGE.
+ pub fn add_challenge_sent(
+ &mut self, data: [u8; 8], pkt_size: usize, sent_time: time::Instant,
+ ) {
+ self.on_challenge_sent();
+ self.in_flight_challenges
+ .push_back((data, pkt_size, sent_time));
+ }
+
+ pub fn on_challenge_received(&mut self, data: [u8; 8]) {
+ self.received_challenges.push_back(data);
+ self.peer_verified_local_address = true;
+ }
+
+ pub fn has_pending_challenge(&self, data: [u8; 8]) -> bool {
+ self.in_flight_challenges.iter().any(|(d, ..)| *d == data)
+ }
+
+ /// Returns whether the path is now validated.
+ pub fn on_response_received(&mut self, data: [u8; 8]) -> bool {
+ self.verified_peer_address = true;
+ self.probing_lost = 0;
+
+ let mut challenge_size = 0;
+ self.in_flight_challenges.retain(|(d, s, _)| {
+ if *d == data {
+ challenge_size = *s;
+ false
+ } else {
+ true
+ }
+ });
+
+ // The 4-tuple is reachable, but we didn't check Path MTU yet.
+ self.promote_to(PathState::ValidatingMTU);
+
+ self.max_challenge_size =
+ std::cmp::max(self.max_challenge_size, challenge_size);
+
+ if self.state == PathState::ValidatingMTU {
+ if self.max_challenge_size >= crate::MIN_CLIENT_INITIAL_LEN {
+ // Path MTU is sufficient for QUIC traffic.
+ self.promote_to(PathState::Validated);
+ return true;
+ }
+
+ // If the MTU was not validated, probe again.
+ self.request_validation();
+ }
+
+ false
+ }
+
+ fn on_failed_validation(&mut self) {
+ self.state = PathState::Failed;
+ self.active = false;
+ }
+
+ #[inline]
+ pub fn pop_received_challenge(&mut self) -> Option<[u8; 8]> {
+ self.received_challenges.pop_front()
+ }
+
+ pub fn on_loss_detection_timeout(
+ &mut self, handshake_status: HandshakeStatus, now: time::Instant,
+ is_server: bool, trace_id: &str,
+ ) -> (usize, usize) {
+ let (lost_packets, lost_bytes) = self.recovery.on_loss_detection_timeout(
+ handshake_status,
+ now,
+ trace_id,
+ );
+
+ let mut lost_probe_time = None;
+ self.in_flight_challenges.retain(|(_, _, sent_time)| {
+ if *sent_time <= now {
+ if lost_probe_time.is_none() {
+ lost_probe_time = Some(*sent_time);
+ }
+ false
+ } else {
+ true
+ }
+ });
+
+ // If we lost probing packets, check if the path failed
+ // validation.
+ if let Some(lost_probe_time) = lost_probe_time {
+ self.last_probe_lost_time = match self.last_probe_lost_time {
+ Some(last) => {
+ // Count a loss if at least 1-RTT happened.
+ if lost_probe_time - last >= self.recovery.rtt() {
+ self.probing_lost += 1;
+ Some(lost_probe_time)
+ } else {
+ Some(last)
+ }
+ },
+ None => {
+ self.probing_lost += 1;
+ Some(lost_probe_time)
+ },
+ };
+ // As a server, if requesting a challenge is not
+ // possible due to the amplification attack, declare the
+ // validation as failed.
+ if self.probing_lost >= crate::MAX_PROBING_TIMEOUTS ||
+ (is_server && self.max_send_bytes < crate::MIN_PROBING_SIZE)
+ {
+ self.on_failed_validation();
+ } else {
+ self.request_validation();
+ }
+ }
+
+ (lost_packets, lost_bytes)
+ }
+
+ pub fn stats(&self) -> PathStats {
+ PathStats {
+ local_addr: self.local_addr,
+ peer_addr: self.peer_addr,
+ validation_state: self.state,
+ active: self.active,
+ recv: self.recv_count,
+ sent: self.sent_count,
+ lost: self.recovery.lost_count,
+ retrans: self.retrans_count,
+ rtt: self.recovery.rtt(),
+ min_rtt: self.recovery.min_rtt(),
+ rttvar: self.recovery.rttvar(),
+ cwnd: self.recovery.cwnd(),
+ sent_bytes: self.sent_bytes,
+ recv_bytes: self.recv_bytes,
+ lost_bytes: self.recovery.bytes_lost,
+ stream_retrans_bytes: self.stream_retrans_bytes,
+ pmtu: self.recovery.max_datagram_size(),
+ delivery_rate: self.recovery.delivery_rate(),
+ }
+ }
+}
+
+/// An iterator over SocketAddr.
+#[derive(Default)]
+pub struct SocketAddrIter {
+ pub(crate) sockaddrs: Vec<SocketAddr>,
+}
+
+impl Iterator for SocketAddrIter {
+ type Item = SocketAddr;
+
+ #[inline]
+ fn next(&mut self) -> Option<Self::Item> {
+ self.sockaddrs.pop()
+ }
+}
+
+impl ExactSizeIterator for SocketAddrIter {
+ #[inline]
+ fn len(&self) -> usize {
+ self.sockaddrs.len()
+ }
+}
+
+/// All path-related information.
+pub struct PathMap {
+ /// The paths of the connection. Each of them has an internal identifier
+ /// that is used by `addrs_to_paths` and `ConnectionEntry`.
+ paths: Slab<Path>,
+
+ /// The maximum number of concurrent paths allowed.
+ max_concurrent_paths: usize,
+
+ /// The mapping from the (local `SocketAddr`, peer `SocketAddr`) to the
+ /// `Path` structure identifier.
+ addrs_to_paths: BTreeMap<(SocketAddr, SocketAddr), usize>,
+
+ /// Path-specific events to be notified to the application.
+ events: VecDeque<PathEvent>,
+
+ /// Whether this manager serves a connection as a server.
+ is_server: bool,
+}
+
+impl PathMap {
+ /// Creates a new `PathMap` with the initial provided `path` and a
+ /// capacity limit.
+ pub fn new(
+ mut initial_path: Path, max_concurrent_paths: usize, is_server: bool,
+ ) -> Self {
+ let mut paths = Slab::with_capacity(1); // most connections only have one path
+ let mut addrs_to_paths = BTreeMap::new();
+
+ let local_addr = initial_path.local_addr;
+ let peer_addr = initial_path.peer_addr;
+
+ // As it is the first path, it is active by default.
+ initial_path.active = true;
+
+ let active_path_id = paths.insert(initial_path);
+ addrs_to_paths.insert((local_addr, peer_addr), active_path_id);
+
+ Self {
+ paths,
+ max_concurrent_paths,
+ addrs_to_paths,
+ events: VecDeque::new(),
+ is_server,
+ }
+ }
+
+ /// Gets an immutable reference to the path identified by `path_id`. If the
+ /// provided `path_id` does not identify any current `Path`, returns an
+ /// [`InvalidState`].
+ ///
+ /// [`InvalidState`]: enum.Error.html#variant.InvalidState
+ #[inline]
+ pub fn get(&self, path_id: usize) -> Result<&Path> {
+ self.paths.get(path_id).ok_or(Error::InvalidState)
+ }
+
+ /// Gets a mutable reference to the path identified by `path_id`. If the
+ /// provided `path_id` does not identify any current `Path`, returns an
+ /// [`InvalidState`].
+ ///
+ /// [`InvalidState`]: enum.Error.html#variant.InvalidState
+ #[inline]
+ pub fn get_mut(&mut self, path_id: usize) -> Result<&mut Path> {
+ self.paths.get_mut(path_id).ok_or(Error::InvalidState)
+ }
+
+ #[inline]
+ /// Gets an immutable reference to the active path with the value of the
+ /// lowest identifier. If there is no active path, returns `None`.
+ pub fn get_active_with_pid(&self) -> Option<(usize, &Path)> {
+ self.paths.iter().find(|(_, p)| p.active())
+ }
+
+ /// Gets an immutable reference to the active path with the lowest
+ /// identifier. If there is no active path, returns an [`InvalidState`].
+ ///
+ /// [`InvalidState`]: enum.Error.html#variant.InvalidState
+ #[inline]
+ pub fn get_active(&self) -> Result<&Path> {
+ self.get_active_with_pid()
+ .map(|(_, p)| p)
+ .ok_or(Error::InvalidState)
+ }
+
+ /// Gets the lowest active path identifier. If there is no active path,
+ /// returns an [`InvalidState`].
+ ///
+ /// [`InvalidState`]: enum.Error.html#variant.InvalidState
+ #[inline]
+ pub fn get_active_path_id(&self) -> Result<usize> {
+ self.get_active_with_pid()
+ .map(|(pid, _)| pid)
+ .ok_or(Error::InvalidState)
+ }
+
+ /// Gets an mutable reference to the active path with the lowest identifier.
+ /// If there is no active path, returns an [`InvalidState`].
+ ///
+ /// [`InvalidState`]: enum.Error.html#variant.InvalidState
+ #[inline]
+ pub fn get_active_mut(&mut self) -> Result<&mut Path> {
+ self.paths
+ .iter_mut()
+ .map(|(_, p)| p)
+ .find(|p| p.active())
+ .ok_or(Error::InvalidState)
+ }
+
+ /// Returns an iterator over all existing paths.
+ #[inline]
+ pub fn iter(&self) -> slab::Iter<Path> {
+ self.paths.iter()
+ }
+
+ /// Returns a mutable iterator over all existing paths.
+ #[inline]
+ pub fn iter_mut(&mut self) -> slab::IterMut<Path> {
+ self.paths.iter_mut()
+ }
+
+ /// Returns the number of existing paths.
+ #[inline]
+ pub fn len(&self) -> usize {
+ self.paths.len()
+ }
+
+ /// Returns the `Path` identifier related to the provided `addrs`.
+ #[inline]
+ pub fn path_id_from_addrs(
+ &self, addrs: &(SocketAddr, SocketAddr),
+ ) -> Option<usize> {
+ self.addrs_to_paths.get(addrs).copied()
+ }
+
+ /// Checks if creating a new path will not exceed the current `self.paths`
+ /// capacity. If yes, this method tries to remove one unused path. If it
+ /// fails to do so, returns [`Done`].
+ ///
+ /// [`Done`]: enum.Error.html#variant.Done
+ fn make_room_for_new_path(&mut self) -> Result<()> {
+ if self.paths.len() < self.max_concurrent_paths {
+ return Ok(());
+ }
+
+ let (pid_to_remove, _) = self
+ .paths
+ .iter()
+ .find(|(_, p)| p.unused())
+ .ok_or(Error::Done)?;
+
+ let path = self.paths.remove(pid_to_remove);
+ self.addrs_to_paths
+ .remove(&(path.local_addr, path.peer_addr));
+
+ self.notify_event(PathEvent::Closed(path.local_addr, path.peer_addr));
+
+ Ok(())
+ }
+
+ /// Records the provided `Path` and returns its assigned identifier.
+ ///
+ /// On success, this method takes care of creating a notification to the
+ /// serving application, if it serves a server-side connection.
+ ///
+ /// If there are already `max_concurrent_paths` currently recorded, this
+ /// method tries to remove an unused `Path` first. If it fails to do so,
+ /// it returns [`Done`].
+ ///
+ /// [`Done`]: enum.Error.html#variant.Done
+ pub fn insert_path(&mut self, path: Path, is_server: bool) -> Result<usize> {
+ self.make_room_for_new_path()?;
+
+ let local_addr = path.local_addr;
+ let peer_addr = path.peer_addr;
+
+ let pid = self.paths.insert(path);
+ self.addrs_to_paths.insert((local_addr, peer_addr), pid);
+
+ // Notifies the application if we are in server mode.
+ if is_server {
+ self.notify_event(PathEvent::New(local_addr, peer_addr));
+ }
+
+ Ok(pid)
+ }
+
+ /// Notifies a path event to the application served by the connection.
+ pub fn notify_event(&mut self, ev: PathEvent) {
+ self.events.push_back(ev);
+ }
+
+ /// Gets the first path event to be notified to the application.
+ pub fn pop_event(&mut self) -> Option<PathEvent> {
+ self.events.pop_front()
+ }
+
+ /// Notifies all failed validations to the application.
+ pub fn notify_failed_validations(&mut self) {
+ let validation_failed = self
+ .paths
+ .iter_mut()
+ .filter(|(_, p)| p.validation_failed() && !p.failure_notified);
+
+ for (_, p) in validation_failed {
+ self.events.push_back(PathEvent::FailedValidation(
+ p.local_addr,
+ p.peer_addr,
+ ));
+
+ p.failure_notified = true;
+ }
+ }
+
+ /// Finds a path candidate to be active and returns its identifier.
+ pub fn find_candidate_path(&self) -> Option<usize> {
+ // TODO: also consider unvalidated paths if there are no more validated.
+ self.paths
+ .iter()
+ .find(|(_, p)| p.usable())
+ .map(|(pid, _)| pid)
+ }
+
+ /// Handles incoming PATH_RESPONSE data.
+ pub fn on_response_received(&mut self, data: [u8; 8]) -> Result<()> {
+ let active_pid = self.get_active_path_id()?;
+
+ let challenge_pending =
+ self.iter_mut().find(|(_, p)| p.has_pending_challenge(data));
+
+ if let Some((pid, p)) = challenge_pending {
+ if p.on_response_received(data) {
+ let local_addr = p.local_addr;
+ let peer_addr = p.peer_addr;
+ let was_migrating = p.migrating;
+
+ p.migrating = false;
+
+ // Notifies the application.
+ self.notify_event(PathEvent::Validated(local_addr, peer_addr));
+
+ // If this path was the candidate for migration, notifies the
+ // application.
+ if pid == active_pid && was_migrating {
+ self.notify_event(PathEvent::PeerMigrated(
+ local_addr, peer_addr,
+ ));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ /// Sets the path with identifier 'path_id' to be active.
+ ///
+ /// There can be exactly one active path on which non-probing packets can be
+ /// sent. If another path is marked as active, it will be superseded by the
+ /// one having `path_id` as identifier.
+ ///
+ /// A server should always ensure that the active path is validated. If it
+ /// is already the case, it notifies the application that the connection
+ /// migrated. Otherwise, it triggers a path validation and defers the
+ /// notification once it is actually validated.
+ pub fn set_active_path(&mut self, path_id: usize) -> Result<()> {
+ let is_server = self.is_server;
+
+ if let Ok(old_active_path) = self.get_active_mut() {
+ old_active_path.active = false;
+ }
+
+ let new_active_path = self.get_mut(path_id)?;
+ new_active_path.active = true;
+
+ if is_server {
+ if new_active_path.validated() {
+ let local_addr = new_active_path.local_addr();
+ let peer_addr = new_active_path.peer_addr();
+
+ self.notify_event(PathEvent::PeerMigrated(local_addr, peer_addr));
+ } else {
+ new_active_path.migrating = true;
+
+ // Requests path validation if needed.
+ if !new_active_path.under_validation() {
+ new_active_path.request_validation();
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Handles potential connection migration.
+ pub fn on_peer_migrated(
+ &mut self, new_pid: usize, disable_dcid_reuse: bool,
+ ) -> Result<()> {
+ let active_path_id = self.get_active_path_id()?;
+
+ if active_path_id == new_pid {
+ return Ok(());
+ }
+
+ self.set_active_path(new_pid)?;
+
+ let no_spare_dcid = self.get_mut(new_pid)?.active_dcid_seq.is_none();
+
+ if no_spare_dcid && !disable_dcid_reuse {
+ self.get_mut(new_pid)?.active_dcid_seq =
+ self.get_mut(active_path_id)?.active_dcid_seq;
+ }
+
+ Ok(())
+ }
+}
+
+/// Statistics about the path of a connection.
+///
+/// It is part of the `Stats` structure returned by the [`stats()`] method.
+///
+/// [`stats()`]: struct.Connection.html#method.stats
+#[derive(Clone)]
+pub struct PathStats {
+ /// The local address of the path.
+ pub local_addr: SocketAddr,
+
+ /// The peer address of the path.
+ pub peer_addr: SocketAddr,
+
+ /// The path validation state.
+ pub validation_state: PathState,
+
+ /// Whether the path is marked as active.
+ pub active: bool,
+
+ /// The number of QUIC packets received.
+ pub recv: usize,
+
+ /// The number of QUIC packets sent.
+ pub sent: usize,
+
+ /// The number of QUIC packets that were lost.
+ pub lost: usize,
+
+ /// The number of sent QUIC packets with retransmitted data.
+ pub retrans: usize,
+
+ /// The estimated round-trip time of the connection.
+ pub rtt: time::Duration,
+
+ /// The minimum round-trip time observed.
+ pub min_rtt: Option<time::Duration>,
+
+ /// The estimated round-trip time variation in samples using a mean
+ /// variation.
+ pub rttvar: time::Duration,
+
+ /// The size of the connection's congestion window in bytes.
+ pub cwnd: usize,
+
+ /// The number of sent bytes.
+ pub sent_bytes: u64,
+
+ /// The number of received bytes.
+ pub recv_bytes: u64,
+
+ /// The number of bytes lost.
+ pub lost_bytes: u64,
+
+ /// The number of stream bytes retransmitted.
+ pub stream_retrans_bytes: u64,
+
+ /// The current PMTU for the connection.
+ pub pmtu: usize,
+
+ /// The most recent data delivery rate estimate in bytes/s.
+ ///
+ /// Note that this value could be inaccurate if the application does not
+ /// respect pacing hints (see [`SendInfo.at`] and [Pacing] for more
+ /// details).
+ ///
+ /// [`SendInfo.at`]: struct.SendInfo.html#structfield.at
+ /// [Pacing]: index.html#pacing
+ pub delivery_rate: u64,
+}
+
+impl std::fmt::Debug for PathStats {
+ #[inline]
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(
+ f,
+ "local_addr={:?} peer_addr={:?} ",
+ self.local_addr, self.peer_addr,
+ )?;
+ write!(
+ f,
+ "validation_state={:?} active={} ",
+ self.validation_state, self.active,
+ )?;
+ write!(
+ f,
+ "recv={} sent={} lost={} retrans={} rtt={:?} min_rtt={:?} rttvar={:?} cwnd={}",
+ self.recv, self.sent, self.lost, self.retrans, self.rtt, self.min_rtt, self.rttvar, self.cwnd,
+ )?;
+
+ write!(
+ f,
+ " sent_bytes={} recv_bytes={} lost_bytes={}",
+ self.sent_bytes, self.recv_bytes, self.lost_bytes,
+ )?;
+
+ write!(
+ f,
+ " stream_retrans_bytes={} pmtu={} delivery_rate={}",
+ self.stream_retrans_bytes, self.pmtu, self.delivery_rate,
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::rand;
+ use crate::MIN_CLIENT_INITIAL_LEN;
+
+ use crate::recovery::RecoveryConfig;
+ use crate::Config;
+
+ use super::*;
+
+ #[test]
+ fn path_validation_limited_mtu() {
+ let client_addr = "127.0.0.1:1234".parse().unwrap();
+ let client_addr_2 = "127.0.0.1:5678".parse().unwrap();
+ let server_addr = "127.0.0.1:4321".parse().unwrap();
+
+ let config = Config::new(crate::PROTOCOL_VERSION).unwrap();
+ let recovery_config = RecoveryConfig::from_config(&config);
+
+ let path = Path::new(client_addr, server_addr, &recovery_config, true);
+ let mut path_mgr = PathMap::new(path, 2, false);
+
+ let probed_path =
+ Path::new(client_addr_2, server_addr, &recovery_config, false);
+ path_mgr.insert_path(probed_path, false).unwrap();
+
+ let pid = path_mgr
+ .path_id_from_addrs(&(client_addr_2, server_addr))
+ .unwrap();
+ path_mgr.get_mut(pid).unwrap().request_validation();
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validation_requested(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().probing_required(), true);
+
+ // Fake sending of PathChallenge in a packet of MIN_CLIENT_INITIAL_LEN - 1
+ // bytes.
+ let data = rand::rand_u64().to_be_bytes();
+ path_mgr.get_mut(pid).unwrap().add_challenge_sent(
+ data,
+ MIN_CLIENT_INITIAL_LEN - 1,
+ time::Instant::now(),
+ );
+
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validation_requested(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().probing_required(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().under_validation(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validated(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().state, PathState::Validating);
+ assert_eq!(path_mgr.pop_event(), None);
+
+ // Receives the response. The path is reachable, but the MTU is not
+ // validated yet.
+ path_mgr.on_response_received(data).unwrap();
+
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validation_requested(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().probing_required(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().under_validation(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validated(), false);
+ assert_eq!(
+ path_mgr.get_mut(pid).unwrap().state,
+ PathState::ValidatingMTU
+ );
+ assert_eq!(path_mgr.pop_event(), None);
+
+ // Fake sending of PathChallenge in a packet of MIN_CLIENT_INITIAL_LEN
+ // bytes.
+ let data = rand::rand_u64().to_be_bytes();
+ path_mgr.get_mut(pid).unwrap().add_challenge_sent(
+ data,
+ MIN_CLIENT_INITIAL_LEN,
+ time::Instant::now(),
+ );
+
+ path_mgr.on_response_received(data).unwrap();
+
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validation_requested(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().probing_required(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().under_validation(), false);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().validated(), true);
+ assert_eq!(path_mgr.get_mut(pid).unwrap().state, PathState::Validated);
+ assert_eq!(
+ path_mgr.pop_event(),
+ Some(PathEvent::Validated(client_addr_2, server_addr))
+ );
+ }
+
+ #[test]
+ fn multiple_probes() {
+ let client_addr = "127.0.0.1:1234".parse().unwrap();
+ let server_addr = "127.0.0.1:4321".parse().unwrap();
+
+ let config = Config::new(crate::PROTOCOL_VERSION).unwrap();
+ let recovery_config = RecoveryConfig::from_config(&config);
+
+ let path = Path::new(client_addr, server_addr, &recovery_config, true);
+ let mut client_path_mgr = PathMap::new(path, 2, false);
+ let mut server_path =
+ Path::new(server_addr, client_addr, &recovery_config, false);
+
+ let client_pid = client_path_mgr
+ .path_id_from_addrs(&(client_addr, server_addr))
+ .unwrap();
+
+ // First probe.
+ let data = rand::rand_u64().to_be_bytes();
+
+ client_path_mgr
+ .get_mut(client_pid)
+ .unwrap()
+ .add_challenge_sent(
+ data,
+ MIN_CLIENT_INITIAL_LEN,
+ time::Instant::now(),
+ );
+
+ // Second probe.
+ let data_2 = rand::rand_u64().to_be_bytes();
+
+ client_path_mgr
+ .get_mut(client_pid)
+ .unwrap()
+ .add_challenge_sent(
+ data_2,
+ MIN_CLIENT_INITIAL_LEN,
+ time::Instant::now(),
+ );
+ assert_eq!(
+ client_path_mgr
+ .get(client_pid)
+ .unwrap()
+ .in_flight_challenges
+ .len(),
+ 2
+ );
+
+ // If we receive multiple challenges, we can store them.
+ server_path.on_challenge_received(data);
+ assert_eq!(server_path.received_challenges.len(), 1);
+ server_path.on_challenge_received(data_2);
+ assert_eq!(server_path.received_challenges.len(), 2);
+
+ // Response for first probe.
+ client_path_mgr.on_response_received(data).unwrap();
+ assert_eq!(
+ client_path_mgr
+ .get(client_pid)
+ .unwrap()
+ .in_flight_challenges
+ .len(),
+ 1
+ );
+
+ // Response for second probe.
+ client_path_mgr.on_response_received(data_2).unwrap();
+ assert_eq!(
+ client_path_mgr
+ .get(client_pid)
+ .unwrap()
+ .in_flight_challenges
+ .len(),
+ 0
+ );
+ }
+}