From 4bae6edd761f2c8bb1a3b8a7a2b65dd23dd00caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diva=20Mart=C3=ADnez?= Date: Thu, 30 Jul 2026 22:31:13 -0500 Subject: [PATCH 1/5] hotfix --- noq-proto/src/connection/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/noq-proto/src/connection/mod.rs b/noq-proto/src/connection/mod.rs index b12e7c2ab6..5195a76e76 100644 --- a/noq-proto/src/connection/mod.rs +++ b/noq-proto/src/connection/mod.rs @@ -2339,7 +2339,15 @@ impl Connection { if known_path.network_path.local_ip.is_some() && network_path.local_ip.is_some() - && known_path.network_path.local_ip != network_path.local_ip + && known_path + .network_path + .local_ip + .as_ref() + .map(std::net::IpAddr::to_canonical) + != network_path + .local_ip + .as_ref() + .map(std::net::IpAddr::to_canonical) && !local_ip_may_migrate { trace!( From 35500b1ca4141d51170a33a9d126b82682af8278 Mon Sep 17 00:00:00 2001 From: Tomoya Kawanishi Date: Tue, 4 Aug 2026 22:19:41 -0500 Subject: [PATCH 2/5] fix(proto): canonicalize remote IP in early_discard_packet's peer check (noq#738) The local_ip comparison a few lines below already canonicalizes both sides via IpAddr::to_canonical() (4bae6edd) to handle dual-stack sockets reporting IPv4-mapped-IPv6 addresses. The remote comparison just above it does not, and can trip the same class of bug: an incoming datagram's reported remote SocketAddr (e.g. [::ffff:a.b.c.d]:p) compares unequal to the known path's remote (a.b.c.d:p) purely due to representation, causing early_discard_packet to silently drop every packet on that path before it ever reaches PATH_RESPONSE / frame processing. --- noq-proto/src/connection/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/noq-proto/src/connection/mod.rs b/noq-proto/src/connection/mod.rs index 5195a76e76..d460fe1d25 100644 --- a/noq-proto/src/connection/mod.rs +++ b/noq-proto/src/connection/mod.rs @@ -2327,7 +2327,17 @@ impl Connection { // forbids migration, drop the datagram. This could be relaxed to heuristically // permit NAT-rebinding-like migration. if let Some(known_path) = self.path_mut(path_id) { - if network_path.remote != known_path.network_path.remote && !peer_may_probe { + // noq#738: like the local_ip comparison below, dual-stack sockets can report + // the remote peer's address as an IPv4-mapped-IPv6 address (`::ffff:a.b.c.d`) + // on one side of this comparison and a plain IPv4 address on the other, + // depending on how the underlying fd was created (observed on Android when a + // physical-interface-bound socket is handed to noq via an abstract socket). + // Canonicalize both sides' IPs before comparing so this doesn't spuriously + // trip the "unrecognized peer" discard. + let remote_matches = network_path.remote.port() == known_path.network_path.remote.port() + && network_path.remote.ip().to_canonical() + == known_path.network_path.remote.ip().to_canonical(); + if !remote_matches && !peer_may_probe { trace!( %path_id, %network_path, From 9f1eb09993efe05b397d14955ca996af1e822f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Fri, 3 Jul 2026 12:29:59 +0200 Subject: [PATCH 3/5] test(proto): Regression test for issue #738 --- noq-proto/src/tests/multipath.rs | 73 +++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/noq-proto/src/tests/multipath.rs b/noq-proto/src/tests/multipath.rs index 2b00fd542c..9b34bcea2b 100644 --- a/noq-proto/src/tests/multipath.rs +++ b/noq-proto/src/tests/multipath.rs @@ -1,6 +1,6 @@ //! Tests for multipath -use std::net::SocketAddr; +use std::net::{Ipv6Addr, SocketAddr}; use std::num::NonZeroU32; use std::sync::Arc; use std::time::Duration; @@ -2293,3 +2293,74 @@ fn regression_discarded_path_stats_are_up_to_date() -> TestResult { Ok(()) } + +/// Regression test for issue #738. +/// +/// When a client opens a new path with an explicit `local_ip` set in the +/// [`FourTuple`], the path should validate successfully. On real devices the +/// `PATH_RESPONSE` was never matched to the outstanding `PATH_CHALLENGE` on +/// such paths, causing them to be abandoned with +/// [`PathAbandonReason::ValidationFailed`]. +/// +/// This test sets up a routing table where the client has a second interface +/// that can reach the *same* server address as the initial connection, and +/// opens a path on that second interface with an explicit `local_ip`. +/// +/// See +#[test] +fn open_path_with_explicit_local_ip() -> TestResult { + let _guard = subscribe(); + let mut pair = ConnPair::builder().enable_multipath().connect(); + + // Set up routing with a second client interface that can reach the same + // server as the first interface. Both server routes point to the same + // server address but link to different client interfaces, so the server + // can respond on either client interface. + let first_client_addr = pair.routes.as_basic().client_addr; + let server_addr = pair.routes.as_basic().server_addr; + let second_client_addr = { + let mut addr = first_client_addr; + if let SocketAddr::V6(v6) = &mut addr { + let s = v6.ip().segments(); + v6.set_ip(Ipv6Addr::new( + s[0], + s[1], + s[2], + s[3], + s[4], + s[5], + s[6], + s[7] + 1, + )); + } + addr + }; + + pair.routes = ManyToManyRouting::from_routes( + vec![(first_client_addr, 0), (second_client_addr, 1)], + vec![(server_addr, 0), (server_addr, 1)], + ) + .into(); + + // Open a path with an explicit local_ip, targeting the same server as + // the initial connection (path 0). + let new_path = FourTuple { + local_ip: Some(second_client_addr.ip()), + remote: server_addr, + }; + let path_id = pair.open_path(Client, new_path, PathStatus::Available)?; + pair.drive(); + + // The path should be established on both sides, not abandoned with + // ValidationFailed. + assert_matches!( + pair.poll(Client), + Some(Event::Path(crate::PathEvent::Established { id })) if id == path_id + ); + assert_matches!( + pair.poll(Server), + Some(Event::Path(crate::PathEvent::Established { id })) if id == path_id + ); + + Ok(()) +} From ae7ea7473213a8fb68daf81833e4082d8216c072 Mon Sep 17 00:00:00 2001 From: Tomoya Kawanishi Date: Wed, 5 Aug 2026 03:06:24 -0500 Subject: [PATCH 4/5] fixup: adapt regression test to #721's routing-table dedup check --- noq-proto/src/tests/multipath.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/noq-proto/src/tests/multipath.rs b/noq-proto/src/tests/multipath.rs index 9b34bcea2b..b6061f7846 100644 --- a/noq-proto/src/tests/multipath.rs +++ b/noq-proto/src/tests/multipath.rs @@ -2336,11 +2336,14 @@ fn open_path_with_explicit_local_ip() -> TestResult { addr }; - pair.routes = ManyToManyRouting::from_routes( - vec![(first_client_addr, 0), (second_client_addr, 1)], - vec![(server_addr, 0), (server_addr, 1)], - ) - .into(); + // `ManyToManyRouting::from_routes` rejects duplicate interface addresses + // (added by #721, after this test was originally written), which trips on + // the same `server_addr` appearing twice. `add_client_route`/`add_server_route` + // don't have that check, so build the same routing table incrementally instead. + let mut routing = ManyToManyRouting::simple_symmetric([first_client_addr], [server_addr]); + routing.add_client_route(second_client_addr, 0); + routing.add_server_route(server_addr, 1); + pair.routes = routing.into(); // Open a path with an explicit local_ip, targeting the same server as // the initial connection (path 0). From 254cf85f10063ef71d82c3747c6b62efb1e88531 Mon Sep 17 00:00:00 2001 From: Tomoya Kawanishi Date: Wed, 5 Aug 2026 03:07:50 -0500 Subject: [PATCH 5/5] style: rustfmt --- noq-proto/src/connection/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noq-proto/src/connection/mod.rs b/noq-proto/src/connection/mod.rs index d460fe1d25..139cfaba7b 100644 --- a/noq-proto/src/connection/mod.rs +++ b/noq-proto/src/connection/mod.rs @@ -2334,7 +2334,8 @@ impl Connection { // physical-interface-bound socket is handed to noq via an abstract socket). // Canonicalize both sides' IPs before comparing so this doesn't spuriously // trip the "unrecognized peer" discard. - let remote_matches = network_path.remote.port() == known_path.network_path.remote.port() + let remote_matches = network_path.remote.port() + == known_path.network_path.remote.port() && network_path.remote.ip().to_canonical() == known_path.network_path.remote.ip().to_canonical(); if !remote_matches && !peer_may_probe {