Skip to content

Conversation

@matheus23
Copy link
Member

@matheus23 matheus23 commented Dec 16, 2025

Description

The main change of this PR is removing PathData::remote in favor of PathData::addresses which is of type FourTuple instead of SocketAddr.

FourTuple is pretty much this struct:

struct FourTuple {
    remote: SocketAddr,
    local_ip: Option<IpAddr>,
}

(A follow-up should change local_ip to be an Option<SocketAddr> instead of IpAddr.)

We then track path validation by full 4-tuple instead of by remote address. This is more correct, as seen in the regression test in #258.

Breaking Changes

  • quinn_proto::DatagramEventInner now stores a addresses: FourTuple instead of a remote: SocketAddr.
  • quinn_proto::Endpoint::handle now expects a FourTuple instead of SocketAddr.
  • quinn_proto::Connection::open_path and open_path_ensure now take a FourTuple instead of SocketAddr.
  • quinn_proto::Connection::local_ip was removed. Look at the paths instead.

(A follow-up should also update the quinn API to use full FourTuples to make it possible to short-circuit path validation.)

Notes & open questions

The biggest hurdle in this I can see right now has to do with path validation and path migration on the client side:

  • Previously, when the client migrated, the server would send datagrams that came in on the client with a new dst_ip. This IP address was completely ignored though. And the client kept assuming the path is validated, because it didn't see the path "changing". (All while the server triggered a migration and validated the new network path.)
  • Now, we can (and should) detect the path changing and should re-validate the path. (E.g. an on-path attacker might've spoofed the address and sent the packet to another interface of ours. We shouldn't assume we can just use the new interface to send now.)
  • This means we need a full "migration" mechanism on the client side that attempts to send on the new path, but remembers the old path and switches back to it in case the new path fails (exactly as what happens on the server side). This seems to go beyond what RFC 9000 describes though, so I'm unsure what to do here.

Another hurdle is the fact that this means all APIs like open_path_ensure now need to take full 4-tuples, so we can actually distinguish whether we already have a path on that exact network path or not. Again, looking at the remote only is not enough to identify the path!
However, this extends into iroh work as well: Now iroh would need to be aware of which local address to use for which path, which means it needs to do something similar to ICE's address pairing, which is somewhere we don't necessarily want to go.

So. Does this mean we'll stay in this half-broken world where we track path validation by remote address only instead of 4-tuple, or do we fully fix it in the future?
We should probably make sure we know which edge cases we can hit in which we're not properly tracking the path's validity. The tricky thing is that some of these things are only happening with active on-path attackers, which RFC 9000 cares about, but is really hard to reason about/test/detect in the real world.

@matheus23 matheus23 self-assigned this Dec 16, 2025
@github-actions
Copy link

github-actions bot commented Dec 16, 2025

Documentation for this PR has been generated and is available at: https://n0-computer.github.io/quinn/pr/264/docs/iroh_quinn/

Last updated: 2025-12-22T15:00:52Z

@codecov-commenter
Copy link

codecov-commenter commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 73.82812% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.66%. Comparing base (1b96a5f) to head (1c3c87b).

Files with missing lines Patch % Lines
quinn-proto/src/connection/mod.rs 80.24% 32 Missing ⚠️
quinn/src/connection.rs 28.00% 18 Missing ⚠️
quinn-proto/src/connection/qlog.rs 12.50% 7 Missing ⚠️
quinn-proto/src/lib.rs 80.00% 5 Missing ⚠️
quinn-proto/src/connection/paths.rs 83.33% 4 Missing ⚠️
quinn/src/path.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #264   +/-   ##
=======================================
  Coverage   76.66%   76.66%           
=======================================
  Files          83       83           
  Lines       23347    23440   +93     
=======================================
+ Hits        17898    17971   +73     
- Misses       5449     5469   +20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@n0bot n0bot bot added this to iroh Dec 16, 2025
@github-project-automation github-project-automation bot moved this to 🏗 In progress in iroh Dec 16, 2025
Base automatically changed from matheus23/fix-path-validation to main December 16, 2025 14:53
@matheus23 matheus23 marked this pull request as ready for review December 18, 2025 15:47
Copy link
Collaborator

@divagant-martian divagant-martian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very partial review so that this can advance a bit. I'm a fan of the direction, but I can't live with the naming of addresses. Left suggestions

rem_cid: ConnectionId,
remote: SocketAddr,
local_ip: Option<IpAddr>,
addresses: FourTuple,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no a fan of the field name, it's very unespecific. What about the unoriginal foutuple or maybe better, network_path

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love network_path, and thought about it as well.
An original version had tuple: FourTuple, but I changed that to addresses: FourTuple once I saw that that's what the existing code used. (I'm just explaining myself for now reason 🙃 )

Will change it to network_path!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love it! yes, I know quinn used the addresses one but I'm glad we agree on doing it better 😎

/// false)` if was opened.
///
/// [`open_path`]: Connection::open_path
// TODO(matheus23): Adjust docs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this be part of this PR?

match self
.paths
.iter()
.find(|(_id, path)| addresses.is_probably_same_path(&path.data.addresses))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lines like this make think network_path would work well


/// Returns the path's remote socket address
pub fn path_remote_address(&self, path_id: PathId) -> Result<SocketAddr, ClosedPath> {
pub fn path_addresses(&self, path_id: PathId) -> Result<FourTuple, ClosedPath> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs need to be adjusted

}

/// Check if the 4-tuple path (as in RFC9000 Path, not multipath path) had already been validated.
fn find_open_path_on_addresses(&self, addresses: FourTuple) -> Option<(&PathId, &PathState)> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion:

find_path_id_on_network_path or find_network_paths_path_id

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more than the network path. So perhaps find_path_on_network_path. Will change 👍

fn find_open_path_on_addresses(&self, addresses: FourTuple) -> Option<(&PathId, &PathState)> {
self.paths.iter().find(|(path_id, path_state)| {
path_state.data.validated
// Would this use the same network path, if addresses were used to send right now?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

network path 🙌

?remote,
path_remote = ?self.path(path_id).map(|p| p.remote),
%addresses,
%known_path.addresses,
Copy link
Collaborator

@divagant-martian divagant-martian Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the field name of this when logged?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? The log line will look something like this:

... addresses=(::1, [::1]:4433) known_path.addresses=(::1, [::ffff, 1.1.1.1]:4433)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that was the question, I didn't know it was going to end up being known_path.addresses. I'm fairly certain @flub will have opinions here 😅

);
return;
}
if known_path.addresses.local_ip.is_some()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any chance this code can be encapsulated somewhere?, in the path itself partially, maybe?

}
if let Some(token) = params.stateless_reset_token {
let remote = self.path_data(path_id).remote;
// TODO(matheus23): Reset token for a remote, or for a 4-tuple?
Copy link
Collaborator

@divagant-martian divagant-martian Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset tokens are per CID, which is per remote, not network path afaiu. I might be wrong, need to check (part of why review is partial)

path.path_responses.push(number, challenge.0, remote);
if remote == path.remote {
path.path_responses.push(number, challenge.0, addresses);
if addresses == path.addresses {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm I think this is correct, but a bit tricky. Random comment here that simply means I need to check a bit more this for me to feel confident about this, given what we do track

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🏗 In progress

Development

Successfully merging this pull request may close these issues.

4 participants