Skip to content

Commit aea85bb

Browse files
committed
refactor: Track 4-tuples instead of only the remote per path.
1 parent 09fafd0 commit aea85bb

File tree

14 files changed

+399
-255
lines changed

14 files changed

+399
-255
lines changed

quinn-proto/src/connection/mod.rs

Lines changed: 164 additions & 131 deletions
Large diffs are not rendered by default.

quinn-proto/src/connection/paths.rs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use super::{
1111
spaces::{PacketNumberSpace, SentPacket},
1212
};
1313
use crate::{
14-
ConnectionId, Duration, Instant, TIMER_GRANULARITY, TransportConfig, VarInt, coding,
14+
ConnectionId, Duration, FourTuple, Instant, TIMER_GRANULARITY, TransportConfig, VarInt, coding,
1515
congestion, frame::ObservedAddr, packet::SpaceId,
1616
};
1717

@@ -124,14 +124,14 @@ impl PathState {
124124
pub(super) struct SentChallengeInfo {
125125
/// When was the challenge sent on the wire.
126126
pub(super) sent_instant: Instant,
127-
/// The remote to which this path challenge was sent.
128-
pub(super) remote: SocketAddr,
127+
/// The 4-tuple on which this path challenge was sent.
128+
pub(super) addresses: FourTuple,
129129
}
130130

131131
/// Description of a particular network path
132132
#[derive(Debug)]
133133
pub(super) struct PathData {
134-
pub(super) remote: SocketAddr,
134+
pub(super) addresses: FourTuple,
135135
pub(super) rtt: RttEstimator,
136136
/// Whether we're enabling ECN on outgoing packets
137137
pub(super) sending_ecn: bool,
@@ -223,7 +223,7 @@ pub(super) struct PathData {
223223

224224
impl PathData {
225225
pub(super) fn new(
226-
remote: SocketAddr,
226+
addresses: FourTuple,
227227
allow_mtud: bool,
228228
peer_max_udp_payload_size: Option<u16>,
229229
generation: u64,
@@ -235,7 +235,7 @@ impl PathData {
235235
.clone()
236236
.build(now, config.get_initial_mtu());
237237
Self {
238-
remote,
238+
addresses,
239239
rtt: RttEstimator::new(config.initial_rtt),
240240
sending_ecn: true,
241241
pacing: Pacer::new(
@@ -287,15 +287,15 @@ impl PathData {
287287
///
288288
/// This should only be called when migrating paths.
289289
pub(super) fn from_previous(
290-
remote: SocketAddr,
290+
addresses: FourTuple,
291291
prev: &Self,
292292
generation: u64,
293293
now: Instant,
294294
) -> Self {
295295
let congestion = prev.congestion.clone_box();
296296
let smoothed_rtt = prev.rtt.get();
297297
Self {
298-
remote,
298+
addresses,
299299
rtt: prev.rtt,
300300
pacing: Pacer::new(smoothed_rtt, congestion.window(), prev.current_mtu(), now),
301301
sending_ecn: true,
@@ -366,7 +366,7 @@ impl PathData {
366366
self.total_sent = self.total_sent.saturating_add(inc);
367367
if !self.validated {
368368
trace!(
369-
remote = %self.remote,
369+
addresses = %self.addresses,
370370
anti_amplification_budget = %(self.total_recvd * 3).saturating_sub(self.total_sent),
371371
"anti amplification budget decreased"
372372
);
@@ -378,7 +378,7 @@ impl PathData {
378378
self.total_recvd = self.total_recvd.saturating_add(inc);
379379
if !self.validated {
380380
trace!(
381-
remote = %self.remote,
381+
addresses = %self.addresses,
382382
anti_amplification_budget = %(self.total_recvd * 3).saturating_sub(self.total_sent),
383383
"anti amplification budget increased"
384384
);
@@ -638,15 +638,18 @@ pub(crate) struct PathResponses {
638638
}
639639

640640
impl PathResponses {
641-
pub(crate) fn push(&mut self, packet: u64, token: u64, remote: SocketAddr) {
641+
pub(crate) fn push(&mut self, packet: u64, token: u64, addresses: FourTuple) {
642642
/// Arbitrary permissive limit to prevent abuse
643643
const MAX_PATH_RESPONSES: usize = 16;
644644
let response = PathResponse {
645645
packet,
646646
token,
647-
remote,
647+
addresses,
648648
};
649-
let existing = self.pending.iter_mut().find(|x| x.remote == remote);
649+
let existing = self
650+
.pending
651+
.iter_mut()
652+
.find(|x| x.addresses.remote == addresses.remote);
650653
if let Some(existing) = existing {
651654
// Update a queued response
652655
if existing.packet <= packet {
@@ -663,20 +666,20 @@ impl PathResponses {
663666
}
664667
}
665668

666-
pub(crate) fn pop_off_path(&mut self, remote: SocketAddr) -> Option<(u64, SocketAddr)> {
669+
pub(crate) fn pop_off_path(&mut self, addresses: FourTuple) -> Option<(u64, FourTuple)> {
667670
let response = *self.pending.last()?;
668-
if response.remote == remote {
671+
if response.addresses.is_same_path(&addresses) {
669672
// We don't bother searching further because we expect that the on-path response will
670673
// get drained in the immediate future by a call to `pop_on_path`
671674
return None;
672675
}
673676
self.pending.pop();
674-
Some((response.token, response.remote))
677+
Some((response.token, response.addresses))
675678
}
676679

677-
pub(crate) fn pop_on_path(&mut self, remote: SocketAddr) -> Option<u64> {
680+
pub(crate) fn pop_on_path(&mut self, addresses: FourTuple) -> Option<u64> {
678681
let response = *self.pending.last()?;
679-
if response.remote != remote {
682+
if !response.addresses.is_same_path(&addresses) {
680683
// We don't bother searching further because we expect that the off-path response will
681684
// get drained in the immediate future by a call to `pop_off_path`
682685
return None;
@@ -696,8 +699,8 @@ struct PathResponse {
696699
packet: u64,
697700
/// The token of the PATH_CHALLENGE
698701
token: u64,
699-
/// The address the corresponding PATH_CHALLENGE was received from
700-
remote: SocketAddr,
702+
/// The path the corresponding PATH_CHALLENGE was received from
703+
addresses: FourTuple,
701704
}
702705

703706
/// Summary statistics of packets that have been sent on a particular path, but which have not yet

quinn-proto/src/connection/qlog.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use qlog::{
3636
use tracing::warn;
3737

3838
use crate::{
39-
Connection, ConnectionId, Frame, Instant, PathId,
39+
Connection, ConnectionId, FourTuple, Frame, Instant, PathId,
4040
connection::{PathData, SentPacket, timer::Timer},
4141
frame::{EcnCounts, StreamMeta},
4242
packet::{Header, SpaceId},
@@ -104,10 +104,10 @@ impl QlogStream {
104104
self.emit_event_with_tuple_id(event, now, None);
105105
}
106106

107-
fn emit_event_with_tuple_id(&self, event: EventData, now: Instant, tuple: Option<String>) {
107+
fn emit_event_with_tuple_id(&self, event: EventData, now: Instant, addresses: Option<String>) {
108108
// Time will be overwritten by `add_event_with_instant`
109109
let mut event = Event::with_time(0.0, event);
110-
event.tuple = tuple;
110+
event.tuple = addresses;
111111
let mut qlog_streamer = self.0.lock().unwrap();
112112
if let Err(e) = qlog_streamer.add_event_with_instant(event, now) {
113113
warn!("could not emit qlog event: {e}");
@@ -242,7 +242,7 @@ impl QlogSink {
242242
}
243243
}
244244

245-
pub(super) fn emit_tuple_assigned(&self, path_id: PathId, remote: SocketAddr, now: Instant) {
245+
pub(super) fn emit_tuple_assigned(&self, path_id: PathId, tuple: FourTuple, now: Instant) {
246246
#[cfg(feature = "qlog")]
247247
{
248248
let Some(stream) = self.stream.as_ref() else {
@@ -251,10 +251,12 @@ impl QlogSink {
251251
let tuple_id = fmt_tuple_id(path_id.as_u32() as u64);
252252
let event = TupleAssigned {
253253
tuple_id,
254-
tuple_local: None,
254+
tuple_local: tuple
255+
.local_ip
256+
.map(|local_ip| tuple_endpoint_info(Some(local_ip), None, None)),
255257
tuple_remote: Some(tuple_endpoint_info(
256-
Some(remote.ip()),
257-
Some(remote.port()),
258+
Some(tuple.remote.ip()),
259+
Some(tuple.remote.port()),
258260
None,
259261
)),
260262
};

quinn-proto/src/connection/spaces.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use tracing::trace;
1313

1414
use super::{PathId, assembler::Assembler};
1515
use crate::{
16-
Dir, Duration, Instant, SocketAddr, StreamId, TransportError, TransportErrorCode, VarInt,
16+
Dir, Duration, FourTuple, Instant, StreamId, TransportError, TransportErrorCode, VarInt,
1717
connection::StreamsState,
1818
crypto::Keys,
1919
frame::{self, AddAddress, RemoveAddress},
@@ -525,10 +525,10 @@ pub struct Retransmits {
525525
///
526526
/// It is true that a QUIC endpoint will only want to effectively have NEW_TOKEN frames
527527
/// enqueued for its current path at a given point in time. Based on that, we could conceivably
528-
/// change this from a vector to an `Option<(SocketAddr, usize)>` or just a `usize` or
528+
/// change this from a vector to an `Option<(FourTuple, usize)>` or just a `usize` or
529529
/// something. However, due to the architecture of Quinn, it is considerably simpler to not do
530530
/// that; consider what such a change would mean for implementing `BitOrAssign` on Self.
531-
pub(super) new_tokens: Vec<SocketAddr>,
531+
pub(super) new_tokens: Vec<FourTuple>,
532532
/// Paths which need to be abandoned
533533
pub(super) path_abandon: BTreeMap<PathId, TransportErrorCode>,
534534
/// If a [`frame::PathStatusAvailable`] and [`frame::PathStatusBackup`] need to be sent for a path

quinn-proto/src/endpoint.rs

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use thiserror::Error;
1515
use tracing::{debug, error, trace, warn};
1616

1717
use crate::{
18-
Duration, INITIAL_MTU, Instant, MAX_CID_SIZE, MIN_INITIAL_SIZE, PathId, RESET_TOKEN_SIZE,
19-
ResetToken, Side, Transmit, TransportConfig, TransportError,
18+
Duration, FourTuple, INITIAL_MTU, Instant, MAX_CID_SIZE, MIN_INITIAL_SIZE, PathId,
19+
RESET_TOKEN_SIZE, ResetToken, Side, Transmit, TransportConfig, TransportError,
2020
cid_generator::ConnectionIdGenerator,
2121
coding::BufMutExt,
2222
config::{ClientConfig, EndpointConfig, ServerConfig},
@@ -152,8 +152,7 @@ impl Endpoint {
152152
pub fn handle(
153153
&mut self,
154154
now: Instant,
155-
remote: SocketAddr,
156-
local_ip: Option<IpAddr>,
155+
addresses: FourTuple,
157156
ecn: Option<EcnCodepoint>,
158157
data: BytesMut,
159158
buf: &mut Vec<u8>,
@@ -168,7 +167,7 @@ impl Endpoint {
168167
) {
169168
Ok((first_decode, remaining)) => DatagramConnectionEvent {
170169
now,
171-
remote,
170+
addresses,
172171
path_id: PathId::ZERO, // Corrected later for existing paths
173172
ecn,
174173
first_decode,
@@ -200,11 +199,11 @@ impl Endpoint {
200199
buf.write(version);
201200
}
202201
return Some(DatagramEvent::Response(Transmit {
203-
destination: remote,
202+
destination: addresses.remote,
204203
ecn: None,
205204
size: buf.len(),
206205
segment_size: None,
207-
src_ip: local_ip,
206+
src_ip: addresses.local_ip,
208207
}));
209208
}
210209
Err(e) => {
@@ -213,7 +212,6 @@ impl Endpoint {
213212
}
214213
};
215214

216-
let addresses = FourTuple { remote, local_ip };
217215
let dst_cid = event.first_decode.dst_cid();
218216

219217
if let Some(route_to) = self.index.get(&addresses, &event.first_decode) {
@@ -663,7 +661,7 @@ impl Endpoint {
663661

664662
match conn.handle_first_packet(
665663
incoming.received_at,
666-
incoming.addresses.remote,
664+
incoming.addresses,
667665
incoming.ecn,
668666
packet_number,
669667
incoming.packet,
@@ -848,8 +846,7 @@ impl Endpoint {
848846
init_cid,
849847
loc_cid,
850848
rem_cid,
851-
addresses.remote,
852-
addresses.local_ip,
849+
addresses,
853850
tls,
854851
self.local_cid_generator.as_ref(),
855852
now,
@@ -1023,7 +1020,7 @@ struct ConnectionIndex {
10231020
/// Identifies outgoing connections with zero-length CIDs
10241021
///
10251022
/// We don't yet support explicit source addresses for client connections, and zero-length CIDs
1026-
/// require a unique four-tuple, so at most one client connection with zero-length local CIDs
1023+
/// require a unique 4-tuple, so at most one client connection with zero-length local CIDs
10271024
/// may be established per remote. We must omit the local address from the key because we don't
10281025
/// necessarily know what address we're sending from, and hence receiving at.
10291026
///
@@ -1386,14 +1383,3 @@ impl ResetTokenTable {
13861383
self.0.get(&remote)?.get(&token)
13871384
}
13881385
}
1389-
1390-
/// Identifies a connection by the combination of remote and local addresses
1391-
///
1392-
/// Including the local ensures good behavior when the host has multiple IP addresses on the same
1393-
/// subnet and zero-length connection IDs are in use.
1394-
#[derive(Hash, Eq, PartialEq, Debug, Copy, Clone)]
1395-
struct FourTuple {
1396-
remote: SocketAddr,
1397-
// A single socket can only listen on a single port, so no need to store it explicitly
1398-
local_ip: Option<IpAddr>,
1399-
}

quinn-proto/src/lib.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,69 @@ const MAX_UDP_PAYLOAD: u16 = 65527;
338338
const TIMER_GRANULARITY: Duration = Duration::from_millis(1);
339339
/// Maximum number of streams that can be uniquely identified by a stream ID
340340
const MAX_STREAM_COUNT: u64 = 1 << 60;
341+
342+
/// Identifies a network path by the combination of remote and local addresses
343+
///
344+
/// Including the local ensures good behavior when the host has multiple IP addresses on the same
345+
/// subnet and zero-length connection IDs are in use or when multipath is enabled and multiple
346+
/// paths exist with the same remote, but different local IP interfaces.
347+
#[derive(Hash, Eq, PartialEq, Copy, Clone)]
348+
pub struct FourTuple {
349+
/// The remote side of this tuple
350+
pub remote: SocketAddr,
351+
/// The local side of this tuple.
352+
// A single socket can only listen on a single port, so no need to store it explicitly
353+
// TODO(matheus23): add back the port. The comment above is wrong. There *can* be multiple sockets!
354+
// Even ignoring multiple sockets behind a single abstract `AsyncUdpSocket`, there can
355+
// be multiple `AsyncUdpSocket`s when you use `rebind`!
356+
pub local_ip: Option<IpAddr>,
357+
}
358+
359+
impl FourTuple {
360+
/// Returns whether we know that these two tuples represent the same path.
361+
///
362+
/// If any of these tuples doesn't have a local address, then we cannot know
363+
/// if the two tuples represent the same path, even if the remote is the same
364+
/// address, because the local address might be different.
365+
// TODO(matheus23): Should this be the `PartialEq` definition on FourTuple? It might be too hidden.
366+
pub fn is_same_path(&self, other: &Self) -> bool {
367+
self.local_ip.is_some() && other.local_ip.is_some() && self == other
368+
}
369+
370+
/// Updates this tuple's local address iff
371+
/// - it was unset before,
372+
/// - the other tuple has the same remote, and
373+
/// - the other tuple has a local address set.
374+
///
375+
/// Returns whether this and the other remote are now fully equal.
376+
pub fn update_local_if_same_remote(&mut self, other: &Self) -> bool {
377+
if self.remote != other.remote {
378+
return false;
379+
}
380+
if self.local_ip.is_some() && self.local_ip != other.local_ip {
381+
return false;
382+
}
383+
self.local_ip = other.local_ip;
384+
true
385+
}
386+
}
387+
388+
impl fmt::Display for FourTuple {
389+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390+
f.write_str("(")?;
391+
if let Some(local_ip) = &self.local_ip {
392+
local_ip.fmt(f)?;
393+
f.write_str(":<port>, ")?;
394+
} else {
395+
f.write_str("<unknown>:<port>, ")?;
396+
}
397+
self.remote.fmt(f)?;
398+
f.write_str(")")
399+
}
400+
}
401+
402+
impl fmt::Debug for FourTuple {
403+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404+
fmt::Display::fmt(&self, f)
405+
}
406+
}

quinn-proto/src/shared.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{fmt, net::SocketAddr};
22

33
use bytes::{Buf, BufMut, BytesMut};
44

5+
use crate::FourTuple;
56
use crate::PathId;
67
use crate::{Duration, Instant, MAX_CID_SIZE, ResetToken, coding::BufExt, packet::PartialDecode};
78

@@ -21,7 +22,7 @@ pub(crate) enum ConnectionEventInner {
2122
#[derive(Debug)]
2223
pub(crate) struct DatagramConnectionEvent {
2324
pub(crate) now: Instant,
24-
pub(crate) remote: SocketAddr,
25+
pub(crate) addresses: FourTuple,
2526
pub(crate) path_id: PathId,
2627
pub(crate) ecn: Option<EcnCodepoint>,
2728
pub(crate) first_decode: PartialDecode,

0 commit comments

Comments
 (0)