cloudflare / cloudflare/quiche
PMTUD state is consumed by wrong path when sending on probing path
- Dominant language
- Rust
- Stars
- 11.8k
- Forks
- 1.1k
- Avg merge
- 21h 9m
- Merged PRs (30d)
- 6
Description
## Summary
When a connection has an active path (`old_path`) with a pending PMTUD probe, sending a packet on a different path being validated (`new_path`) can incorrectly consume the PMTUD state of `old_path`.
The `new_path` is not active yet. It is only selected as the send path because the connection needs to transmit `PATH_RESPONSE` frame.
As a result, the PMTUD probe on `old_path` is marked as in flight even though no PMTUD probe was sent on that path.
## Scenario
1. `old_path` is the current active path.
2. PMTU revalidation is requested on `old_path`.
3. `old_path` now has a pending PMTUD probe.
4. The client starts validating `new_path` and sends a
`PATH_CHALLENGE` frame on it.
5. The server receives the `PATH_CHALLENGE`, creates `new_path`, and
queues a `PATH_RESPONSE` frame.
6. The next server packet is sent on `new_path`.
7. The send logic incorrectly updates the PMTUD state associated with
`old_path`.
## Regression test
```rust
#[test]
fn sending_on_probing_path_does_not_consume_active_path_pmtud_probe() {
const PROBE_SIZE: usize = 1400;
let mut config = Config::new(PROTOCOL_VERSION).unwrap();
config
.load_cert_chain_from_pem_file("examples/cert.crt")
.unwrap();
config
.load_priv_key_from_pem_file("examples/cert.key")
.unwrap();
config.set_application_protos(&[b"proto1"]).unwrap();
config.verify_peer(false);
config.set_active_connection_id_limit(2);
config.set_max_send_udp_payload_size(PROBE_SIZE);
config.discover_pmtu(true);
let mut pipe = pipe_with_exchanged_cids(&mut config, 16, 16, 1);
// `old_path` is the current active path.
let old_path_id = pipe.server.paths.get_active_path_id().unwrap();
// PMTU revalidation is requested on `old_path`.
pipe.server.revalidate_pmtu();
// `old_path` now has a pending PMTUD probe.
assert!(
pipe.server
.paths
.get(old_path_id)
.unwrap()
.pmtud
.as_ref()
.unwrap()
.should_probe(),
"`old_path` should have a pending PMTUD probe"
);
let server_addr = test_utils::Pipe::server_addr();
let new_client_addr = "127.0.0.1:5678".parse().unwrap();
let mut out = vec![0; PROBE_SIZE];
// The client starts validating `new_path`.
pipe.client
.probe_path(new_client_addr, server_addr)
.unwrap();
// The client sends a PATH_CHALLENGE frame on `new_path`.
let (written, info) = pipe
.client
.send_on_path(
&mut out,
Some(new_client_addr),
Some(server_addr),
)
.unwrap();
assert_eq!(info.from, new_client_addr);
assert_eq!(info.to, server_addr);
// The server receives the PATH_CHALLENGE, creates `new_path`, and
// queues a PATH_RESPONSE frame.
pipe.server
.recv(
&mut out[..written],
RecvInfo {
from: info.from,
to: info.to,
},
)
.unwrap();
// `new_path` is being validated, but `old_path` remains active.
assert_eq!(
pipe.server.paths.get_active_path_id().unwrap(),
old_path_id
);
// The next server packet is sent on `new_path`.
let (_, info) = pipe.server.send(&mut out).unwrap();
assert_eq!(info.from, server_addr);
assert_eq!(info.to, new_client_addr);
// Sending on `new_path` must not consume the pending PMTUD probe
// associated with `old_path`.
assert!(
pipe.server
.paths
.get(old_path_id)
.unwrap()
.pmtud
.as_ref()
.unwrap()
.should_probe(),
"sending on `new_path` consumed `old_path`'s PMTUD probe"
);
}
```
Contributor guide
Research direction
Start by locating the regression test sending_on_probing_path_does_not_consume_active_path_pmtud_probe and trace the send(), send_on_path(), and PMTUD state updates it exercises. Reproduce the scenario with the test, then verify that sending PATH_RESPONSE on new_path leaves old_path's pending PMTUD probe unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100