Skip to content

Commit 479303f

Browse files
rklaehnclaude
andcommitted
test(iroh): reproduce #4114 invalid retry token with multi-homed peers
Manual retry-then-validate accept loop (no Router) with both peers bound to the wildcard address; the client dials all of the server's discovered addresses. On a multi-homed host the client sprays the Initial from several source IPs, so a retry token minted for one source can be answered from another -> INVALID_TOKEN. Runs 10 handshakes and asserts none fail. Reproduces on this April 2026 state (iroh 0.98.1, noq 0.18). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 42d6983 commit 479303f

1 file changed

Lines changed: 88 additions & 0 deletions

File tree

iroh/src/protocol.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,94 @@ mod tests {
10431043
Ok(())
10441044
}
10451045

1046+
/// Reproduces <https://github.com/n0-computer/iroh/issues/4114> with a
1047+
/// manual retry-then-validate accept loop (no Router / incoming filter).
1048+
///
1049+
/// Both peers bind the wildcard address (every interface, dual-stack);
1050+
/// the client dials all of the server's discovered addresses. On a
1051+
/// multi-homed host the client sprays from several source IPs, so a retry
1052+
/// token minted for one source can be answered from another →
1053+
/// `INVALID_TOKEN`. Runs many handshakes and reports the failure rate.
1054+
///
1055+
/// On this (April 2026) code we expect failures > 0 on a multi-homed box.
1056+
#[tokio::test]
1057+
#[traced_test]
1058+
async fn addr_retry_multihomed() -> Result {
1059+
use crate::endpoint::IncomingAddr;
1060+
1061+
const ITERS: usize = 10;
1062+
let mut failures = 0usize;
1063+
let mut first_err: Option<String> = None;
1064+
let mut ip_count = 0usize;
1065+
let mut max_client_sources = 0usize;
1066+
let mut multi_source_iters = 0usize;
1067+
1068+
for i in 0..ITERS {
1069+
let server = Endpoint::builder(presets::Minimal)
1070+
.alpns(vec![ECHO_ALPN.to_vec()])
1071+
.bind()
1072+
.await?;
1073+
let client = Endpoint::builder(presets::Minimal).bind().await?;
1074+
1075+
let addr = server.addr();
1076+
ip_count = addr.addrs.iter().filter(|a| a.is_ip()).count();
1077+
1078+
let sources = Arc::new(std::sync::Mutex::new(std::collections::HashSet::<
1079+
std::net::IpAddr,
1080+
>::new()));
1081+
let sources_task = sources.clone();
1082+
let server_accept = server.clone();
1083+
let accept = tokio::spawn(async move {
1084+
while let Some(incoming) = server_accept.accept().await {
1085+
if let IncomingAddr::Ip(sa) = incoming.remote_addr() {
1086+
sources_task.lock().unwrap().insert(sa.ip());
1087+
}
1088+
if incoming.remote_addr_validated() {
1089+
if let Ok(accepting) = incoming.accept() {
1090+
let _ = accepting.await;
1091+
}
1092+
} else {
1093+
let _ = incoming.retry();
1094+
}
1095+
}
1096+
});
1097+
1098+
let conn =
1099+
tokio::time::timeout(Duration::from_secs(8), client.connect(addr, ECHO_ALPN))
1100+
.await;
1101+
match conn {
1102+
Ok(Ok(_)) => {}
1103+
Ok(Err(err)) => {
1104+
failures += 1;
1105+
first_err.get_or_insert_with(|| format!("iter {i}: {err:#}"));
1106+
}
1107+
Err(_) => {
1108+
failures += 1;
1109+
first_err.get_or_insert_with(|| format!("iter {i}: connect timed out"));
1110+
}
1111+
}
1112+
1113+
accept.abort();
1114+
client.close().await;
1115+
server.close().await;
1116+
1117+
let n = sources.lock().unwrap().len();
1118+
max_client_sources = max_client_sources.max(n);
1119+
if n >= 2 {
1120+
multi_source_iters += 1;
1121+
}
1122+
}
1123+
1124+
// Demo branch: fail unconditionally so the tally prints in CI output
1125+
// (a passing test swallows it). `multi_source_iters` tells us whether
1126+
// the runner was actually multi-homed; `failures` is the #4114 signal.
1127+
panic!(
1128+
"addr_retry_multihomed: failures={failures}/{ITERS} \
1129+
server_ip_count={ip_count} max_client_sources={max_client_sources} \
1130+
multi_source_iters={multi_source_iters}/{ITERS} first_err={first_err:?}",
1131+
);
1132+
}
1133+
10461134
/// Verify that returning `Retry` for a relay connection also causes
10471135
/// the remote to retry with a token. The "validation" has no
10481136
/// security meaning over a relay, but it does impose a round-trip

0 commit comments

Comments
 (0)