forked from mirrors/gecko-dev
1030 lines
35 KiB
Rust
1030 lines
35 KiB
Rust
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
|
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
|
// option. This file may not be copied, modified, or distributed
|
|
// except according to those terms.
|
|
|
|
#![allow(clippy::module_name_repetitions)]
|
|
|
|
use std::{
|
|
cell::RefCell,
|
|
fmt::{self, Display},
|
|
mem,
|
|
net::{IpAddr, SocketAddr},
|
|
rc::Rc,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use neqo_common::{hex, qdebug, qinfo, qlog::NeqoQlog, qtrace, Datagram, Encoder, IpTos};
|
|
use neqo_crypto::random;
|
|
|
|
use crate::{
|
|
ackrate::{AckRate, PeerAckDelay},
|
|
cc::CongestionControlAlgorithm,
|
|
cid::{ConnectionId, ConnectionIdRef, ConnectionIdStore, RemoteConnectionIdEntry},
|
|
frame::{FRAME_TYPE_PATH_CHALLENGE, FRAME_TYPE_PATH_RESPONSE, FRAME_TYPE_RETIRE_CONNECTION_ID},
|
|
packet::PacketBuilder,
|
|
recovery::RecoveryToken,
|
|
rtt::RttEstimate,
|
|
sender::PacketSender,
|
|
stats::FrameStats,
|
|
tracking::{PacketNumberSpace, SentPacket},
|
|
Stats,
|
|
};
|
|
|
|
/// This is the MTU that we assume when using IPv6.
|
|
/// We use this size for Initial packets, so we don't need to worry about probing for support.
|
|
/// If the path doesn't support this MTU, we will assume that it doesn't support QUIC.
|
|
///
|
|
/// This is a multiple of 16 greater than the largest possible short header (1 + 20 + 4).
|
|
pub const PATH_MTU_V6: usize = 1337;
|
|
/// The path MTU for IPv4 can be 20 bytes larger than for v6.
|
|
pub const PATH_MTU_V4: usize = PATH_MTU_V6 + 20;
|
|
/// The number of times that a path will be probed before it is considered failed.
|
|
const MAX_PATH_PROBES: usize = 3;
|
|
/// The maximum number of paths that `Paths` will track.
|
|
const MAX_PATHS: usize = 15;
|
|
|
|
pub type PathRef = Rc<RefCell<Path>>;
|
|
|
|
/// A collection for network paths.
|
|
/// This holds a collection of paths that have been used for sending or
|
|
/// receiving, plus an additional "temporary" path that is held only while
|
|
/// processing a packet.
|
|
/// This structure limits its storage and will forget about paths if it
|
|
/// is exposed to too many paths.
|
|
#[derive(Debug, Default)]
|
|
pub struct Paths {
|
|
/// All of the paths. All of these paths will be permanent.
|
|
#[allow(unknown_lints)] // available with Rust v1.75
|
|
#[allow(clippy::struct_field_names)]
|
|
paths: Vec<PathRef>,
|
|
/// This is the primary path. This will only be `None` initially, so
|
|
/// care needs to be taken regarding that only during the handshake.
|
|
/// This path will also be in `paths`.
|
|
primary: Option<PathRef>,
|
|
|
|
/// The path that we would prefer to migrate to.
|
|
migration_target: Option<PathRef>,
|
|
|
|
/// Connection IDs that need to be retired.
|
|
to_retire: Vec<u64>,
|
|
|
|
/// `QLog` handler.
|
|
qlog: NeqoQlog,
|
|
}
|
|
|
|
impl Paths {
|
|
/// Find the path for the given addresses.
|
|
/// This might be a temporary path.
|
|
pub fn find_path(
|
|
&self,
|
|
local: SocketAddr,
|
|
remote: SocketAddr,
|
|
cc: CongestionControlAlgorithm,
|
|
pacing: bool,
|
|
now: Instant,
|
|
) -> PathRef {
|
|
self.paths
|
|
.iter()
|
|
.find_map(|p| {
|
|
if p.borrow().received_on(local, remote, false) {
|
|
Some(Rc::clone(p))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or_else(|| {
|
|
let mut p = Path::temporary(local, remote, cc, pacing, self.qlog.clone(), now);
|
|
if let Some(primary) = self.primary.as_ref() {
|
|
p.prime_rtt(primary.borrow().rtt());
|
|
}
|
|
Rc::new(RefCell::new(p))
|
|
})
|
|
}
|
|
|
|
/// Find the path, but allow for rebinding. That matches the pair of addresses
|
|
/// to paths that match the remote address only based on IP addres, not port.
|
|
/// We use this when the other side migrates to skip address validation and
|
|
/// creating a new path.
|
|
pub fn find_path_with_rebinding(
|
|
&self,
|
|
local: SocketAddr,
|
|
remote: SocketAddr,
|
|
cc: CongestionControlAlgorithm,
|
|
pacing: bool,
|
|
now: Instant,
|
|
) -> PathRef {
|
|
self.paths
|
|
.iter()
|
|
.find_map(|p| {
|
|
if p.borrow().received_on(local, remote, false) {
|
|
Some(Rc::clone(p))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| {
|
|
self.paths.iter().find_map(|p| {
|
|
if p.borrow().received_on(local, remote, true) {
|
|
Some(Rc::clone(p))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
.unwrap_or_else(|| {
|
|
Rc::new(RefCell::new(Path::temporary(
|
|
local,
|
|
remote,
|
|
cc,
|
|
pacing,
|
|
self.qlog.clone(),
|
|
now,
|
|
)))
|
|
})
|
|
}
|
|
|
|
/// Get a reference to the primary path. This will assert if there is no primary
|
|
/// path, which happens at a server prior to receiving a valid Initial packet
|
|
/// from a client. So be careful using this method.
|
|
pub fn primary(&self) -> PathRef {
|
|
self.primary_fallible().unwrap()
|
|
}
|
|
|
|
/// Get a reference to the primary path. Use this prior to handshake completion.
|
|
pub fn primary_fallible(&self) -> Option<PathRef> {
|
|
self.primary.clone()
|
|
}
|
|
|
|
/// Returns true if the path is not permanent.
|
|
pub fn is_temporary(&self, path: &PathRef) -> bool {
|
|
// Ask the path first, which is simpler.
|
|
path.borrow().is_temporary() || !self.paths.iter().any(|p| Rc::ptr_eq(p, path))
|
|
}
|
|
|
|
fn retire(to_retire: &mut Vec<u64>, retired: &PathRef) {
|
|
let seqno = retired
|
|
.borrow()
|
|
.remote_cid
|
|
.as_ref()
|
|
.unwrap()
|
|
.sequence_number();
|
|
to_retire.push(seqno);
|
|
}
|
|
|
|
/// Adopt a temporary path as permanent.
|
|
/// The first path that is made permanent is made primary.
|
|
pub fn make_permanent(
|
|
&mut self,
|
|
path: &PathRef,
|
|
local_cid: Option<ConnectionId>,
|
|
remote_cid: RemoteConnectionIdEntry,
|
|
) {
|
|
debug_assert!(self.is_temporary(path));
|
|
|
|
// Make sure not to track too many paths.
|
|
// This protects index 0, which contains the primary path.
|
|
if self.paths.len() >= MAX_PATHS {
|
|
debug_assert_eq!(self.paths.len(), MAX_PATHS);
|
|
let removed = self.paths.remove(1);
|
|
Self::retire(&mut self.to_retire, &removed);
|
|
if self
|
|
.migration_target
|
|
.as_ref()
|
|
.map_or(false, |target| Rc::ptr_eq(target, &removed))
|
|
{
|
|
qinfo!(
|
|
[path.borrow()],
|
|
"The migration target path had to be removed"
|
|
);
|
|
self.migration_target = None;
|
|
}
|
|
debug_assert_eq!(Rc::strong_count(&removed), 1);
|
|
}
|
|
|
|
qdebug!([path.borrow()], "Make permanent");
|
|
path.borrow_mut().make_permanent(local_cid, remote_cid);
|
|
self.paths.push(Rc::clone(path));
|
|
if self.primary.is_none() {
|
|
assert!(self.select_primary(path).is_none());
|
|
}
|
|
}
|
|
|
|
/// Select a path as the primary. Returns the old primary path.
|
|
/// Using the old path is only necessary if this change in path is a reaction
|
|
/// to a migration from a peer, in which case the old path needs to be probed.
|
|
#[must_use]
|
|
fn select_primary(&mut self, path: &PathRef) -> Option<PathRef> {
|
|
qdebug!([path.borrow()], "set as primary path");
|
|
let old_path = self.primary.replace(Rc::clone(path)).map(|old| {
|
|
old.borrow_mut().set_primary(false);
|
|
old
|
|
});
|
|
|
|
// Swap the primary path into slot 0, so that it is protected from eviction.
|
|
let idx = self
|
|
.paths
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(i, p)| if Rc::ptr_eq(p, path) { Some(i) } else { None })
|
|
.expect("migration target should be permanent");
|
|
self.paths.swap(0, idx);
|
|
|
|
path.borrow_mut().set_primary(true);
|
|
old_path
|
|
}
|
|
|
|
/// Migrate to the identified path. If `force` is true, the path
|
|
/// is forcibly marked as valid and the path is used immediately.
|
|
/// Otherwise, migration will occur after probing succeeds.
|
|
/// The path is always probed and will be abandoned if probing fails.
|
|
/// Returns `true` if the path was migrated.
|
|
pub fn migrate(&mut self, path: &PathRef, force: bool, now: Instant) -> bool {
|
|
debug_assert!(!self.is_temporary(path));
|
|
if force || path.borrow().is_valid() {
|
|
path.borrow_mut().set_valid(now);
|
|
mem::drop(self.select_primary(path));
|
|
self.migration_target = None;
|
|
} else {
|
|
self.migration_target = Some(Rc::clone(path));
|
|
}
|
|
path.borrow_mut().probe();
|
|
self.migration_target.is_none()
|
|
}
|
|
|
|
/// Process elapsed time for active paths.
|
|
/// Returns an true if there are viable paths remaining after tidying up.
|
|
///
|
|
/// TODO(mt) - the paths should own the RTT estimator, so they can find the PTO
|
|
/// for themselves.
|
|
pub fn process_timeout(&mut self, now: Instant, pto: Duration) -> bool {
|
|
let to_retire = &mut self.to_retire;
|
|
let mut primary_failed = false;
|
|
self.paths.retain(|p| {
|
|
if p.borrow_mut().process_timeout(now, pto) {
|
|
true
|
|
} else {
|
|
qdebug!([p.borrow()], "Retiring path");
|
|
if p.borrow().is_primary() {
|
|
primary_failed = true;
|
|
}
|
|
Self::retire(to_retire, p);
|
|
false
|
|
}
|
|
});
|
|
|
|
if primary_failed {
|
|
self.primary = None;
|
|
// Find a valid path to fall back to.
|
|
if let Some(fallback) = self
|
|
.paths
|
|
.iter()
|
|
.rev() // More recent paths are toward the end.
|
|
.find(|p| p.borrow().is_valid())
|
|
{
|
|
// Need a clone as `fallback` is borrowed from `self`.
|
|
let path = Rc::clone(fallback);
|
|
qinfo!([path.borrow()], "Failing over after primary path failed");
|
|
mem::drop(self.select_primary(&path));
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Get when the next call to `process_timeout()` should be scheduled.
|
|
pub fn next_timeout(&self, pto: Duration) -> Option<Instant> {
|
|
self.paths
|
|
.iter()
|
|
.filter_map(|p| p.borrow().next_timeout(pto))
|
|
.min()
|
|
}
|
|
|
|
/// Set the identified path to be primary.
|
|
/// This panics if `make_permanent` hasn't been called.
|
|
pub fn handle_migration(&mut self, path: &PathRef, remote: SocketAddr, now: Instant) {
|
|
qtrace!([self.primary().borrow()], "handle_migration");
|
|
// The update here needs to match the checks in `Path::received_on`.
|
|
// Here, we update the remote port number to match the source port on the
|
|
// datagram that was received. This ensures that we send subsequent
|
|
// packets back to the right place.
|
|
path.borrow_mut().update_port(remote.port());
|
|
|
|
if path.borrow().is_primary() {
|
|
// Update when the path was last regarded as valid.
|
|
path.borrow_mut().update(now);
|
|
return;
|
|
}
|
|
|
|
if let Some(old_path) = self.select_primary(path) {
|
|
// Need to probe the old path if the peer migrates.
|
|
old_path.borrow_mut().probe();
|
|
// TODO(mt) - suppress probing if the path was valid within 3PTO.
|
|
}
|
|
}
|
|
|
|
/// Select a path to send on. This will select the first path that has
|
|
/// probes to send, then fall back to the primary path.
|
|
pub fn select_path(&self) -> Option<PathRef> {
|
|
self.paths
|
|
.iter()
|
|
.find_map(|p| {
|
|
if p.borrow().has_probe() {
|
|
Some(Rc::clone(p))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| self.primary.clone())
|
|
}
|
|
|
|
/// A `PATH_RESPONSE` was received.
|
|
/// Returns `true` if migration occurred.
|
|
#[must_use]
|
|
pub fn path_response(&mut self, response: [u8; 8], now: Instant) -> bool {
|
|
// TODO(mt) consider recording an RTT measurement here as we don't train
|
|
// RTT for non-primary paths.
|
|
for p in &self.paths {
|
|
if p.borrow_mut().path_response(response, now) {
|
|
// The response was accepted. If this path is one we intend
|
|
// to migrate to, then migrate.
|
|
if self
|
|
.migration_target
|
|
.as_ref()
|
|
.map_or(false, |target| Rc::ptr_eq(target, p))
|
|
{
|
|
let primary = self.migration_target.take();
|
|
mem::drop(self.select_primary(&primary.unwrap()));
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Retire all of the connection IDs prior to the indicated sequence number.
|
|
/// Keep active paths if possible by pulling new connection IDs from the provided store.
|
|
/// One slightly non-obvious consequence of this is that if migration is being attempted
|
|
/// and the new path cannot obtain a new connection ID, the migration attempt will fail.
|
|
pub fn retire_cids(&mut self, retire_prior: u64, store: &mut ConnectionIdStore<[u8; 16]>) {
|
|
let to_retire = &mut self.to_retire;
|
|
let migration_target = &mut self.migration_target;
|
|
|
|
// First, tell the store to release any connection IDs that are too old.
|
|
let mut retired = store.retire_prior_to(retire_prior);
|
|
to_retire.append(&mut retired);
|
|
|
|
self.paths.retain(|p| {
|
|
let current = p.borrow().remote_cid.as_ref().unwrap().sequence_number();
|
|
if current < retire_prior {
|
|
to_retire.push(current);
|
|
let new_cid = store.next();
|
|
let has_replacement = new_cid.is_some();
|
|
// There must be a connection ID available for the primary path as we
|
|
// keep that path at the first index.
|
|
debug_assert!(!p.borrow().is_primary() || has_replacement);
|
|
p.borrow_mut().remote_cid = new_cid;
|
|
if !has_replacement
|
|
&& migration_target
|
|
.as_ref()
|
|
.map_or(false, |target| Rc::ptr_eq(target, p))
|
|
{
|
|
qinfo!(
|
|
[p.borrow()],
|
|
"NEW_CONNECTION_ID with Retire Prior To forced migration to fail"
|
|
);
|
|
*migration_target = None;
|
|
}
|
|
has_replacement
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Write out any `RETIRE_CONNECTION_ID` frames that are outstanding.
|
|
pub fn write_frames(
|
|
&mut self,
|
|
builder: &mut PacketBuilder,
|
|
tokens: &mut Vec<RecoveryToken>,
|
|
stats: &mut FrameStats,
|
|
) {
|
|
while let Some(seqno) = self.to_retire.pop() {
|
|
if builder.remaining() < 1 + Encoder::varint_len(seqno) {
|
|
self.to_retire.push(seqno);
|
|
break;
|
|
}
|
|
builder.encode_varint(FRAME_TYPE_RETIRE_CONNECTION_ID);
|
|
builder.encode_varint(seqno);
|
|
tokens.push(RecoveryToken::RetireConnectionId(seqno));
|
|
stats.retire_connection_id += 1;
|
|
}
|
|
|
|
// Write out any ACK_FREQUENCY frames.
|
|
self.primary()
|
|
.borrow_mut()
|
|
.write_cc_frames(builder, tokens, stats);
|
|
}
|
|
|
|
pub fn lost_retire_cid(&mut self, lost: u64) {
|
|
self.to_retire.push(lost);
|
|
}
|
|
|
|
pub fn acked_retire_cid(&mut self, acked: u64) {
|
|
self.to_retire.retain(|&seqno| seqno != acked);
|
|
}
|
|
|
|
pub fn lost_ack_frequency(&mut self, lost: &AckRate) {
|
|
self.primary().borrow_mut().lost_ack_frequency(lost);
|
|
}
|
|
|
|
pub fn acked_ack_frequency(&mut self, acked: &AckRate) {
|
|
self.primary().borrow_mut().acked_ack_frequency(acked);
|
|
}
|
|
|
|
/// Get an estimate of the RTT on the primary path.
|
|
#[cfg(test)]
|
|
pub fn rtt(&self) -> Duration {
|
|
// Rather than have this fail when there is no active path,
|
|
// make a new RTT esimate and interrogate that.
|
|
// That is more expensive, but it should be rare and breaking encapsulation
|
|
// is worse, especially as this is only used in tests.
|
|
self.primary_fallible()
|
|
.map_or(RttEstimate::default().estimate(), |p| {
|
|
p.borrow().rtt().estimate()
|
|
})
|
|
}
|
|
|
|
pub fn set_qlog(&mut self, qlog: NeqoQlog) {
|
|
for p in &mut self.paths {
|
|
p.borrow_mut().set_qlog(qlog.clone());
|
|
}
|
|
self.qlog = qlog;
|
|
}
|
|
}
|
|
|
|
/// The state of a path with respect to address validation.
|
|
#[derive(Debug)]
|
|
enum ProbeState {
|
|
/// The path was last valid at the indicated time.
|
|
Valid,
|
|
/// The path was previously valid, but a new probe is needed.
|
|
ProbeNeeded { probe_count: usize },
|
|
/// The path hasn't been validated, but a probe has been sent.
|
|
Probing {
|
|
/// The number of probes that have been sent.
|
|
probe_count: usize,
|
|
/// The probe that was last sent.
|
|
data: [u8; 8],
|
|
/// Whether the probe was sent in a datagram padded to the path MTU.
|
|
mtu: bool,
|
|
/// When the probe was sent.
|
|
sent: Instant,
|
|
},
|
|
/// Validation failed the last time it was attempted.
|
|
Failed,
|
|
}
|
|
|
|
impl ProbeState {
|
|
/// Determine whether the current state requires probing.
|
|
fn probe_needed(&self) -> bool {
|
|
matches!(self, Self::ProbeNeeded { .. })
|
|
}
|
|
}
|
|
|
|
/// A network path.
|
|
///
|
|
/// Paths are used a little bit strangely by connections:
|
|
/// they need to encapsulate all the state for a path (which
|
|
/// is normal), but that information is not propagated to the
|
|
/// `Paths` instance that holds them. This is because the packet
|
|
/// processing where changes occur can't hold a reference to the
|
|
/// `Paths` instance that owns the `Path`. Any changes to the
|
|
/// path are communicated to `Paths` afterwards.
|
|
#[derive(Debug)]
|
|
pub struct Path {
|
|
/// A local socket address.
|
|
local: SocketAddr,
|
|
/// A remote socket address.
|
|
remote: SocketAddr,
|
|
/// The connection IDs that we use when sending on this path.
|
|
/// This is only needed during the handshake.
|
|
local_cid: Option<ConnectionId>,
|
|
/// The current connection ID that we are using and its details.
|
|
remote_cid: Option<RemoteConnectionIdEntry>,
|
|
|
|
/// Whether this is the primary path.
|
|
primary: bool,
|
|
/// Whether the current path is considered valid.
|
|
state: ProbeState,
|
|
/// For a path that is not validated, this is `None`. For a validated
|
|
/// path, the time that the path was last valid.
|
|
validated: Option<Instant>,
|
|
/// A path challenge was received and `PATH_RESPONSE` has not been sent.
|
|
challenge: Option<[u8; 8]>,
|
|
|
|
/// The round trip time estimate for this path.
|
|
rtt: RttEstimate,
|
|
/// A packet sender for the path, which includes congestion control and a pacer.
|
|
sender: PacketSender,
|
|
/// The DSCP/ECN marking to use for outgoing packets on this path.
|
|
tos: IpTos,
|
|
/// The IP TTL to use for outgoing packets on this path.
|
|
ttl: u8,
|
|
|
|
/// The number of bytes received on this path.
|
|
/// Note that this value might saturate on a long-lived connection,
|
|
/// but we only use it before the path is validated.
|
|
received_bytes: usize,
|
|
/// The number of bytes sent on this path.
|
|
sent_bytes: usize,
|
|
|
|
/// For logging of events.
|
|
qlog: NeqoQlog,
|
|
}
|
|
|
|
impl Path {
|
|
/// Create a path from addresses and a remote connection ID.
|
|
/// This is used for migration and for new datagrams.
|
|
pub fn temporary(
|
|
local: SocketAddr,
|
|
remote: SocketAddr,
|
|
cc: CongestionControlAlgorithm,
|
|
pacing: bool,
|
|
qlog: NeqoQlog,
|
|
now: Instant,
|
|
) -> Self {
|
|
let mut sender = PacketSender::new(cc, pacing, Self::mtu_by_addr(remote.ip()), now);
|
|
sender.set_qlog(qlog.clone());
|
|
Self {
|
|
local,
|
|
remote,
|
|
local_cid: None,
|
|
remote_cid: None,
|
|
primary: false,
|
|
state: ProbeState::ProbeNeeded { probe_count: 0 },
|
|
validated: None,
|
|
challenge: None,
|
|
rtt: RttEstimate::default(),
|
|
sender,
|
|
tos: IpTos::default(), // TODO: Default to Ect0 when ECN is supported.
|
|
ttl: 64, // This is the default TTL on many OSes.
|
|
received_bytes: 0,
|
|
sent_bytes: 0,
|
|
qlog,
|
|
}
|
|
}
|
|
|
|
/// Whether this path is the primary or current path for the connection.
|
|
pub fn is_primary(&self) -> bool {
|
|
self.primary
|
|
}
|
|
|
|
/// Whether this path is a temporary one.
|
|
pub fn is_temporary(&self) -> bool {
|
|
self.remote_cid.is_none()
|
|
}
|
|
|
|
/// By adding a remote connection ID, we make the path permanent
|
|
/// and one that we will later send packets on.
|
|
/// If `local_cid` is `None`, the existing value will be kept.
|
|
pub(crate) fn make_permanent(
|
|
&mut self,
|
|
local_cid: Option<ConnectionId>,
|
|
remote_cid: RemoteConnectionIdEntry,
|
|
) {
|
|
if self.local_cid.is_none() {
|
|
self.local_cid = local_cid;
|
|
}
|
|
self.remote_cid.replace(remote_cid);
|
|
}
|
|
|
|
/// Determine if this path was the one that the provided datagram was received on.
|
|
/// This uses the full local socket address, but ignores the port number on the peer
|
|
/// if `flexible` is true, allowing for NAT rebinding that retains the same IP.
|
|
fn received_on(&self, local: SocketAddr, remote: SocketAddr, flexible: bool) -> bool {
|
|
self.local == local
|
|
&& self.remote.ip() == remote.ip()
|
|
&& (flexible || self.remote.port() == remote.port())
|
|
}
|
|
|
|
/// Update the remote port number. Any flexibility we allow in `received_on`
|
|
/// need to be adjusted at this point.
|
|
fn update_port(&mut self, port: u16) {
|
|
self.remote.set_port(port);
|
|
}
|
|
|
|
/// Set whether this path is primary.
|
|
pub(crate) fn set_primary(&mut self, primary: bool) {
|
|
qtrace!([self], "Make primary {}", primary);
|
|
debug_assert!(self.remote_cid.is_some());
|
|
self.primary = primary;
|
|
if !primary {
|
|
self.sender.discard_in_flight();
|
|
}
|
|
}
|
|
|
|
/// Set the current path as valid. This updates the time that the path was
|
|
/// last validated and cancels any path validation.
|
|
pub fn set_valid(&mut self, now: Instant) {
|
|
qdebug!([self], "Path validated {:?}", now);
|
|
self.state = ProbeState::Valid;
|
|
self.validated = Some(now);
|
|
}
|
|
|
|
/// Update the last use of this path, if it is valid.
|
|
/// This will keep the path active slightly longer.
|
|
pub fn update(&mut self, now: Instant) {
|
|
if self.validated.is_some() {
|
|
self.validated = Some(now);
|
|
}
|
|
}
|
|
|
|
fn mtu_by_addr(addr: IpAddr) -> usize {
|
|
match addr {
|
|
IpAddr::V4(_) => PATH_MTU_V4,
|
|
IpAddr::V6(_) => PATH_MTU_V6,
|
|
}
|
|
}
|
|
|
|
/// Get the path MTU. This is currently fixed based on IP version.
|
|
pub fn mtu(&self) -> usize {
|
|
Self::mtu_by_addr(self.remote.ip())
|
|
}
|
|
|
|
/// Get the first local connection ID.
|
|
/// Only do this for the primary path during the handshake.
|
|
pub fn local_cid(&self) -> &ConnectionId {
|
|
self.local_cid.as_ref().unwrap()
|
|
}
|
|
|
|
/// Set the remote connection ID based on the peer's choice.
|
|
/// This is only valid during the handshake.
|
|
pub fn set_remote_cid(&mut self, cid: ConnectionIdRef) {
|
|
self.remote_cid
|
|
.as_mut()
|
|
.unwrap()
|
|
.update_cid(ConnectionId::from(cid));
|
|
}
|
|
|
|
/// Access the remote connection ID.
|
|
pub fn remote_cid(&self) -> &ConnectionId {
|
|
self.remote_cid.as_ref().unwrap().connection_id()
|
|
}
|
|
|
|
/// Set the stateless reset token for the connection ID that is currently in use.
|
|
/// Panics if the sequence number is non-zero as this is only necessary during
|
|
/// the handshake; all other connection IDs are initialized with a token.
|
|
pub fn set_reset_token(&mut self, token: [u8; 16]) {
|
|
self.remote_cid
|
|
.as_mut()
|
|
.unwrap()
|
|
.set_stateless_reset_token(token);
|
|
}
|
|
|
|
/// Determine if the provided token is a stateless reset token.
|
|
pub fn is_stateless_reset(&self, token: &[u8; 16]) -> bool {
|
|
self.remote_cid
|
|
.as_ref()
|
|
.map_or(false, |rcid| rcid.is_stateless_reset(token))
|
|
}
|
|
|
|
/// Make a datagram.
|
|
pub fn datagram<V: Into<Vec<u8>>>(&self, payload: V) -> Datagram {
|
|
Datagram::new(self.local, self.remote, self.tos, Some(self.ttl), payload)
|
|
}
|
|
|
|
/// Get local address as `SocketAddr`
|
|
pub fn local_address(&self) -> SocketAddr {
|
|
self.local
|
|
}
|
|
|
|
/// Get remote address as `SocketAddr`
|
|
pub fn remote_address(&self) -> SocketAddr {
|
|
self.remote
|
|
}
|
|
|
|
/// Whether the path has been validated.
|
|
pub fn is_valid(&self) -> bool {
|
|
self.validated.is_some()
|
|
}
|
|
|
|
/// Handle a `PATH_RESPONSE` frame. Returns true if the response was accepted.
|
|
pub fn path_response(&mut self, response: [u8; 8], now: Instant) -> bool {
|
|
if let ProbeState::Probing { data, mtu, .. } = &mut self.state {
|
|
if response == *data {
|
|
let need_full_probe = !*mtu;
|
|
self.set_valid(now);
|
|
if need_full_probe {
|
|
qdebug!([self], "Sub-MTU probe successful, reset probe count");
|
|
self.probe();
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The path has been challenged. This generates a response.
|
|
/// This only generates a single response at a time.
|
|
pub fn challenged(&mut self, challenge: [u8; 8]) {
|
|
self.challenge = Some(challenge.to_owned());
|
|
}
|
|
|
|
/// At the next opportunity, send a probe.
|
|
/// If the probe count has been exhausted already, marks the path as failed.
|
|
fn probe(&mut self) {
|
|
let probe_count = match &self.state {
|
|
ProbeState::Probing { probe_count, .. } => *probe_count + 1,
|
|
ProbeState::ProbeNeeded { probe_count, .. } => *probe_count,
|
|
_ => 0,
|
|
};
|
|
self.state = if probe_count >= MAX_PATH_PROBES {
|
|
qinfo!([self], "Probing failed");
|
|
ProbeState::Failed
|
|
} else {
|
|
qdebug!([self], "Initiating probe");
|
|
ProbeState::ProbeNeeded { probe_count }
|
|
};
|
|
}
|
|
|
|
/// Returns true if this path have any probing frames to send.
|
|
pub fn has_probe(&self) -> bool {
|
|
self.challenge.is_some() || self.state.probe_needed()
|
|
}
|
|
|
|
pub fn write_frames(
|
|
&mut self,
|
|
builder: &mut PacketBuilder,
|
|
stats: &mut FrameStats,
|
|
mtu: bool, // Whether the packet we're writing into will be a full MTU.
|
|
now: Instant,
|
|
) -> bool {
|
|
if builder.remaining() < 9 {
|
|
return false;
|
|
}
|
|
|
|
// Send PATH_RESPONSE.
|
|
let resp_sent = if let Some(challenge) = self.challenge.take() {
|
|
qtrace!([self], "Responding to path challenge {}", hex(challenge));
|
|
builder.encode_varint(FRAME_TYPE_PATH_RESPONSE);
|
|
builder.encode(&challenge[..]);
|
|
|
|
// These frames are not retransmitted in the usual fashion.
|
|
// There is no token, therefore we need to count `all` specially.
|
|
stats.path_response += 1;
|
|
stats.all += 1;
|
|
|
|
if builder.remaining() < 9 {
|
|
return true;
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
|
|
// Send PATH_CHALLENGE.
|
|
if let ProbeState::ProbeNeeded { probe_count } = self.state {
|
|
qtrace!([self], "Initiating path challenge {}", probe_count);
|
|
let data = random::<8>();
|
|
builder.encode_varint(FRAME_TYPE_PATH_CHALLENGE);
|
|
builder.encode(&data);
|
|
|
|
// As above, no recovery token.
|
|
stats.path_challenge += 1;
|
|
stats.all += 1;
|
|
|
|
self.state = ProbeState::Probing {
|
|
probe_count,
|
|
data,
|
|
mtu,
|
|
sent: now,
|
|
};
|
|
true
|
|
} else {
|
|
resp_sent
|
|
}
|
|
}
|
|
|
|
/// Write `ACK_FREQUENCY` frames.
|
|
pub fn write_cc_frames(
|
|
&mut self,
|
|
builder: &mut PacketBuilder,
|
|
tokens: &mut Vec<RecoveryToken>,
|
|
stats: &mut FrameStats,
|
|
) {
|
|
self.rtt.write_frames(builder, tokens, stats);
|
|
}
|
|
|
|
pub fn lost_ack_frequency(&mut self, lost: &AckRate) {
|
|
self.rtt.frame_lost(lost);
|
|
}
|
|
|
|
pub fn acked_ack_frequency(&mut self, acked: &AckRate) {
|
|
self.rtt.frame_acked(acked);
|
|
}
|
|
|
|
/// Process a timer for this path.
|
|
/// This returns true if the path is viable and can be kept alive.
|
|
pub fn process_timeout(&mut self, now: Instant, pto: Duration) -> bool {
|
|
if let ProbeState::Probing { sent, .. } = &self.state {
|
|
if now >= *sent + pto {
|
|
self.probe();
|
|
}
|
|
}
|
|
if let ProbeState::Failed = self.state {
|
|
// Retire failed paths immediately.
|
|
false
|
|
} else if self.primary {
|
|
// Keep valid primary paths otherwise.
|
|
true
|
|
} else if let ProbeState::Valid = self.state {
|
|
// Retire validated, non-primary paths.
|
|
// Allow more than `MAX_PATH_PROBES` times the PTO so that an old
|
|
// path remains around until after a previous path fails.
|
|
let count = u32::try_from(MAX_PATH_PROBES + 1).unwrap();
|
|
self.validated.unwrap() + (pto * count) > now
|
|
} else {
|
|
// Keep paths that are being actively probed.
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Return the next time that this path needs servicing.
|
|
/// This only considers retransmissions of probes, not cleanup of the path.
|
|
/// If there is no other activity, then there is no real need to schedule a
|
|
/// timer to cleanup old paths.
|
|
pub fn next_timeout(&self, pto: Duration) -> Option<Instant> {
|
|
if let ProbeState::Probing { sent, .. } = &self.state {
|
|
Some(*sent + pto)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Get the RTT estimator for this path.
|
|
pub fn rtt(&self) -> &RttEstimate {
|
|
&self.rtt
|
|
}
|
|
|
|
/// Mutably borrow the RTT estimator for this path.
|
|
pub fn rtt_mut(&mut self) -> &mut RttEstimate {
|
|
&mut self.rtt
|
|
}
|
|
|
|
/// Read-only access to the owned sender.
|
|
pub fn sender(&self) -> &PacketSender {
|
|
&self.sender
|
|
}
|
|
|
|
/// Pass on RTT configuration: the maximum acknowledgment delay of the peer,
|
|
/// and maybe the minimum delay.
|
|
pub fn set_ack_delay(
|
|
&mut self,
|
|
max_ack_delay: Duration,
|
|
min_ack_delay: Option<Duration>,
|
|
ack_ratio: u8,
|
|
) {
|
|
let ack_delay = min_ack_delay.map_or_else(
|
|
|| PeerAckDelay::fixed(max_ack_delay),
|
|
|m| {
|
|
PeerAckDelay::flexible(
|
|
max_ack_delay,
|
|
m,
|
|
ack_ratio,
|
|
self.sender.cwnd(),
|
|
self.mtu(),
|
|
self.rtt.estimate(),
|
|
)
|
|
},
|
|
);
|
|
self.rtt.set_ack_delay(ack_delay);
|
|
}
|
|
|
|
/// Initialize the RTT for the path based on an existing estimate.
|
|
pub fn prime_rtt(&mut self, rtt: &RttEstimate) {
|
|
self.rtt.prime_rtt(rtt);
|
|
}
|
|
|
|
/// Record received bytes for the path.
|
|
pub fn add_received(&mut self, count: usize) {
|
|
self.received_bytes = self.received_bytes.saturating_add(count);
|
|
}
|
|
|
|
/// Record sent bytes for the path.
|
|
pub fn add_sent(&mut self, count: usize) {
|
|
self.sent_bytes = self.sent_bytes.saturating_add(count);
|
|
}
|
|
|
|
/// Record a packet as having been sent on this path.
|
|
pub fn packet_sent(&mut self, sent: &mut SentPacket) {
|
|
if !self.is_primary() {
|
|
sent.clear_primary_path();
|
|
}
|
|
self.sender.on_packet_sent(sent, self.rtt.estimate());
|
|
}
|
|
|
|
/// Discard a packet that previously might have been in-flight.
|
|
pub fn discard_packet(&mut self, sent: &SentPacket, now: Instant, stats: &mut Stats) {
|
|
if self.rtt.first_sample_time().is_none() {
|
|
// When discarding a packet there might not be a good RTT estimate.
|
|
// But discards only occur after receiving something, so that means
|
|
// that there is some RTT information, which is better than nothing.
|
|
// Two cases: 1. at the client when handling a Retry and
|
|
// 2. at the server when disposing the Initial packet number space.
|
|
qinfo!(
|
|
[self],
|
|
"discarding a packet without an RTT estimate; guessing RTT={:?}",
|
|
now - sent.time_sent
|
|
);
|
|
stats.rtt_init_guess = true;
|
|
self.rtt.update(
|
|
&mut self.qlog,
|
|
now - sent.time_sent,
|
|
Duration::new(0, 0),
|
|
false,
|
|
now,
|
|
);
|
|
}
|
|
|
|
self.sender.discard(sent);
|
|
}
|
|
|
|
/// Record packets as acknowledged with the sender.
|
|
pub fn on_packets_acked(&mut self, acked_pkts: &[SentPacket], now: Instant) {
|
|
debug_assert!(self.is_primary());
|
|
self.sender.on_packets_acked(acked_pkts, &self.rtt, now);
|
|
}
|
|
|
|
/// Record packets as lost with the sender.
|
|
pub fn on_packets_lost(
|
|
&mut self,
|
|
prev_largest_acked_sent: Option<Instant>,
|
|
space: PacketNumberSpace,
|
|
lost_packets: &[SentPacket],
|
|
) {
|
|
debug_assert!(self.is_primary());
|
|
let cwnd_reduced = self.sender.on_packets_lost(
|
|
self.rtt.first_sample_time(),
|
|
prev_largest_acked_sent,
|
|
self.rtt.pto(space), // Important: the base PTO, not adjusted.
|
|
lost_packets,
|
|
);
|
|
if cwnd_reduced {
|
|
self.rtt.update_ack_delay(self.sender.cwnd(), self.mtu());
|
|
}
|
|
}
|
|
|
|
/// Get the number of bytes that can be written to this path.
|
|
pub fn amplification_limit(&self) -> usize {
|
|
if matches!(self.state, ProbeState::Failed) {
|
|
0
|
|
} else if self.is_valid() {
|
|
usize::MAX
|
|
} else {
|
|
self.received_bytes
|
|
.checked_mul(3)
|
|
.map_or(usize::MAX, |limit| {
|
|
let budget = if limit == 0 {
|
|
// If we have received absolutely nothing thus far, then this endpoint
|
|
// is the one initiating communication on this path. Allow enough space for
|
|
// probing.
|
|
self.mtu() * 5
|
|
} else {
|
|
limit
|
|
};
|
|
budget.saturating_sub(self.sent_bytes)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Update the `NeqoQLog` instance.
|
|
pub fn set_qlog(&mut self, qlog: NeqoQlog) {
|
|
self.sender.set_qlog(qlog);
|
|
}
|
|
}
|
|
|
|
impl Display for Path {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
if self.is_primary() {
|
|
write!(f, "pri-")?; // primary
|
|
}
|
|
if !self.is_valid() {
|
|
write!(f, "unv-")?; // unvalidated
|
|
}
|
|
write!(f, "path")?;
|
|
if let Some(entry) = self.remote_cid.as_ref() {
|
|
write!(f, ":{}", entry.connection_id())?;
|
|
}
|
|
write!(f, " {}->{}", self.local, self.remote)?;
|
|
Ok(())
|
|
}
|
|
}
|