Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 84 additions & 5 deletions bin/node/src/flags/p2p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,40 @@ use kona_peers::{BootNode, BootStoreFile, PeerMonitoring, PeerScoreLevel};
use kona_providers_alloy::AlloyChainProvider;
use libp2p::identity::Keypair;
use std::{
net::{IpAddr, SocketAddr},
net::{IpAddr, SocketAddr, ToSocketAddrs},
num::ParseIntError,
path::PathBuf,
str::FromStr,
};
use tokio::time::Duration;
use url::Url;

/// Resolves a hostname or IP address string to an [`IpAddr`].
///
/// Accepts either:
/// - A valid IP address string (e.g., "127.0.0.1", "::1")
/// - A DNS hostname (e.g., "node1.example.com")
///
/// For DNS hostnames, this performs synchronous DNS resolution and returns the first
/// resolved IP address.
fn resolve_host(host: &str) -> Result<IpAddr, String> {
// First, try to parse as a direct IP address
if let Ok(ip) = host.parse::<IpAddr>() {
return Ok(ip);
}

// If that fails, try DNS resolution
// We append a port to make it a valid socket address for resolution
let socket_addr = format!("{host}:0");
match socket_addr.to_socket_addrs() {
Ok(mut addrs) => addrs
.next()
.map(|addr| addr.ip())
.ok_or_else(|| format!("DNS resolution for '{host}' returned no addresses")),
Err(e) => Err(format!("Failed to resolve '{host}': {e}")),
}
}

/// P2P CLI Flags
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct P2PArgs {
Expand All @@ -43,13 +69,15 @@ pub struct P2PArgs {
#[arg(long = "p2p.priv.raw", env = "KONA_NODE_P2P_PRIV_RAW")]
pub private_key: Option<B256>,

/// IP to advertise to external peers from Discv5.
/// IP address or DNS hostname to advertise to external peers from Discv5.
/// Optional argument. Use the `p2p.listen.ip` if not set.
/// Accepts either an IP address (e.g., "1.2.3.4") or a DNS hostname (e.g.,
/// "node1.example.com"). DNS hostnames are resolved to IP addresses at startup.
///
/// Technical note: if this argument is set, the dynamic ENR updates from the discovery layer
/// will be disabled. This is to allow the advertised IP to be static (to use in a network
/// behind a NAT for instance).
#[arg(long = "p2p.advertise.ip", env = "KONA_NODE_P2P_ADVERTISE_IP")]
#[arg(long = "p2p.advertise.ip", env = "KONA_NODE_P2P_ADVERTISE_IP", value_parser = resolve_host)]
pub advertise_ip: Option<IpAddr>,
/// TCP port to advertise to external peers from the discovery layer. Same as `p2p.listen.tcp`
/// if set to zero.
Expand All @@ -60,8 +88,10 @@ pub struct P2PArgs {
#[arg(long = "p2p.advertise.udp", env = "KONA_NODE_P2P_ADVERTISE_UDP_PORT")]
pub advertise_udp_port: Option<u16>,

/// IP to bind LibP2P/Discv5 to.
#[arg(long = "p2p.listen.ip", default_value = "0.0.0.0", env = "KONA_NODE_P2P_LISTEN_IP")]
/// IP address or DNS hostname to bind LibP2P/Discv5 to.
/// Accepts either an IP address (e.g., "0.0.0.0") or a DNS hostname (e.g.,
/// "node1.example.com"). DNS hostnames are resolved to IP addresses at startup.
#[arg(long = "p2p.listen.ip", default_value = "0.0.0.0", env = "KONA_NODE_P2P_LISTEN_IP", value_parser = resolve_host)]
pub listen_ip: IpAddr,
/// TCP port to bind LibP2P to. Any available system port if set to 0.
#[arg(long = "p2p.listen.tcp", default_value = "9222", env = "KONA_NODE_P2P_LISTEN_TCP_PORT")]
Expand Down Expand Up @@ -627,4 +657,53 @@ mod tests {
]
);
}

#[test]
fn test_p2p_args_listen_ip_dns_resolution() {
// Test that DNS hostnames are resolved to IP addresses
// Using localhost which should resolve reliably
let args = MockCommand::parse_from(["test", "--p2p.listen.ip", "localhost"]);
// localhost typically resolves to 127.0.0.1 or ::1
assert!(
args.p2p.listen_ip == "127.0.0.1".parse::<IpAddr>().unwrap() ||
args.p2p.listen_ip == "::1".parse::<IpAddr>().unwrap()
);
}

#[test]
fn test_p2p_args_advertise_ip_dns_resolution() {
// Test that DNS hostnames are resolved to IP addresses for advertise_ip
let args = MockCommand::parse_from(["test", "--p2p.advertise.ip", "localhost"]);
// localhost typically resolves to 127.0.0.1 or ::1
let ip = args.p2p.advertise_ip.unwrap();
assert!(
ip == "127.0.0.1".parse::<IpAddr>().unwrap() || ip == "::1".parse::<IpAddr>().unwrap()
);
}

#[test]
fn test_resolve_host_with_ip() {
// Test that IP addresses are passed through directly
let ip = resolve_host("192.168.1.1").unwrap();
assert_eq!(ip, "192.168.1.1".parse::<IpAddr>().unwrap());

let ipv6 = resolve_host("::1").unwrap();
assert_eq!(ipv6, "::1".parse::<IpAddr>().unwrap());
}

#[test]
fn test_resolve_host_with_dns() {
// Test DNS resolution with localhost
let ip = resolve_host("localhost").unwrap();
assert!(
ip == "127.0.0.1".parse::<IpAddr>().unwrap() || ip == "::1".parse::<IpAddr>().unwrap()
);
}

#[test]
fn test_resolve_host_invalid() {
// Test that invalid hostnames return an error
let result = resolve_host("this-hostname-definitely-does-not-exist.invalid");
assert!(result.is_err());
}
}
Loading