From 8ac9cdf5aebd1fdd301fae60b14d459b0bed2f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 17:04:56 +0200 Subject: [PATCH 1/9] Run non-extralight proptests with optimizations enabled --- Cargo.toml | 4 ++++ Makefile.toml | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e235ddec..0dd38c829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,10 @@ debug = true [profile.release] debug = true +[profile.proptests] +inherits = "dev" +opt-level = 3 + [workspace.lints.rust] elided_lifetimes_in_paths = "warn" # https://rust-fuzz.github.io/book/cargo-fuzz/guide.html#cfgfuzzing diff --git a/Makefile.toml b/Makefile.toml index 9fd1fb8d1..c99f464af 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -79,16 +79,16 @@ args = [ [tasks.proptests-long] description = "Run proptests with high case count (runs for ~10 minutes)" command = "cargo" -args = ["nextest", "run", "-P", "proptests", "--no-fail-fast"] +args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--cargo-profile", "proptests", "--no-fail-fast", "${@}"] env = { "PROPTEST_CASES" = "100000" } [tasks.proptests-light] description = "Run proptests for CI (~1 minute)" command = "cargo" -args = ["nextest", "run", "-P", "proptests", "--no-fail-fast"] +args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--cargo-profile", "proptests", "--no-fail-fast", "${@}"] env = { "PROPTEST_CASES" = "10000" } [tasks.proptests-extralight] description = "Run proptests in regression-only mode (runs for <5 seconds)" command = "cargo" -args = ["nextest", "run", "-P", "proptests", "--no-fail-fast"] +args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--no-fail-fast", "${@}"] From 8ff996a617020728421ef6d8c0ffc42adbb0e26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 17:06:12 +0200 Subject: [PATCH 2/9] fix(proto): Correctly generate 1-8 ranges in `ArrayRangeSet` arbitrary instance --- noq-proto/src/frame.rs | 6 ++---- noq-proto/src/range_set/array_range_set.rs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/noq-proto/src/frame.rs b/noq-proto/src/frame.rs index 6aedb886f..29339a882 100644 --- a/noq-proto/src/frame.rs +++ b/noq-proto/src/frame.rs @@ -1005,8 +1005,7 @@ impl proptest::arbitrary::Arbitrary for PathAck { ( any::(), varint_u64(), - any::() - .prop_filter("ranges must be non empty", |ranges| !ranges.is_empty()), + any::(), any::>(), ) .prop_map(|(path_id, delay, ranges, ecn)| Self { @@ -1134,8 +1133,7 @@ impl proptest::arbitrary::Arbitrary for Ack { use proptest::prelude::*; ( varint_u64(), - any::() - .prop_filter("ranges must be non empty", |ranges| !ranges.is_empty()), + any::(), any::>(), ) .prop_map(|(delay, ranges, ecn)| Self { diff --git a/noq-proto/src/range_set/array_range_set.rs b/noq-proto/src/range_set/array_range_set.rs index 268618c6d..9bc6fa93f 100644 --- a/noq-proto/src/range_set/array_range_set.rs +++ b/noq-proto/src/range_set/array_range_set.rs @@ -236,7 +236,7 @@ impl proptest::arbitrary::Arbitrary for ArrayRangeSet { use proptest::prelude::*; // Generate 1-8 ranges. Each range is defined by a gap from the previous and a size. // We use small values to keep encoding reasonable. - prop::collection::vec((1u64..100, 1u64..50), 0..8) + prop::collection::vec((1u64..100, 1u64..50), 1..8) .prop_map(|gaps_and_sizes| { let mut ranges = Self::new(); let mut pos = 0u64; From ca6b59acb35ea5fda2b411dffb4367fcfd29844f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 17:55:40 +0200 Subject: [PATCH 3/9] refactor(proptests): One `PairSetup` to rule all random interaction proptests --- noq-proto/src/tests/proptests.rs | 439 +++++++++++++--------- noq-proto/src/tests/random_interaction.rs | 16 +- 2 files changed, 257 insertions(+), 198 deletions(-) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index b03c28639..89b230aa8 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -12,20 +12,21 @@ use test_strategy::proptest; use tracing::error; use crate::{ - Connection, ConnectionClose, ConnectionError, Event, PathStatus, Side, TransportConfig, - TransportErrorCode, + ClientConfig, Connection, ConnectionClose, ConnectionError, Event, PathStatus, Side, + TransportConfig, TransportErrorCode, tests::{ - Pair, RoutingTable, + Pair, RoutingTable, client_config, random_interaction::{TestOp, run_random_interaction}, server_config, subscribe, }, }; -const MAX_PATHS: u32 = 3; +const MAX_PATHS: u32 = 12; +const MAX_QNT_ADDRS: u8 = 12; const CLIENT_PORT: u16 = 44433; const SERVER_PORT: u16 = 4433; -const CLIENT_ADDRS: [SocketAddr; MAX_PATHS as usize] = [ +const CLIENT_ADDRS: [SocketAddr; 3] = [ SocketAddr::new( IpAddr::V6(Ipv4Addr::new(1, 1, 1, 0).to_ipv6_mapped()), CLIENT_PORT, @@ -39,7 +40,7 @@ const CLIENT_ADDRS: [SocketAddr; MAX_PATHS as usize] = [ CLIENT_PORT, ), ]; -const SERVER_ADDRS: [SocketAddr; MAX_PATHS as usize] = [ +const SERVER_ADDRS: [SocketAddr; 3] = [ SocketAddr::new( IpAddr::V6(Ipv4Addr::new(2, 2, 2, 0).to_ipv6_mapped()), SERVER_PORT, @@ -54,67 +55,104 @@ const SERVER_ADDRS: [SocketAddr; MAX_PATHS as usize] = [ ), ]; -fn setup_deterministic_with_multipath( - seed: [u8; 32], - routes: RoutingTable, - qlog_prefix: &'static str, -) -> Pair { - let mut pair = Pair::seeded(seed); - - let mut cfg = server_config(); - let transport = multipath_transport_config(qlog_prefix); - cfg.transport = Arc::new(transport); - pair.server.endpoint.set_server_config(Some(Arc::new(cfg))); - - pair.client.addr = routes.client_addr(0).unwrap(); - pair.server.addr = routes.server_addr(0).unwrap(); - pair.routes = Some(routes); - pair +#[derive(Debug, test_strategy::Arbitrary)] +struct PairSetup { + seed: Seed, + multipath: bool, + qnt: bool, + routing_setup: RoutingSetup, } -fn multipath_transport_config(qlog_prefix: &'static str) -> TransportConfig { - let mut cfg = TransportConfig::default(); - // enable multipath - cfg.max_concurrent_multipath_paths(MAX_PATHS); - cfg.default_path_max_idle_timeout(Some(std::time::Duration::from_secs(15))); - cfg.default_path_keep_alive_interval(Some(std::time::Duration::from_secs(5))); - // cfg.mtu_discovery_config(None); - #[cfg(feature = "qlog")] - cfg.qlog_from_env(qlog_prefix); - #[cfg(not(feature = "qlog"))] - let _ = qlog_prefix; - cfg +#[derive(Debug, test_strategy::Arbitrary)] +enum RoutingSetup { + None, + SimpleSymmetric, + Complex(#[strategy(routing_table())] RoutingTable), } -#[proptest(cases = 256)] -fn random_interaction( - #[strategy(any::<[u8; 32]>().no_shrink())] seed: [u8; 32], - #[strategy(vec(any::(), 0..100))] interactions: Vec, -) { - let prefix = "random_interaction"; - let mut pair = Pair::seeded(seed); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); +#[derive(Debug, test_strategy::Arbitrary)] +enum Seed { + Zeroes, + Generated(#[strategy(any::<[u8; 32]>().no_shrink())] [u8; 32]), +} - prop_assert!(!pair.drive_bounded(1000), "connection never became idle"); - prop_assert!(allowed_error(poll_to_close( - pair.client_conn_mut(client_ch) - ))); - prop_assert!(allowed_error(poll_to_close( - pair.server_conn_mut(server_ch) - ))); +impl PairSetup { + fn run(self, prefix: &'static str) -> (Pair, ClientConfig) { + let mut pair = Pair::seeded(self.seed.into_slice()); + + // Initialize the transport config + + let mut transport = TransportConfig::default(); + // Set the qlog prefix, if the feature is enabled + #[cfg(feature = "qlog")] + transport.qlog_from_env(prefix); + #[cfg(not(feature = "qlog"))] + let _ = prefix; + + if self.multipath { + // enable multipath + transport.max_concurrent_multipath_paths(MAX_PATHS); + transport.default_path_max_idle_timeout(Some(std::time::Duration::from_secs(15))); + transport.default_path_keep_alive_interval(Some(std::time::Duration::from_secs(5))); + } + + if self.qnt { + // enable QNT: + transport.set_max_remote_nat_traversal_addresses(12); + } + + // Initialize the server config + + let mut server_cfg = server_config(); + server_cfg.transport = Arc::new(transport.clone()); + pair.server + .endpoint + .set_server_config(Some(Arc::new(server_cfg))); + + // Initialize the client config + + let mut client_cfg = client_config(); + client_cfg.transport = Arc::new(transport); + + // Add routing, if enabled + + match self.routing_setup { + RoutingSetup::None => { + pair.routes = None; + } + RoutingSetup::SimpleSymmetric => { + let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); + pair.client.addr = routes.client_addr(0).unwrap(); + pair.server.addr = routes.server_addr(0).unwrap(); + pair.routes = Some(routes); + } + RoutingSetup::Complex(routes) => { + pair.client.addr = routes.client_addr(0).unwrap(); + pair.server.addr = routes.server_addr(0).unwrap(); + pair.routes = Some(routes); + } + } + + (pair, client_cfg) + } +} + +impl Seed { + fn into_slice(&self) -> [u8; 32] { + match self { + Seed::Zeroes => [0u8; 32], + Seed::Generated(generated) => *generated, + } + } } #[proptest(cases = 256)] -fn random_interaction_with_multipath_simple_routing( - #[strategy(any::<[u8; 32]>().no_shrink())] seed: [u8; 32], +fn random_interaction( + setup: PairSetup, #[strategy(vec(any::(), 0..100))] interactions: Vec, ) { - let prefix = "random_interaction_with_multipath_simple_routing"; - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run("random_interaction"); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); prop_assert!(!pair.drive_bounded(1000), "connection never became idle"); prop_assert!(allowed_error(poll_to_close( @@ -158,26 +196,6 @@ fn routing_table() -> impl Strategy { }) } -#[proptest(cases = 256)] -fn random_interaction_with_multipath_complex_routing( - #[strategy(any::<[u8; 32]>().no_shrink())] seed: [u8; 32], - #[strategy(vec(any::(), 0..100))] interactions: Vec, - #[strategy(routing_table())] routes: RoutingTable, -) { - let prefix = "random_interaction_with_multipath_complex_routing"; - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); - - prop_assert!(!pair.drive_bounded(1000), "connection never became idle"); - prop_assert!(allowed_error(poll_to_close( - pair.client_conn_mut(client_ch) - ))); - prop_assert!(allowed_error(poll_to_close( - pair.server_conn_mut(server_ch) - ))); -} - /// All outgoing links go to first destination interface. /// /// Client and server have multiple interfaces, but all outgoing links go to the first @@ -231,10 +249,15 @@ fn poll_to_close(conn: &mut Connection) -> Option { #[test] fn regression_unset_packet_acked() { let prefix = "regression_unset_packet_acked"; - let seed: [u8; 32] = [ - 60, 116, 60, 165, 136, 238, 239, 131, 14, 159, 221, 16, 80, 60, 30, 15, 15, 69, 133, 33, - 89, 203, 28, 107, 123, 117, 6, 54, 215, 244, 47, 1, - ]; + let setup = PairSetup { + seed: Seed::Generated([ + 60, 116, 60, 165, 136, 238, 239, 131, 14, 159, 221, 16, 80, 60, 30, 15, 15, 69, 133, + 33, 89, 203, 28, 107, 123, 117, 6, 54, 215, 244, 47, 1, + ]), + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -253,13 +276,8 @@ fn regression_unset_packet_acked() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - #[allow(unused_mut)] - let mut cfg = TransportConfig::default(); - #[cfg(feature = "qlog")] - cfg.qlog_from_env(prefix); - let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, cfg); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -273,10 +291,15 @@ fn regression_unset_packet_acked() { #[test] fn regression_invalid_key() { let prefix = "regression_invalid_key"; - let seed = [ - 41, 24, 232, 72, 136, 73, 31, 115, 14, 101, 61, 219, 30, 168, 130, 122, 120, 238, 6, 130, - 117, 84, 250, 190, 50, 237, 14, 167, 60, 5, 140, 149, - ]; + let setup = PairSetup { + seed: Seed::Generated([ + 41, 24, 232, 72, 136, 73, 31, 115, 14, 101, 61, 219, 30, 168, 130, 122, 120, 238, 6, + 130, 117, 84, 250, 190, 50, 237, 14, 167, 60, 5, 140, 149, + ]), + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -293,10 +316,8 @@ fn regression_invalid_key() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -321,7 +342,12 @@ fn regression_invalid_key() { #[test] fn regression_invalid_key2() { let prefix = "regression_invalid_key2"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::CloseConn { side: Side::Client, @@ -342,10 +368,8 @@ fn regression_invalid_key2() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -359,10 +383,15 @@ fn regression_invalid_key2() { #[test] fn regression_key_update_error() { let prefix = "regression_key_update_error"; - let seed: [u8; 32] = [ - 68, 93, 15, 237, 88, 31, 93, 255, 246, 51, 203, 224, 20, 124, 107, 163, 143, 43, 193, 187, - 208, 54, 158, 239, 190, 82, 198, 62, 91, 51, 53, 226, - ]; + let setup = PairSetup { + seed: Seed::Generated([ + 68, 93, 15, 237, 88, 31, 93, 255, 246, 51, 203, 224, 20, 124, 107, 163, 143, 43, 193, + 187, 208, 54, 158, 239, 190, 82, 198, 62, 91, 51, 53, 226, + ]), + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -374,10 +403,8 @@ fn regression_key_update_error() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -391,7 +418,12 @@ fn regression_key_update_error() { #[test] fn regression_never_idle() { let prefix = "regression_never_idle"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -411,10 +443,8 @@ fn regression_never_idle() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -428,7 +458,12 @@ fn regression_never_idle() { #[test] fn regression_never_idle2() { let prefix = "regression_never_idle2"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -450,10 +485,8 @@ fn regression_never_idle2() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); // We needed to increase the bounds. It eventually times out. assert!(!pair.drive_bounded(1000), "connection never became idle"); @@ -468,7 +501,12 @@ fn regression_never_idle2() { #[test] fn regression_packet_number_space_missing() { let prefix = "regression_packet_number_space_missing"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -490,10 +528,8 @@ fn regression_packet_number_space_missing() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric([CLIENT_ADDRS[0]], [SERVER_ADDRS[0]]); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -507,7 +543,12 @@ fn regression_packet_number_space_missing() { #[test] fn regression_peer_failed_to_respond_with_path_abandon() { let prefix = "regression_peer_failed_to_respond_with_path_abandon"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(old_routing_table()), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -522,10 +563,8 @@ fn regression_peer_failed_to_respond_with_path_abandon() { ]; let _guard = subscribe(); - let routes = old_routing_table(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -539,7 +578,12 @@ fn regression_peer_failed_to_respond_with_path_abandon() { #[test] fn regression_peer_failed_to_respond_with_path_abandon2() { let prefix = "regression_peer_failed_to_respond_with_path_abandon2"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -564,10 +608,8 @@ fn regression_peer_failed_to_respond_with_path_abandon2() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -612,7 +654,18 @@ fn regression_peer_failed_to_respond_with_path_abandon2() { #[test] fn regression_path_validation() { let prefix = "regression_path_validation"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(RoutingTable::from_routes( + vec![("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0)], + vec![ + ("[::ffff:2.2.2.0]:4433".parse().unwrap(), 0), + ("[::ffff:2.2.2.1]:4433".parse().unwrap(), 0), + ], + )), + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -633,18 +686,10 @@ fn regression_path_validation() { error_code: 0, }, ]; - let routes = RoutingTable::from_routes( - vec![("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0)], - vec![ - ("[::ffff:2.2.2.0]:4433".parse().unwrap(), 0), - ("[::ffff:2.2.2.1]:4433".parse().unwrap(), 0), - ], - ); let _guard = subscribe(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -674,7 +719,12 @@ fn regression_path_validation() { #[test] fn regression_never_idle3() { let prefix = "regression_never_idle3"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::CloseConn { side: Side::Server, @@ -696,10 +746,8 @@ fn regression_never_idle3() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric([CLIENT_ADDRS[0]], [SERVER_ADDRS[0]]); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -713,7 +761,12 @@ fn regression_never_idle3() { #[test] fn regression_frame_encoding_error() { let prefix = "regression_frame_encoding_error"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -733,10 +786,8 @@ fn regression_frame_encoding_error() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -750,7 +801,12 @@ fn regression_frame_encoding_error() { #[test] fn regression_there_should_be_at_least_one_path() { let prefix = "regression_there_should_be_at_least_one_path"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::PassiveMigration { side: Side::Client, @@ -763,10 +819,8 @@ fn regression_there_should_be_at_least_one_path() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric([CLIENT_ADDRS[0]], [SERVER_ADDRS[0]]); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -799,7 +853,12 @@ fn regression_there_should_be_at_least_one_path() { #[test] fn regression_conn_never_idle5() { let prefix = "regression_conn_never_idle5"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::PassiveMigration { side: Side::Server, @@ -813,10 +872,8 @@ fn regression_conn_never_idle5() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric([CLIENT_ADDRS[0]], [SERVER_ADDRS[0]]); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -852,8 +909,12 @@ fn regression_conn_never_idle5() { #[test] fn regression_peer_ignored_path_abandon() { let prefix = "regression_peer_ignored_path_abandon"; - - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -880,10 +941,8 @@ fn regression_peer_ignored_path_abandon() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -920,7 +979,18 @@ fn regression_peer_ignored_path_abandon() { #[test] fn regression_never_idle4() { let prefix = "regression_never_idle4"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::Complex(RoutingTable::from_routes( + vec![ + ("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0), + ("[::ffff:1.1.1.1]:44433".parse().unwrap(), 0), + ], + vec![("[::ffff:2.2.2.0]:4433".parse().unwrap(), 0)], + )), + }; let interactions = vec![ // Open path 1 with the same remote address as path 0 TestOp::OpenPath { @@ -959,18 +1029,10 @@ fn regression_never_idle4() { addr_idx: 0, }, ]; - let routes = RoutingTable::from_routes( - vec![ - ("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0), - ("[::ffff:1.1.1.1]:44433".parse().unwrap(), 0), - ], - vec![("[::ffff:2.2.2.0]:4433".parse().unwrap(), 0)], - ); let _guard = subscribe(); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); assert!(!pair.drive_bounded(1000), "connection never became idle"); assert!(allowed_error(poll_to_close( @@ -1008,7 +1070,12 @@ fn regression_never_idle4() { #[test] fn regression_infinite_loop() { let prefix = "regression_infinite_loop"; - let seed = [0u8; 32]; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: true, + qnt: false, + routing_setup: RoutingSetup::SimpleSymmetric, + }; let interactions = vec![ TestOp::OpenPath { side: Side::Client, @@ -1027,10 +1094,8 @@ fn regression_infinite_loop() { ]; let _guard = subscribe(); - let routes = RoutingTable::simple_symmetric(CLIENT_ADDRS, SERVER_ADDRS); - let mut pair = setup_deterministic_with_multipath(seed, routes, prefix); - let (client_ch, server_ch) = - run_random_interaction(&mut pair, interactions, multipath_transport_config(prefix)); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); // This bug originally occurred at exactly 4540 iterations. // At 4539 it still finishes (but fails the assertion). diff --git a/noq-proto/src/tests/random_interaction.rs b/noq-proto/src/tests/random_interaction.rs index 3c7bb3a9f..b87036811 100644 --- a/noq-proto/src/tests/random_interaction.rs +++ b/noq-proto/src/tests/random_interaction.rs @@ -1,16 +1,12 @@ -use std::{ - net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4}, - sync::Arc, -}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4}; use bytes::Bytes; use test_strategy::Arbitrary; use tracing::{debug, error, info, trace}; use crate::{ - Connection, ConnectionHandle, Dir, FourTuple, PathId, PathStatus, Side, StreamId, - TransportConfig, - tests::{Pair, TestEndpoint, client_config}, + ClientConfig, Connection, ConnectionHandle, Dir, FourTuple, PathId, PathStatus, Side, StreamId, + tests::{Pair, TestEndpoint}, }; #[derive(Debug, Clone, Copy, Arbitrary)] @@ -335,11 +331,9 @@ fn inc_last_addr_octet(addr: SocketAddr) -> SocketAddr { pub(super) fn run_random_interaction( pair: &mut Pair, interactions: Vec, - transport_config: TransportConfig, + client_config: ClientConfig, ) -> (ConnectionHandle, ConnectionHandle) { - let mut client_cfg = client_config(); - client_cfg.transport = Arc::new(transport_config); - let (client_ch, server_ch) = pair.connect_with(client_cfg); + let (client_ch, server_ch) = pair.connect_with(client_config); pair.drive(); // finish establishing the connection; info!("INTERACTION SETUP FINISHED"); let mut client = State::new(Side::Client, client_ch); From 5b10f1000bd118526092eaf7a29bba4b24bdcfda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 19:32:11 +0200 Subject: [PATCH 4/9] Add documentation --- noq-proto/src/tests/proptests.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 89b230aa8..98c2852b3 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -55,6 +55,14 @@ const SERVER_ADDRS: [SocketAddr; 3] = [ ), ]; +/// Struct for generating random pair setups. +/// +/// Compared to randomly generating e.g. the `TransportConfig` on both sides, +/// this has several advantages: +/// - On a proptest failure, it is easy to see which minimal setup the proptest fails with. +/// - The definition of the setup itself is concise, e.g. when copying it into regression tests. +/// - We can be more precise/smaller in the "search space" and have more efficient shrinking, +/// see [`Seed`] or [`RoutingSetup`]. #[derive(Debug, test_strategy::Arbitrary)] struct PairSetup { seed: Seed, @@ -63,16 +71,38 @@ struct PairSetup { routing_setup: RoutingSetup, } +/// Categories of routing setups used for proptests. +/// +/// The advantage of using this is very efficient shrinking: The first attempt at shrinking the +/// routing setup will be to reduce the routing setup to nothing or a simple symmetric one. #[derive(Debug, test_strategy::Arbitrary)] enum RoutingSetup { + /// Set [`Pair::routes`] to `None` None, + /// Use [`RoutingTable::simple_symmetric`] with the default [`CLIENT_ADDRS`] and [`SERVER_ADDRS`]. SimpleSymmetric, + /// Use given generated routing table. Complex(#[strategy(routing_table())] RoutingTable), } +/// Which seed to use in the test setup. +/// +/// This structure has an advantage over a simple `[u8; 32]`, because on one hand, we don't want +/// to waste too much time shrinking the seed itself (reducing individual values inside the array), +/// but also we don't want to disable shrinking altogether: when the seed shrinks to zero, this +/// helps us understand that the seed is likely irrelevant to the test failure. +/// +/// This struct achieves the best of both worlds: If the seed is generated as `Generated(some_seed)`, +/// shrinking will try `Zeroes` once, and if that fails, fall back to using the generated seed +/// and avoid doing any further shrinking of `some_seed`. #[derive(Debug, test_strategy::Arbitrary)] enum Seed { + /// The zero seed. + /// + /// If a test generates the zero seed, then it's likely that the seed doesn't have + /// any effect on the test failure. Zeroes, + /// A specific generated seed. Generated(#[strategy(any::<[u8; 32]>().no_shrink())] [u8; 32]), } From d3cf2acf13bbc202cd7c9a79caef663a83d7c57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 19:45:05 +0200 Subject: [PATCH 5/9] Make clippy happy --- noq-proto/src/tests/proptests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 98c2852b3..5fe4278f9 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -168,10 +168,10 @@ impl PairSetup { } impl Seed { - fn into_slice(&self) -> [u8; 32] { + fn into_slice(self) -> [u8; 32] { match self { - Seed::Zeroes => [0u8; 32], - Seed::Generated(generated) => *generated, + Self::Zeroes => [0u8; 32], + Self::Generated(generated) => generated, } } } From 71b032c2a0ad45f6735d3b5b8d6f5b8b52e7d426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Mon, 13 Apr 2026 19:52:43 +0200 Subject: [PATCH 6/9] Add regression test with failing proptest --- noq-proto/src/tests/proptests.rs | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 5fe4278f9..488cb489f 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -1138,3 +1138,44 @@ fn regression_infinite_loop() { pair.server_conn_mut(server_ch) ))); } + +#[test] +fn regression_qnt_without_multipath() { + let prefix = "regression_qnt_without_multipath"; + let setup = PairSetup { + seed: Seed::Zeroes, + multipath: false, + qnt: true, + routing_setup: RoutingSetup::SimpleSymmetric, + }; + let interactions = vec![ + TestOp::AddHpAddr { + side: Side::Server, + addr_idx: 0, + }, + TestOp::Drive { side: Side::Server }, + TestOp::AdvanceTime, + TestOp::Drive { side: Side::Client }, + TestOp::PassiveMigration { + side: Side::Server, + addr_idx: 0, + }, + TestOp::AddHpAddr { + side: Side::Client, + addr_idx: 0, + }, + TestOp::InitiateHpRound { side: Side::Client }, + ]; + + let _guard = subscribe(); + let (mut pair, client_config) = setup.run(prefix); + let (client_ch, server_ch) = run_random_interaction(&mut pair, interactions, client_config); + + assert!(!pair.drive_bounded(1000), "connection never became idle"); + assert!(allowed_error(poll_to_close( + pair.client_conn_mut(client_ch) + ))); + assert!(allowed_error(poll_to_close( + pair.server_conn_mut(server_ch) + ))); +} From 739264f8a226694b9455d5064381cb588181c658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 14 Apr 2026 10:07:56 +0200 Subject: [PATCH 7/9] fix(proto): Eventually stop resending PATH_CHALLENGEs on revalidations --- noq-proto/src/tests/proptests.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 488cb489f..35ab05cad 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -1139,9 +1139,27 @@ fn regression_infinite_loop() { ))); } +/// This test reproduced a situation in which a QNT-enabled connection sends path challenges indefinitely. +/// +/// In this test setup, we enable QNT, call the required functions for adding addresses to holepunch, +/// and then eventually initiate the first holepunching round. +/// Before that, we also trigger a passive migration on the server side, effectively severing the connection +/// in the server -> client direction on path 0 (the only path at that time), because all packets are +/// rejected on the client side as coming from the wrong address. +/// +/// What follows is that the server sends PATH_CHALLENGEs for path 0 (as that's what we've added as the +/// "holepunching address"), and initiating the holepunching means that we re-use existing paths if we +/// already have one on the required address, but we do *revalidate* them (triggering new PATH_CHALLENGEs). +/// +/// However, in this code path, we didn't have anything that would prevent re-validated path challenges +/// to ever be stopped, so this revalidation would keep the connection busy in the path challenge sent -> +/// path challenge lost -> path challenge sent loop. +/// +/// We fixed this bug by introducing another `OpenState::Revalidating`, and arming the `PathOpenFailed` +/// timer when we start revalidating a path. #[test] -fn regression_qnt_without_multipath() { - let prefix = "regression_qnt_without_multipath"; +fn regression_qnt_revalidating_path_forever() { + let prefix = "regression_qnt_revalidating_path_forever"; let setup = PairSetup { seed: Seed::Zeroes, multipath: false, From f133084bcd67d644179cf01676aadf2af83af1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 14 Apr 2026 10:53:10 +0200 Subject: [PATCH 8/9] Use an `Extensions` enum instead of two bools for extensions --- noq-proto/src/tests/proptests.rs | 81 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 35ab05cad..319180dc5 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -66,11 +66,18 @@ const SERVER_ADDRS: [SocketAddr; 3] = [ #[derive(Debug, test_strategy::Arbitrary)] struct PairSetup { seed: Seed, - multipath: bool, - qnt: bool, + extensions: Extensions, routing_setup: RoutingSetup, } +/// Extensions to enable or not enable in the proptests. +#[derive(Debug, test_strategy::Arbitrary)] +enum Extensions { + None, + MultipathOnly, + QntAndMultipath, +} + /// Categories of routing setups used for proptests. /// /// The advantage of using this is very efficient shrinking: The first attempt at shrinking the @@ -119,14 +126,14 @@ impl PairSetup { #[cfg(not(feature = "qlog"))] let _ = prefix; - if self.multipath { + if self.extensions.is_multipath_enabled() { // enable multipath transport.max_concurrent_multipath_paths(MAX_PATHS); transport.default_path_max_idle_timeout(Some(std::time::Duration::from_secs(15))); transport.default_path_keep_alive_interval(Some(std::time::Duration::from_secs(5))); } - if self.qnt { + if self.extensions.is_qnt_enabled() { // enable QNT: transport.set_max_remote_nat_traversal_addresses(12); } @@ -167,6 +174,16 @@ impl PairSetup { } } +impl Extensions { + fn is_multipath_enabled(&self) -> bool { + matches!(self, Self::MultipathOnly | Self::QntAndMultipath) + } + + fn is_qnt_enabled(&self) -> bool { + matches!(self, Self::QntAndMultipath) + } +} + impl Seed { fn into_slice(self) -> [u8; 32] { match self { @@ -284,8 +301,7 @@ fn regression_unset_packet_acked() { 60, 116, 60, 165, 136, 238, 239, 131, 14, 159, 221, 16, 80, 60, 30, 15, 15, 69, 133, 33, 89, 203, 28, 107, 123, 117, 6, 54, 215, 244, 47, 1, ]), - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -326,8 +342,7 @@ fn regression_invalid_key() { 41, 24, 232, 72, 136, 73, 31, 115, 14, 101, 61, 219, 30, 168, 130, 122, 120, 238, 6, 130, 117, 84, 250, 190, 50, 237, 14, 167, 60, 5, 140, 149, ]), - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -374,8 +389,7 @@ fn regression_invalid_key2() { let prefix = "regression_invalid_key2"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -418,8 +432,7 @@ fn regression_key_update_error() { 68, 93, 15, 237, 88, 31, 93, 255, 246, 51, 203, 224, 20, 124, 107, 163, 143, 43, 193, 187, 208, 54, 158, 239, 190, 82, 198, 62, 91, 51, 53, 226, ]), - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -450,8 +463,7 @@ fn regression_never_idle() { let prefix = "regression_never_idle"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -490,8 +502,7 @@ fn regression_never_idle2() { let prefix = "regression_never_idle2"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -533,8 +544,7 @@ fn regression_packet_number_space_missing() { let prefix = "regression_packet_number_space_missing"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -575,8 +585,7 @@ fn regression_peer_failed_to_respond_with_path_abandon() { let prefix = "regression_peer_failed_to_respond_with_path_abandon"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(old_routing_table()), }; let interactions = vec![ @@ -610,8 +619,7 @@ fn regression_peer_failed_to_respond_with_path_abandon2() { let prefix = "regression_peer_failed_to_respond_with_path_abandon2"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -686,8 +694,7 @@ fn regression_path_validation() { let prefix = "regression_path_validation"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(RoutingTable::from_routes( vec![("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0)], vec![ @@ -751,8 +758,7 @@ fn regression_never_idle3() { let prefix = "regression_never_idle3"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -793,8 +799,7 @@ fn regression_frame_encoding_error() { let prefix = "regression_frame_encoding_error"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -833,8 +838,7 @@ fn regression_there_should_be_at_least_one_path() { let prefix = "regression_there_should_be_at_least_one_path"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -885,8 +889,7 @@ fn regression_conn_never_idle5() { let prefix = "regression_conn_never_idle5"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -941,8 +944,7 @@ fn regression_peer_ignored_path_abandon() { let prefix = "regression_peer_ignored_path_abandon"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -1011,8 +1013,7 @@ fn regression_never_idle4() { let prefix = "regression_never_idle4"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::Complex(RoutingTable::from_routes( vec![ ("[::ffff:1.1.1.0]:44433".parse().unwrap(), 0), @@ -1102,8 +1103,7 @@ fn regression_infinite_loop() { let prefix = "regression_infinite_loop"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: true, - qnt: false, + extensions: Extensions::MultipathOnly, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ @@ -1148,7 +1148,7 @@ fn regression_infinite_loop() { /// rejected on the client side as coming from the wrong address. /// /// What follows is that the server sends PATH_CHALLENGEs for path 0 (as that's what we've added as the -/// "holepunching address"), and initiating the holepunching means that we re-use existing paths if we +/// "holepunching address"), and initiating the holepunching means that we reuse existing paths if we /// already have one on the required address, but we do *revalidate* them (triggering new PATH_CHALLENGEs). /// /// However, in this code path, we didn't have anything that would prevent re-validated path challenges @@ -1162,8 +1162,7 @@ fn regression_qnt_revalidating_path_forever() { let prefix = "regression_qnt_revalidating_path_forever"; let setup = PairSetup { seed: Seed::Zeroes, - multipath: false, - qnt: true, + extensions: Extensions::QntAndMultipath, routing_setup: RoutingSetup::SimpleSymmetric, }; let interactions = vec![ From ae61bb525e3a540ec1e23d57dded5905f4472b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Wed, 15 Apr 2026 08:51:18 +0200 Subject: [PATCH 9/9] cr: Use more constants and improve makefile tasks --- Makefile.toml | 6 +++--- noq-proto/src/tests/proptests.rs | 15 ++++++++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index c99f464af..ea88048d1 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -79,16 +79,16 @@ args = [ [tasks.proptests-long] description = "Run proptests with high case count (runs for ~10 minutes)" command = "cargo" -args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--cargo-profile", "proptests", "--no-fail-fast", "${@}"] +args = ["nextest", "run", "--package=noq-proto", "-P", "proptests", "--cargo-profile=proptests", "--no-fail-fast", "${@}"] env = { "PROPTEST_CASES" = "100000" } [tasks.proptests-light] description = "Run proptests for CI (~1 minute)" command = "cargo" -args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--cargo-profile", "proptests", "--no-fail-fast", "${@}"] +args = ["nextest", "run", "--package=noq-proto", "-P", "proptests", "--cargo-profile=proptests", "--no-fail-fast", "${@}"] env = { "PROPTEST_CASES" = "10000" } [tasks.proptests-extralight] description = "Run proptests in regression-only mode (runs for <5 seconds)" command = "cargo" -args = ["nextest", "run", "--package", "noq-proto", "-P", "proptests", "--no-fail-fast", "${@}"] +args = ["nextest", "run", "--package=noq-proto", "-P", "proptests", "--no-fail-fast", "${@}"] diff --git a/noq-proto/src/tests/proptests.rs b/noq-proto/src/tests/proptests.rs index 319180dc5..3baee3dc2 100644 --- a/noq-proto/src/tests/proptests.rs +++ b/noq-proto/src/tests/proptests.rs @@ -1,6 +1,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, + time::Duration, }; use proptest::{ @@ -21,8 +22,12 @@ use crate::{ }, }; -const MAX_PATHS: u32 = 12; +// These TransportConfig constants are designed to match iroh for now. +const MAX_MULTIPATH_PATHS: u32 = 13; const MAX_QNT_ADDRS: u8 = 12; +const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(15); +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); + const CLIENT_PORT: u16 = 44433; const SERVER_PORT: u16 = 4433; @@ -128,14 +133,14 @@ impl PairSetup { if self.extensions.is_multipath_enabled() { // enable multipath - transport.max_concurrent_multipath_paths(MAX_PATHS); - transport.default_path_max_idle_timeout(Some(std::time::Duration::from_secs(15))); - transport.default_path_keep_alive_interval(Some(std::time::Duration::from_secs(5))); + transport.max_concurrent_multipath_paths(MAX_MULTIPATH_PATHS); + transport.default_path_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)); + transport.default_path_keep_alive_interval(Some(HEARTBEAT_INTERVAL)); } if self.extensions.is_qnt_enabled() { // enable QNT: - transport.set_max_remote_nat_traversal_addresses(12); + transport.set_max_remote_nat_traversal_addresses(MAX_QNT_ADDRS); } // Initialize the server config