From 316dc5f7d5eb7ae307a130f48320bc459e93b62f Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 19 May 2026 14:15:48 +0200 Subject: [PATCH 1/4] fix(relay-server): pass dummy port to lookup_host tokio::net::lookup_host requires a host:port input, so passing a bare hostname (e.g. pluto-relay-0-p2p.ovh.dev-nethermind.xyz) failed with "invalid socket address" and the relay never resolved the external host for its ENR. Use a dummy port of 0 and iterate through the resolved addresses to find the first IPv4, rather than silently giving up if the first record happens to be IPv6. --- crates/relay-server/src/web.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/relay-server/src/web.rs b/crates/relay-server/src/web.rs index 97576a0a..20e5f24d 100644 --- a/crates/relay-server/src/web.rs +++ b/crates/relay-server/src/web.rs @@ -329,13 +329,22 @@ async fn resolve_external_host_periodically( /// Resolves the external host to an IP address. async fn resolve_external_host(state: Arc, external_host: &str) { - match tokio::net::lookup_host(external_host).await { - Ok(mut addrs) => { - if let Some(addr) = addrs.next() - && let IpAddr::V4(ipv4) = addr.ip() - { + // `tokio::net::lookup_host` requires a `host:port` input, but we only need + // the IP — use a dummy port of 0 so a bare hostname resolves correctly. + match tokio::net::lookup_host((external_host, 0)).await { + Ok(addrs) => { + let ipv4 = addrs + .filter_map(|a| match a.ip() { + IpAddr::V4(v4) => Some(v4), + IpAddr::V6(_) => None, + }) + .next(); + + if let Some(ipv4) = ipv4 { debug!("Resolved external host {external_host} to {ipv4}"); state.set_external_host_ip(Some(ipv4)).await; + } else { + warn!("External host {external_host} resolved with no IPv4 address"); } } Err(e) => { From 03e7736213177f5e147eeee36fba3a4f1a221fd9 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 19 May 2026 14:31:50 +0200 Subject: [PATCH 2/4] test(relay): expect non-loopback ip after external host resolves The previous assertion required the ENR's ip to be loopback after setting external_host = "www.google.com", which only held because lookup_host silently failed on a bare hostname. With the resolver fixed, the ip override now applies, so poll the ENR until a non-loopback ip is observed (matching the Go assert.Eventually test). --- crates/cli/src/commands/relay.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/cli/src/commands/relay.rs b/crates/cli/src/commands/relay.rs index 1c92663a..fc78d5fb 100644 --- a/crates/cli/src/commands/relay.rs +++ b/crates/cli/src/commands/relay.rs @@ -403,11 +403,26 @@ mod tests { with_relay_server( |args| args.p2p.external_host = Some("www.google.com".into()), async |cfg| { - let response = relay_server_get(cfg, "/enr").await.unwrap(); - let body = response.text().await.unwrap(); - let enr = pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); - - assert!(enr.ip().unwrap().is_loopback()); + // Resolution happens asynchronously on a tick, so poll until the + // ENR reflects a non-loopback IP (mirrors the Go test using + // `assert.Eventually`). + let deadline = time::Instant::now() + time::Duration::from_secs(10); + loop { + let response = relay_server_get(cfg.clone(), "/enr").await.unwrap(); + let body = response.text().await.unwrap(); + let enr = pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); + let ip = enr.ip().unwrap(); + + if !ip.is_loopback() { + break; + } + + assert!( + time::Instant::now() < deadline, + "external host never resolved to non-loopback ip" + ); + tokio::time::sleep(time::Duration::from_millis(200)).await; + } }, ) .await From c39a6907e00dd259e1af7d4e71cd5c27f9192d77 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 19 May 2026 17:55:41 +0200 Subject: [PATCH 3/4] test(relay): use tokio::time::timeout for poll bound --- crates/cli/src/commands/relay.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/cli/src/commands/relay.rs b/crates/cli/src/commands/relay.rs index fc78d5fb..df16d225 100644 --- a/crates/cli/src/commands/relay.rs +++ b/crates/cli/src/commands/relay.rs @@ -406,23 +406,23 @@ mod tests { // Resolution happens asynchronously on a tick, so poll until the // ENR reflects a non-loopback IP (mirrors the Go test using // `assert.Eventually`). - let deadline = time::Instant::now() + time::Duration::from_secs(10); - loop { - let response = relay_server_get(cfg.clone(), "/enr").await.unwrap(); - let body = response.text().await.unwrap(); - let enr = pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); - let ip = enr.ip().unwrap(); - - if !ip.is_loopback() { - break; + tokio::time::timeout(time::Duration::from_secs(10), async { + loop { + let response = relay_server_get(cfg.clone(), "/enr").await.unwrap(); + let body = response.text().await.unwrap(); + let enr = + pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); + let ip = enr.ip().unwrap(); + + if !ip.is_loopback() { + break; + } + + tokio::time::sleep(time::Duration::from_millis(200)).await; } - - assert!( - time::Instant::now() < deadline, - "external host never resolved to non-loopback ip" - ); - tokio::time::sleep(time::Duration::from_millis(200)).await; - } + }) + .await + .expect("external host never resolved to non-loopback ip"); }, ) .await From fb780d76f6faff79d9c6a0e48593b1a2a2b5d8a7 Mon Sep 17 00:00:00 2001 From: Bohdan Ohorodnii <35969035+varex83@users.noreply.github.com> Date: Tue, 19 May 2026 17:56:02 +0200 Subject: [PATCH 4/4] style: rustfmt --- crates/cli/src/commands/relay.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cli/src/commands/relay.rs b/crates/cli/src/commands/relay.rs index df16d225..2edd8d4a 100644 --- a/crates/cli/src/commands/relay.rs +++ b/crates/cli/src/commands/relay.rs @@ -410,8 +410,7 @@ mod tests { loop { let response = relay_server_get(cfg.clone(), "/enr").await.unwrap(); let body = response.text().await.unwrap(); - let enr = - pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); + let enr = pluto_eth2util::enr::Record::try_from(body.as_str()).unwrap(); let ip = enr.ip().unwrap(); if !ip.is_loopback() {