fix: retain peer address so a transient gossip Leave can't strand the dialer
The dialer-side supervisor dialed peers by bare EndpointId, forcing iroh to resolve the address through the gossip-fed MemoryLookup. A transient drop that also triggered a gossip Leave/NeighborDown purged the peer from that lookup, so the supervisor redialed forever with "no address" (observed in the 2026-05-30 field test: ~4.5 min of "No address lookup configured; retrying in 5s"). The transport now retains each peer's full EndpointAddr (relay + direct addrs), refreshed on join and on every re-announce (so a rejoin on a new address updates the dial target), and the dialer dials that retained address directly — which bypasses the lookup entirely. connect_peer now takes an EndpointAddr; core hands over state.addr on PeerJoined and PeerUpdated. New regression test dials and reconnects with the dialer's lookup deliberately empty and the relay disabled, so only the retained address can carry the dial: it passes with the fix and times out under the old bare-id behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -164,7 +164,6 @@ async fn loopback_sequenced_audio_reaches_peer_and_decodes() {
|
||||
b.lookup.add_endpoint_info(a.endpoint.addr());
|
||||
|
||||
let a_id = a.endpoint.id();
|
||||
let b_id = b.endpoint.id();
|
||||
|
||||
// Subscribe to incoming datagrams on B before any are sent.
|
||||
let mut b_rx = b.transport.receive_datagrams().await.expect("subscribe B");
|
||||
@@ -172,8 +171,8 @@ async fn loopback_sequenced_audio_reaches_peer_and_decodes() {
|
||||
// Reactive connection setup, exactly as core does on peer-join. Calling on
|
||||
// both sides is fine: the lower EndpointId dials, the higher accepts, and a
|
||||
// single shared connection forms.
|
||||
a.transport.connect_peer(b_id).await;
|
||||
b.transport.connect_peer(a_id).await;
|
||||
a.transport.connect_peer(b.endpoint.addr()).await;
|
||||
b.transport.connect_peer(a.endpoint.addr()).await;
|
||||
|
||||
// Let the dial + accept registration settle.
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
@@ -276,7 +275,7 @@ async fn dialer_reconnects_after_link_drops() {
|
||||
.subscribe_conn_events()
|
||||
.await
|
||||
.expect("subscribe dialer conn events");
|
||||
dialer.transport.connect_peer(peer_id).await;
|
||||
dialer.transport.connect_peer(peer.endpoint.addr()).await;
|
||||
|
||||
// The peer accepts the dialer's initial connection.
|
||||
let conn1 = tokio::time::timeout(Duration::from_secs(10), peer.conns_rx.recv())
|
||||
@@ -319,3 +318,78 @@ async fn dialer_reconnects_after_link_drops() {
|
||||
"audio should resume after reconnect; got {received} frames"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dialer_connects_and_reconnects_without_an_address_lookup() {
|
||||
// Regression for the redial-loop bug: a transient drop that also triggers a
|
||||
// gossip Leave/NeighborDown purges the peer from the address lookup, so a
|
||||
// dialer that resolves by bare id (the old behavior) has nothing to dial and
|
||||
// loops forever. The fix retains the peer's full address in the transport and
|
||||
// dials it directly. To prove the retained address — not the lookup — is what
|
||||
// carries the dial, we never seed the dialer's lookup with the peer at all
|
||||
// (and disable the relay), so a bare-id dial could not resolve an address for
|
||||
// either the initial connect or the reconnect; only `connect_peer`'s retained
|
||||
// EndpointAddr makes them possible.
|
||||
let s1 = iroh::SecretKey::generate();
|
||||
let s2 = iroh::SecretKey::generate();
|
||||
let (dialer_secret, peer_secret) = if s1.public().to_string() < s2.public().to_string() {
|
||||
(s1, s2)
|
||||
} else {
|
||||
(s2, s1)
|
||||
};
|
||||
|
||||
let dialer = spawn_node_with_key(dialer_secret).await;
|
||||
let mut peer = spawn_capture_peer(peer_secret).await;
|
||||
|
||||
let dialer_id = dialer.endpoint.id();
|
||||
let peer_id = peer.endpoint.id();
|
||||
assert!(dialer_id.to_string() < peer_id.to_string());
|
||||
|
||||
// Deliberately leave the dialer's lookup empty for the peer. (The peer side
|
||||
// only ever accepts, so it needs no addressing for the dialer.)
|
||||
let mut conn_events = dialer
|
||||
.transport
|
||||
.subscribe_conn_events()
|
||||
.await
|
||||
.expect("subscribe dialer conn events");
|
||||
|
||||
// The full address handed here is the dialer's only way to reach the peer.
|
||||
dialer.transport.connect_peer(peer.endpoint.addr()).await;
|
||||
|
||||
let conn1 = tokio::time::timeout(Duration::from_secs(10), peer.conns_rx.recv())
|
||||
.await
|
||||
.expect("timed out awaiting initial connection (retained address path)")
|
||||
.expect("connection channel closed");
|
||||
assert!(
|
||||
await_reconnect(&mut conn_events, tokio::time::Instant::now() + Duration::from_secs(10)).await,
|
||||
"initial link should report Connecting then Connected"
|
||||
);
|
||||
|
||||
// Drop the live link from the peer side so the dialer must reconnect — again
|
||||
// with no lookup to fall back on.
|
||||
conn1.close(0u32.into(), b"transient drop");
|
||||
drop(conn1);
|
||||
|
||||
let conn2 = tokio::time::timeout(Duration::from_secs(15), peer.conns_rx.recv())
|
||||
.await
|
||||
.expect("timed out awaiting reconnect (retained address path)")
|
||||
.expect("connection channel closed");
|
||||
assert!(
|
||||
await_reconnect(&mut conn_events, tokio::time::Instant::now() + Duration::from_secs(15)).await,
|
||||
"reconnect should report Connecting then Connected with no lookup at all"
|
||||
);
|
||||
|
||||
// Confirm the rebuilt link actually carries audio.
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
|
||||
for seq in 0..30u32 {
|
||||
dialer.transport.broadcast(packet(&mut enc, seq));
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
|
||||
let received = count_audio(&conn2, 25, tokio::time::Instant::now() + Duration::from_secs(3)).await;
|
||||
assert!(
|
||||
received >= 20,
|
||||
"audio should resume after reconnecting via the retained address; got {received} frames"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user