Skip to content

Commit 1fdbf69

Browse files
authored
feat(quinn-proto): Use RTT from path challenges, implement path challenge resending (#178)
- Adds a timer to resend another path challenge if the old one was lost and the path isn't validated. - The resent challenges now have different tokens. We keep track of all of them to verify incoming path responses. - The logic for when to send a path challenge is simplified to only send challenges when `send_new_challenge` is set to `true` (instead of triggering *always* when we're in path validation mode)
2 parents 7a094bd + 9f60bb7 commit 1fdbf69

File tree

3 files changed

+117
-74
lines changed

3 files changed

+117
-74
lines changed

quinn-proto/src/connection/mod.rs

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1591,6 +1591,8 @@ impl Connection {
15911591
path_id: PathId,
15921592
) -> Option<Transmit> {
15931593
let (prev_cid, prev_path) = self.paths.get_mut(&path_id)?.prev.as_mut()?;
1594+
// TODO (matheus23): We could use !prev_path.is_validating() here instead to
1595+
// (possibly) also re-send challenges when they get lost.
15941596
if !prev_path.send_new_challenge {
15951597
return None;
15961598
};
@@ -1843,13 +1845,22 @@ impl Connection {
18431845
let Some(path) = self.paths.get_mut(&path_id) else {
18441846
continue;
18451847
};
1848+
self.timers
1849+
.stop(Timer::PerPath(path_id, PathTimer::PathChallengeLost));
18461850
debug!("path validation failed");
18471851
if let Some((_, prev)) = path.prev.take() {
18481852
path.data = prev;
18491853
}
18501854
path.data.challenges_sent.clear();
18511855
path.data.send_new_challenge = false;
18521856
}
1857+
PathTimer::PathChallengeLost => {
1858+
let Some(path) = self.paths.get_mut(&path_id) else {
1859+
continue;
1860+
};
1861+
trace!("path challenge deemed lost");
1862+
path.data.send_new_challenge = true;
1863+
}
18531864
PathTimer::PathOpen => {
18541865
let Some(path) = self.path_mut(path_id) else {
18551866
continue;
@@ -4037,6 +4048,8 @@ impl Connection {
40374048
} else if let Some(&challenge_sent) = path.data.challenges_sent.get(&token) {
40384049
self.timers
40394050
.stop(Timer::PerPath(path_id, PathTimer::PathValidation));
4051+
self.timers
4052+
.stop(Timer::PerPath(path_id, PathTimer::PathChallengeLost));
40404053
if !path.data.validated {
40414054
trace!("new path validated");
40424055
}
@@ -4663,7 +4676,7 @@ impl Connection {
46634676

46644677
let mut prev = mem::replace(path, new_path);
46654678
// Don't clobber the original path if the previous one hasn't been validated yet
4666-
if !prev.challenges_sent.is_empty() {
4679+
if !prev.is_validating_path() {
46674680
prev.send_new_challenge = true;
46684681
// We haven't updated the remote CID yet, this captures the remote CID we were using on
46694682
// the previous path.
@@ -4925,48 +4938,48 @@ impl Connection {
49254938
}
49264939

49274940
// PATH_CHALLENGE
4928-
if buf.remaining_mut() > 9 && space_id == SpaceId::Data {
4929-
// Transmit challenges with every outgoing packet on an unvalidated path
4930-
if path.is_validating_path() {
4931-
// Generate a new challenge every time we send a new PATH_CHALLENGE
4932-
let token = self.rng.random();
4933-
path.challenges_sent.insert(token, now);
4934-
sent.non_retransmits = true;
4935-
sent.requires_padding = true;
4936-
trace!("PATH_CHALLENGE {:08x}", token);
4937-
buf.write(frame::FrameType::PATH_CHALLENGE);
4938-
buf.write(token);
4939-
self.stats.frame_tx.path_challenge += 1;
4941+
if buf.remaining_mut() > 9 && space_id == SpaceId::Data && path.send_new_challenge {
4942+
path.send_new_challenge = false;
49404943

4941-
if is_multipath_negotiated && !path.validated && path.send_new_challenge {
4942-
// queue informing the path status along with the challenge
4943-
space.pending.path_status.insert(path_id);
4944-
}
4944+
// Generate a new challenge every time we send a new PATH_CHALLENGE
4945+
let token = self.rng.random();
4946+
path.challenges_sent.insert(token, now);
4947+
sent.non_retransmits = true;
4948+
sent.requires_padding = true;
4949+
trace!("PATH_CHALLENGE {:08x}", token);
4950+
buf.write(frame::FrameType::PATH_CHALLENGE);
4951+
buf.write(token);
4952+
self.stats.frame_tx.path_challenge += 1;
4953+
let pto = self.ack_frequency.max_ack_delay_for_pto() + path.rtt.pto_base();
4954+
self.timers.set(
4955+
Timer::PerPath(path_id, PathTimer::PathChallengeLost),
4956+
now + pto,
4957+
);
49454958

4946-
// But only send a packet solely for that purpose at most once
4947-
path.send_new_challenge = false;
4959+
if is_multipath_negotiated && !path.validated && path.send_new_challenge {
4960+
// queue informing the path status along with the challenge
4961+
space.pending.path_status.insert(path_id);
4962+
}
49484963

4949-
// Always include an OBSERVED_ADDR frame with a PATH_CHALLENGE, regardless
4950-
// of whether one has already been sent on this path.
4951-
if space_id == SpaceId::Data
4952-
&& self
4953-
.config
4954-
.address_discovery_role
4955-
.should_report(&self.peer_params.address_discovery_role)
4956-
{
4957-
let frame =
4958-
frame::ObservedAddr::new(path.remote, self.next_observed_addr_seq_no);
4959-
if buf.remaining_mut() > frame.size() {
4960-
frame.write(buf);
4964+
// Always include an OBSERVED_ADDR frame with a PATH_CHALLENGE, regardless
4965+
// of whether one has already been sent on this path.
4966+
if space_id == SpaceId::Data
4967+
&& self
4968+
.config
4969+
.address_discovery_role
4970+
.should_report(&self.peer_params.address_discovery_role)
4971+
{
4972+
let frame = frame::ObservedAddr::new(path.remote, self.next_observed_addr_seq_no);
4973+
if buf.remaining_mut() > frame.size() {
4974+
frame.write(buf);
49614975

4962-
self.next_observed_addr_seq_no =
4963-
self.next_observed_addr_seq_no.saturating_add(1u8);
4964-
path.observed_addr_sent = true;
4976+
self.next_observed_addr_seq_no =
4977+
self.next_observed_addr_seq_no.saturating_add(1u8);
4978+
path.observed_addr_sent = true;
49654979

4966-
self.stats.frame_tx.observed_addr += 1;
4967-
sent.retransmits.get_or_create().observed_addr = true;
4968-
space.pending.observed_addr = false;
4969-
}
4980+
self.stats.frame_tx.observed_addr += 1;
4981+
sent.retransmits.get_or_create().observed_addr = true;
4982+
space.pending.observed_addr = false;
49704983
}
49714984
}
49724985
}
@@ -5747,6 +5760,14 @@ impl Connection {
57475760
self.path_data(PathId::ZERO).current_mtu()
57485761
}
57495762

5763+
/// Triggers path validation on all paths
5764+
#[cfg(test)]
5765+
pub(crate) fn trigger_path_validation(&mut self) {
5766+
for path in self.paths.values_mut() {
5767+
path.data.send_new_challenge = true;
5768+
}
5769+
}
5770+
57505771
/// Whether we have 1-RTT data to send
57515772
///
57525773
/// This checks for frames that can only be sent in the data space (1-RTT):

quinn-proto/src/connection/timer.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,28 @@ pub(crate) enum PathTimer {
4444
PathIdle = 1,
4545
/// When to give up on validating a new path from RFC9000 migration
4646
PathValidation = 2,
47+
/// When to resend a path challenge deemed lost
48+
PathChallengeLost = 3,
4749
/// When to give up on validating a new (multi)path
48-
PathOpen = 3,
50+
PathOpen = 4,
4951
/// When to send a `PING` frame to keep the path alive
50-
PathKeepAlive = 4,
52+
PathKeepAlive = 5,
5153
/// When pacing will allow us to send a packet
52-
Pacing = 5,
54+
Pacing = 6,
5355
/// When to send an immediate ACK if there are unacked ack-eliciting packets of the peer
54-
MaxAckDelay = 6,
56+
MaxAckDelay = 7,
5557
/// When to clean up state for an abandoned path
56-
PathAbandoned = 7,
58+
PathAbandoned = 8,
5759
/// When the peer fails to confirm abandoning the path
58-
PathNotAbandoned = 8,
60+
PathNotAbandoned = 9,
5961
}
6062

6163
impl PathTimer {
62-
const VALUES: [Self; 9] = [
64+
const VALUES: [Self; 10] = [
6365
Self::LossDetection,
6466
Self::PathIdle,
6567
Self::PathValidation,
68+
Self::PathChallengeLost,
6669
Self::PathOpen,
6770
Self::PathKeepAlive,
6871
Self::Pacing,

quinn-proto/src/tests/mod.rs

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,43 +1346,62 @@ fn path_challenge_retransmit() {
13461346
let (client_ch, server_ch) = pair.connect();
13471347
pair.drive();
13481348

1349-
let challenges_sent_before = pair
1350-
.server_conn_mut(server_ch)
1351-
.stats()
1352-
.frame_tx
1353-
.path_challenge;
1354-
1355-
println!("-------- client migrates --------");
1356-
pair.client.addr = SocketAddr::new(
1357-
Ipv4Addr::new(127, 0, 0, 1).into(),
1358-
CLIENT_PORTS.lock().unwrap().next().unwrap(),
1359-
);
1360-
// Send more than a ping to make sure we have enough anti-amplification budget to resend
1361-
let stream_id = pair.client_streams(client_ch).open(Dir::Uni).unwrap();
1362-
let to_write = [0u8; 1000];
1363-
let mut written = 0;
1364-
while written < 1000 {
1365-
written += pair
1366-
.client_conn_mut(client_ch)
1367-
.send_stream(stream_id)
1368-
.write(&to_write[written..])
1369-
.unwrap();
1370-
}
1349+
pair.client_conn_mut(client_ch).ping();
1350+
pair.drive();
13711351

1372-
pair.drive_client(); // This will send the stream datagram
1373-
pair.drive_server(); // This will make the server receive the stream datagram, increase its anti-amp budget, and send the first path challenge
1352+
println!("-------- server wants path validation --------");
1353+
pair.server_conn_mut(server_ch).trigger_path_validation();
1354+
pair.drive_server(); // Send the path challenge
13741355
println!("-------- client loses messages --------");
13751356
// Have the client lose the challenge
13761357
pair.client.inbound.clear();
13771358

13781359
pair.drive();
13791360

1361+
let client_tx = pair.client_conn_mut(client_ch).stats().frame_tx;
1362+
let server_tx = pair.server_conn_mut(server_ch).stats().frame_tx;
1363+
13801364
assert_eq!(
1381-
pair.server_conn_mut(server_ch)
1382-
.stats()
1383-
.frame_tx
1384-
.path_challenge,
1385-
challenges_sent_before + 2
1365+
server_tx.path_challenge, 2,
1366+
"expected server to send two path challenges"
1367+
);
1368+
assert_eq!(
1369+
client_tx.path_response, 1,
1370+
"expected client to send one path response"
1371+
);
1372+
}
1373+
1374+
#[test]
1375+
fn path_response_retransmit() {
1376+
let _guard = subscribe();
1377+
let mut pair = Pair::default();
1378+
let (client_ch, server_ch) = pair.connect();
1379+
pair.drive();
1380+
1381+
pair.client_conn_mut(client_ch).ping();
1382+
pair.drive();
1383+
1384+
println!("-------- server wants path validation --------");
1385+
pair.server_conn_mut(server_ch).trigger_path_validation();
1386+
pair.drive_server(); // Send the path challenge
1387+
pair.drive_client(); // Send the path response
1388+
println!("-------- server loses messages --------");
1389+
// Have the server lose the path response
1390+
pair.server.inbound.clear();
1391+
1392+
// The server should decide to re-send the path challenge
1393+
pair.drive();
1394+
1395+
let client_tx = pair.client_conn_mut(client_ch).stats().frame_tx;
1396+
let server_tx = pair.server_conn_mut(server_ch).stats().frame_tx;
1397+
1398+
assert_eq!(
1399+
server_tx.path_challenge, 2,
1400+
"expected server to send two path challenges"
1401+
);
1402+
assert_eq!(
1403+
client_tx.path_response, 2,
1404+
"expected client to send two path responses"
13861405
);
13871406
}
13881407

0 commit comments

Comments
 (0)