From a1ffe0ba954cd58176311511103ada3ca666c711 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 10 Dec 2025 12:28:59 -0500 Subject: [PATCH 1/4] fix: Bug in SwarmEvent::OutgoingConnectionError handler - When an outgoing dial attempt fails asynchronously (network timeout, connection refused, etc.), the peer was NOT removed from the current_dials HashSet, leaving it permanently stuck in dialing state. --- crates/node/gossip/src/driver.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/node/gossip/src/driver.rs b/crates/node/gossip/src/driver.rs index 7228164def..3b4380d366 100644 --- a/crates/node/gossip/src/driver.rs +++ b/crates/node/gossip/src/driver.rs @@ -409,6 +409,10 @@ where } SwarmEvent::OutgoingConnectionError { peer_id: _peer_id, error, .. } => { debug!(target: "gossip", "Outgoing connection error: {:?}", error); + // Remove the peer from current_dials so it can be dialed again + if let Some(peer_id) = _peer_id { + self.connection_gate.remove_dial(&peer_id); + } kona_macros::inc!( gauge, crate::Metrics::GOSSIPSUB_CONNECTION, From 2884332cddaebc5cef3ae420a68d8a937325bb96 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 10 Dec 2025 15:15:21 -0500 Subject: [PATCH 2/4] feat: allow kona to dial dns based multiaddres by resolving them firsT --- crates/node/gossip/src/driver.rs | 23 +++++++++-- crates/node/gossip/src/gater.rs | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/crates/node/gossip/src/driver.rs b/crates/node/gossip/src/driver.rs index 3b4380d366..cc7c5a23b6 100644 --- a/crates/node/gossip/src/driver.rs +++ b/crates/node/gossip/src/driver.rs @@ -265,14 +265,29 @@ where return; } + // Convert DNS multiaddr to IP multiaddr if needed + let dial_addr = match ConnectionGater::resolve_dns_multiaddr(&addr) { + Some(resolved_addr) => { + if resolved_addr != addr { + debug!(target: "gossip", original=?addr, resolved=?resolved_addr, "Converted DNS multiaddr to IP multiaddr"); + } + resolved_addr + } + None => { + warn!(target: "gossip", peer=?addr, "DNS resolution failed, cannot dial"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "dns_resolution_failed", "peer" => peer_id.to_string()); + return; + } + }; + // Let the gate know we are dialing the address. - self.connection_gate.dialing(&addr); + self.connection_gate.dialing(&dial_addr); // Dial - match self.swarm.dial(addr.clone()) { + match self.swarm.dial(dial_addr.clone()) { Ok(_) => { - trace!(target: "gossip", peer=?addr, "Dialed peer"); - self.connection_gate.dialed(&addr); + trace!(target: "gossip", peer=?dial_addr, "Dialed peer"); + self.connection_gate.dialed(&dial_addr); kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER, "peer" => peer_id.to_string()); } Err(e) => { diff --git a/crates/node/gossip/src/gater.rs b/crates/node/gossip/src/gater.rs index 3c871e4be4..40b7c72db2 100644 --- a/crates/node/gossip/src/gater.rs +++ b/crates/node/gossip/src/gater.rs @@ -204,6 +204,44 @@ impl ConnectionGater { } false } + + /// Converts a DNS-based [`Multiaddr`] to an IP-based [`Multiaddr`]. + /// + /// If the multiaddr contains DNS components, attempts to resolve them and replace + /// with the corresponding IP address. Returns the converted multiaddr on success, + /// or None if DNS resolution fails or no DNS component exists. + pub fn resolve_dns_multiaddr(addr: &Multiaddr) -> Option { + let resolved_ip = match Self::try_resolve_dns(addr) { + Some(Ok(ip)) => ip, + Some(Err(())) => return None, // DNS resolution failed + None => return Some(addr.clone()), // No DNS component, return as-is + }; + + // Rebuild the multiaddr with the resolved IP instead of DNS + let mut new_addr = Multiaddr::empty(); + let mut dns_replaced = false; + + for component in addr.iter() { + match component { + libp2p::multiaddr::Protocol::Dns(_) + | libp2p::multiaddr::Protocol::Dns4(_) + | libp2p::multiaddr::Protocol::Dns6(_) + | libp2p::multiaddr::Protocol::Dnsaddr(_) => { + // Replace DNS with resolved IP (only once) + if !dns_replaced { + match resolved_ip { + IpAddr::V4(ip) => new_addr.push(libp2p::multiaddr::Protocol::Ip4(ip)), + IpAddr::V6(ip) => new_addr.push(libp2p::multiaddr::Protocol::Ip6(ip)), + } + dns_replaced = true; + } + } + other => new_addr.push(other), + } + } + + Some(new_addr) + } } impl ConnectionGate for ConnectionGater { @@ -555,3 +593,32 @@ fn test_dns_multiaddr_blocked_by_subnet() { let result = gater.can_dial(&dns_localhost); assert!(matches!(result, Err(DialError::SubnetBlocked { .. }))); } + +#[test] +fn test_resolve_dns_multiaddr() { + use std::str::FromStr; + + // Test DNS4 multiaddr conversion + let dns_addr = Multiaddr::from_str( + "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + let resolved = ConnectionGater::resolve_dns_multiaddr(&dns_addr); + assert!(resolved.is_some()); + + let resolved_addr = resolved.unwrap(); + // Should contain ip4 instead of dns4 + assert!(resolved_addr.to_string().contains("/ip4/127.0.0.1/")); + assert!(!resolved_addr.to_string().contains("/dns4/")); + + // Test IP4 multiaddr (should return as-is) + let ip_addr = Multiaddr::from_str( + "/ip4/192.168.1.1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + let resolved = ConnectionGater::resolve_dns_multiaddr(&ip_addr); + assert!(resolved.is_some()); + assert_eq!(resolved.unwrap(), ip_addr); +} From 577c13c6284292b1513325f7c0151c05ef365711 Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 10 Dec 2025 20:20:13 -0500 Subject: [PATCH 3/4] feat: libp2p add dns module --- crates/node/gossip/Cargo.toml | 2 +- crates/node/gossip/src/builder.rs | 5 +- crates/node/gossip/src/driver.rs | 24 +-- crates/node/gossip/src/gater.rs | 260 ++---------------------------- 4 files changed, 26 insertions(+), 265 deletions(-) diff --git a/crates/node/gossip/Cargo.toml b/crates/node/gossip/Cargo.toml index 1ebba37ccd..1c5deb4928 100644 --- a/crates/node/gossip/Cargo.toml +++ b/crates/node/gossip/Cargo.toml @@ -40,7 +40,7 @@ libp2p-stream.workspace = true discv5 = { workspace = true, features = ["libp2p"] } openssl = { workspace = true, features = ["vendored"] } libp2p-identity = { workspace = true, features = ["secp256k1"] } -libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "noise", "gossipsub", "ping", "yamux", "identify"] } +libp2p = { workspace = true, features = ["macros", "tokio", "tcp", "dns", "noise", "gossipsub", "ping", "yamux", "identify"] } ipnet = { workspace = true, features = ["serde"] } # Misc diff --git a/crates/node/gossip/src/builder.rs b/crates/node/gossip/src/builder.rs index c78cd13e63..e77c333be8 100644 --- a/crates/node/gossip/src/builder.rs +++ b/crates/node/gossip/src/builder.rs @@ -192,7 +192,8 @@ impl GossipDriverBuilder { .accept(sync_protocol_name) .map_err(|_| GossipDriverBuilderError::SyncReqRespAlreadyAccepted)?; - // Build the swarm. + // Build the swarm with DNS+TCP transport. + // Note: with_dns() must be called after with_tcp() to wrap TCP with DNS resolution. debug!(target: "gossip", "Building Swarm with Peer ID: {}", keypair.public().to_peer_id()); let swarm = SwarmBuilder::with_existing_identity(keypair) .with_tokio() @@ -205,6 +206,8 @@ impl GossipDriverBuilder { YamuxConfig::default, ) .map_err(|_| GossipDriverBuilderError::TcpError)? + .with_dns() + .map_err(|_| GossipDriverBuilderError::TcpError)? .with_behaviour(|_| behaviour) .map_err(|_| GossipDriverBuilderError::WithBehaviourError)? .with_swarm_config(|c| c.with_idle_connection_timeout(timeout)) diff --git a/crates/node/gossip/src/driver.rs b/crates/node/gossip/src/driver.rs index cc7c5a23b6..f6233c70d5 100644 --- a/crates/node/gossip/src/driver.rs +++ b/crates/node/gossip/src/driver.rs @@ -265,29 +265,15 @@ where return; } - // Convert DNS multiaddr to IP multiaddr if needed - let dial_addr = match ConnectionGater::resolve_dns_multiaddr(&addr) { - Some(resolved_addr) => { - if resolved_addr != addr { - debug!(target: "gossip", original=?addr, resolved=?resolved_addr, "Converted DNS multiaddr to IP multiaddr"); - } - resolved_addr - } - None => { - warn!(target: "gossip", peer=?addr, "DNS resolution failed, cannot dial"); - kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "dns_resolution_failed", "peer" => peer_id.to_string()); - return; - } - }; - // Let the gate know we are dialing the address. - self.connection_gate.dialing(&dial_addr); + // Note: libp2p-dns will automatically resolve DNS multiaddrs at the transport layer. + self.connection_gate.dialing(&addr); // Dial - match self.swarm.dial(dial_addr.clone()) { + match self.swarm.dial(addr.clone()) { Ok(_) => { - trace!(target: "gossip", peer=?dial_addr, "Dialed peer"); - self.connection_gate.dialed(&dial_addr); + trace!(target: "gossip", peer=?addr, "Dialed peer"); + self.connection_gate.dialed(&addr); kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER, "peer" => peer_id.to_string()); } Err(e) => { diff --git a/crates/node/gossip/src/gater.rs b/crates/node/gossip/src/gater.rs index 40b7c72db2..2fdb06aca0 100644 --- a/crates/node/gossip/src/gater.rs +++ b/crates/node/gossip/src/gater.rs @@ -5,7 +5,7 @@ use ipnet::IpNet; use libp2p::{Multiaddr, PeerId}; use std::{ collections::{HashMap, HashSet}, - net::{IpAddr, ToSocketAddrs}, + net::IpAddr, time::Duration, }; use tokio::time::Instant; @@ -142,59 +142,6 @@ impl ConnectionGater { }) } - /// Attempts to resolve a DNS-based [`Multiaddr`] to an [`IpAddr`]. - /// - /// Returns: - /// - `None` if the multiaddr does not contain a DNS component (use [`Self::ip_from_addr`]) - /// - `Some(Err(()))` if DNS resolution failed - /// - `Some(Ok(ip))` if DNS resolution succeeded - /// - /// Respects the DNS protocol type: `dns4` only returns IPv4, `dns6` only returns IPv6. - pub fn try_resolve_dns(addr: &Multiaddr) -> Option> { - // Track which DNS protocol type was used - let (hostname, ipv4_only, ipv6_only) = - addr.iter().find_map(|component| match component { - libp2p::multiaddr::Protocol::Dns(h) | libp2p::multiaddr::Protocol::Dnsaddr(h) => { - Some((h.to_string(), false, false)) - } - libp2p::multiaddr::Protocol::Dns4(h) => Some((h.to_string(), true, false)), - libp2p::multiaddr::Protocol::Dns6(h) => Some((h.to_string(), false, true)), - _ => None, - })?; - - debug!(target: "p2p", %hostname, ipv4_only, ipv6_only, "Resolving DNS hostname"); - - let ip = match format!("{hostname}:0").to_socket_addrs() { - Ok(addrs) => { - // Filter addresses based on DNS protocol type - addrs.map(|socket_addr| socket_addr.ip()).find(|ip| { - if ipv4_only { - ip.is_ipv4() - } else if ipv6_only { - ip.is_ipv6() - } else { - true - } - }) - } - Err(e) => { - warn!(target: "p2p", %hostname, error = %e, "DNS resolution failed"); - return Some(Err(())); - } - }; - - ip.map_or_else( - || { - warn!(target: "p2p", %hostname, "DNS resolution returned no matching addresses"); - Some(Err(())) - }, - |resolved_ip| { - debug!(target: "p2p", %hostname, %resolved_ip, "DNS resolution successful"); - Some(Ok(resolved_ip)) - }, - ) - } - /// Checks if a given [`IpAddr`] is within any of the `blocked_subnets`. pub fn check_ip_in_blocked_subnets(&self, ip_addr: &IpAddr) -> bool { for subnet in &self.blocked_subnets { @@ -204,44 +151,6 @@ impl ConnectionGater { } false } - - /// Converts a DNS-based [`Multiaddr`] to an IP-based [`Multiaddr`]. - /// - /// If the multiaddr contains DNS components, attempts to resolve them and replace - /// with the corresponding IP address. Returns the converted multiaddr on success, - /// or None if DNS resolution fails or no DNS component exists. - pub fn resolve_dns_multiaddr(addr: &Multiaddr) -> Option { - let resolved_ip = match Self::try_resolve_dns(addr) { - Some(Ok(ip)) => ip, - Some(Err(())) => return None, // DNS resolution failed - None => return Some(addr.clone()), // No DNS component, return as-is - }; - - // Rebuild the multiaddr with the resolved IP instead of DNS - let mut new_addr = Multiaddr::empty(); - let mut dns_replaced = false; - - for component in addr.iter() { - match component { - libp2p::multiaddr::Protocol::Dns(_) - | libp2p::multiaddr::Protocol::Dns4(_) - | libp2p::multiaddr::Protocol::Dns6(_) - | libp2p::multiaddr::Protocol::Dnsaddr(_) => { - // Replace DNS with resolved IP (only once) - if !dns_replaced { - match resolved_ip { - IpAddr::V4(ip) => new_addr.push(libp2p::multiaddr::Protocol::Ip4(ip)), - IpAddr::V6(ip) => new_addr.push(libp2p::multiaddr::Protocol::Ip6(ip)), - } - dns_replaced = true; - } - } - other => new_addr.push(other), - } - } - - Some(new_addr) - } } impl ConnectionGate for ConnectionGater { @@ -279,36 +188,23 @@ impl ConnectionGate for ConnectionGater { return Err(DialError::PeerBlocked { peer_id }); } - // Get IP address - either directly from multiaddr or by resolving DNS. - let ip_addr = match Self::try_resolve_dns(addr) { - Some(Ok(ip)) => { - debug!(target: "gossip", peer=?addr, resolved_ip=?ip, "Resolved DNS multiaddr"); - ip + // For IP-based multiaddrs, check if the address or subnet is blocked. + // For DNS-based multiaddrs, skip IP blocking checks since libp2p-dns will resolve them. + if let Some(ip_addr) = Self::ip_from_addr(addr) { + // If the address is blocked, do not dial. + if self.blocked_addrs.contains(&ip_addr) { + debug!(target: "gossip", peer=?addr, "Address is blocked, not dialing"); + self.connectedness.insert(peer_id, Connectedness::CannotConnect); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_address", "peer" => peer_id.to_string()); + return Err(DialError::AddressBlocked { ip: ip_addr }); } - Some(Err(())) => { - // DNS resolution failed - allow the dial, libp2p will handle it. - debug!(target: "gossip", peer=?addr, "DNS resolution failed, allowing dial"); - return Ok(()); - } - None => Self::ip_from_addr(addr).ok_or_else(|| { - warn!(target: "p2p", peer=?addr, "Failed to extract IpAddr from Multiaddr"); - DialError::InvalidIpAddress { addr: addr.clone() } - })?, - }; - - // If the address is blocked, do not dial. - if self.blocked_addrs.contains(&ip_addr) { - debug!(target: "gossip", peer=?addr, "Address is blocked, not dialing"); - self.connectedness.insert(peer_id, Connectedness::CannotConnect); - kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_address", "peer" => peer_id.to_string()); - return Err(DialError::AddressBlocked { ip: ip_addr }); - } - // If address lies in any blocked subnets, do not dial. - if self.check_ip_in_blocked_subnets(&ip_addr) { - debug!(target: "gossip", ip=?ip_addr, "IP address is in a blocked subnet, not dialing"); - kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_subnet", "peer" => peer_id.to_string()); - return Err(DialError::SubnetBlocked { ip: ip_addr }); + // If address lies in any blocked subnets, do not dial. + if self.check_ip_in_blocked_subnets(&ip_addr) { + debug!(target: "gossip", ip=?ip_addr, "IP address is in a blocked subnet, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_subnet", "peer" => peer_id.to_string()); + return Err(DialError::SubnetBlocked { ip: ip_addr }); + } } Ok(()) @@ -472,53 +368,6 @@ fn test_dial_error_handling() { assert!(matches!(result, Err(DialError::AlreadyDialing { .. }))); } -#[test] -fn test_dns_multiaddr_detection() { - use std::str::FromStr; - - // Test DNS4 multiaddr (try_resolve_dns returns Some for DNS addresses) - let dns4_addr = Multiaddr::from_str( - "/dns4/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&dns4_addr).is_some()); - - // Test DNS6 multiaddr - let dns6_addr = Multiaddr::from_str( - "/dns6/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&dns6_addr).is_some()); - - // Test DNS multiaddr (generic) - let dns_addr = Multiaddr::from_str( - "/dns/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&dns_addr).is_some()); - - // Test dnsaddr multiaddr - let dnsaddr = Multiaddr::from_str( - "/dnsaddr/example.com/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&dnsaddr).is_some()); - - // Test IP4 multiaddr (should NOT be detected as DNS - returns None) - let ip4_addr = Multiaddr::from_str( - "/ip4/127.0.0.1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&ip4_addr).is_none()); - - // Test IP6 multiaddr (should NOT be detected as DNS - returns None) - let ip6_addr = Multiaddr::from_str( - "/ip6/::1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - assert!(ConnectionGater::try_resolve_dns(&ip6_addr).is_none()); -} - #[test] fn test_dns_multiaddr_can_dial() { use crate::ConnectionGate; @@ -545,80 +394,3 @@ fn test_dns_multiaddr_can_dial() { gater.block_peer(&peer_id); assert!(gater.can_dial(&dns4_addr).is_err()); } - -#[test] -fn test_dns_multiaddr_blocked_by_resolved_ip() { - use crate::{ConnectionGate, DialError}; - use std::{net::IpAddr, str::FromStr}; - - let mut gater = ConnectionGater::new(GaterConfig::default()); - - // localhost resolves to 127.0.0.1 - let dns_localhost = Multiaddr::from_str( - "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - - // Should succeed before blocking - assert!(gater.can_dial(&dns_localhost).is_ok()); - - // Block 127.0.0.1 - gater.block_addr(IpAddr::from_str("127.0.0.1").unwrap()); - - // Should now fail because localhost resolves to blocked IP - let result = gater.can_dial(&dns_localhost); - assert!(matches!(result, Err(DialError::AddressBlocked { .. }))); -} - -#[test] -fn test_dns_multiaddr_blocked_by_subnet() { - use crate::{ConnectionGate, DialError}; - use std::str::FromStr; - - let mut gater = ConnectionGater::new(GaterConfig::default()); - - // localhost resolves to 127.0.0.1 - let dns_localhost = Multiaddr::from_str( - "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - - // Should succeed before blocking - assert!(gater.can_dial(&dns_localhost).is_ok()); - - // Block the 127.0.0.0/8 subnet - gater.block_subnet("127.0.0.0/8".parse().unwrap()); - - // Should now fail because localhost resolves to IP in blocked subnet - let result = gater.can_dial(&dns_localhost); - assert!(matches!(result, Err(DialError::SubnetBlocked { .. }))); -} - -#[test] -fn test_resolve_dns_multiaddr() { - use std::str::FromStr; - - // Test DNS4 multiaddr conversion - let dns_addr = Multiaddr::from_str( - "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - - let resolved = ConnectionGater::resolve_dns_multiaddr(&dns_addr); - assert!(resolved.is_some()); - - let resolved_addr = resolved.unwrap(); - // Should contain ip4 instead of dns4 - assert!(resolved_addr.to_string().contains("/ip4/127.0.0.1/")); - assert!(!resolved_addr.to_string().contains("/dns4/")); - - // Test IP4 multiaddr (should return as-is) - let ip_addr = Multiaddr::from_str( - "/ip4/192.168.1.1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", - ) - .unwrap(); - - let resolved = ConnectionGater::resolve_dns_multiaddr(&ip_addr); - assert!(resolved.is_some()); - assert_eq!(resolved.unwrap(), ip_addr); -} From ddff23fc9fd5fe1646b651814445cd7a89db521b Mon Sep 17 00:00:00 2001 From: Jacob Elias Date: Wed, 17 Dec 2025 11:08:11 -0500 Subject: [PATCH 4/4] Add back blocking IPS based on DNS Resolution --- crates/node/gossip/src/gater.rs | 193 +++++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 16 deletions(-) diff --git a/crates/node/gossip/src/gater.rs b/crates/node/gossip/src/gater.rs index 2fdb06aca0..3c871e4be4 100644 --- a/crates/node/gossip/src/gater.rs +++ b/crates/node/gossip/src/gater.rs @@ -5,7 +5,7 @@ use ipnet::IpNet; use libp2p::{Multiaddr, PeerId}; use std::{ collections::{HashMap, HashSet}, - net::IpAddr, + net::{IpAddr, ToSocketAddrs}, time::Duration, }; use tokio::time::Instant; @@ -142,6 +142,59 @@ impl ConnectionGater { }) } + /// Attempts to resolve a DNS-based [`Multiaddr`] to an [`IpAddr`]. + /// + /// Returns: + /// - `None` if the multiaddr does not contain a DNS component (use [`Self::ip_from_addr`]) + /// - `Some(Err(()))` if DNS resolution failed + /// - `Some(Ok(ip))` if DNS resolution succeeded + /// + /// Respects the DNS protocol type: `dns4` only returns IPv4, `dns6` only returns IPv6. + pub fn try_resolve_dns(addr: &Multiaddr) -> Option> { + // Track which DNS protocol type was used + let (hostname, ipv4_only, ipv6_only) = + addr.iter().find_map(|component| match component { + libp2p::multiaddr::Protocol::Dns(h) | libp2p::multiaddr::Protocol::Dnsaddr(h) => { + Some((h.to_string(), false, false)) + } + libp2p::multiaddr::Protocol::Dns4(h) => Some((h.to_string(), true, false)), + libp2p::multiaddr::Protocol::Dns6(h) => Some((h.to_string(), false, true)), + _ => None, + })?; + + debug!(target: "p2p", %hostname, ipv4_only, ipv6_only, "Resolving DNS hostname"); + + let ip = match format!("{hostname}:0").to_socket_addrs() { + Ok(addrs) => { + // Filter addresses based on DNS protocol type + addrs.map(|socket_addr| socket_addr.ip()).find(|ip| { + if ipv4_only { + ip.is_ipv4() + } else if ipv6_only { + ip.is_ipv6() + } else { + true + } + }) + } + Err(e) => { + warn!(target: "p2p", %hostname, error = %e, "DNS resolution failed"); + return Some(Err(())); + } + }; + + ip.map_or_else( + || { + warn!(target: "p2p", %hostname, "DNS resolution returned no matching addresses"); + Some(Err(())) + }, + |resolved_ip| { + debug!(target: "p2p", %hostname, %resolved_ip, "DNS resolution successful"); + Some(Ok(resolved_ip)) + }, + ) + } + /// Checks if a given [`IpAddr`] is within any of the `blocked_subnets`. pub fn check_ip_in_blocked_subnets(&self, ip_addr: &IpAddr) -> bool { for subnet in &self.blocked_subnets { @@ -188,23 +241,36 @@ impl ConnectionGate for ConnectionGater { return Err(DialError::PeerBlocked { peer_id }); } - // For IP-based multiaddrs, check if the address or subnet is blocked. - // For DNS-based multiaddrs, skip IP blocking checks since libp2p-dns will resolve them. - if let Some(ip_addr) = Self::ip_from_addr(addr) { - // If the address is blocked, do not dial. - if self.blocked_addrs.contains(&ip_addr) { - debug!(target: "gossip", peer=?addr, "Address is blocked, not dialing"); - self.connectedness.insert(peer_id, Connectedness::CannotConnect); - kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_address", "peer" => peer_id.to_string()); - return Err(DialError::AddressBlocked { ip: ip_addr }); + // Get IP address - either directly from multiaddr or by resolving DNS. + let ip_addr = match Self::try_resolve_dns(addr) { + Some(Ok(ip)) => { + debug!(target: "gossip", peer=?addr, resolved_ip=?ip, "Resolved DNS multiaddr"); + ip } - - // If address lies in any blocked subnets, do not dial. - if self.check_ip_in_blocked_subnets(&ip_addr) { - debug!(target: "gossip", ip=?ip_addr, "IP address is in a blocked subnet, not dialing"); - kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_subnet", "peer" => peer_id.to_string()); - return Err(DialError::SubnetBlocked { ip: ip_addr }); + Some(Err(())) => { + // DNS resolution failed - allow the dial, libp2p will handle it. + debug!(target: "gossip", peer=?addr, "DNS resolution failed, allowing dial"); + return Ok(()); } + None => Self::ip_from_addr(addr).ok_or_else(|| { + warn!(target: "p2p", peer=?addr, "Failed to extract IpAddr from Multiaddr"); + DialError::InvalidIpAddress { addr: addr.clone() } + })?, + }; + + // If the address is blocked, do not dial. + if self.blocked_addrs.contains(&ip_addr) { + debug!(target: "gossip", peer=?addr, "Address is blocked, not dialing"); + self.connectedness.insert(peer_id, Connectedness::CannotConnect); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_address", "peer" => peer_id.to_string()); + return Err(DialError::AddressBlocked { ip: ip_addr }); + } + + // If address lies in any blocked subnets, do not dial. + if self.check_ip_in_blocked_subnets(&ip_addr) { + debug!(target: "gossip", ip=?ip_addr, "IP address is in a blocked subnet, not dialing"); + kona_macros::inc!(gauge, crate::Metrics::DIAL_PEER_ERROR, "type" => "blocked_subnet", "peer" => peer_id.to_string()); + return Err(DialError::SubnetBlocked { ip: ip_addr }); } Ok(()) @@ -368,6 +434,53 @@ fn test_dial_error_handling() { assert!(matches!(result, Err(DialError::AlreadyDialing { .. }))); } +#[test] +fn test_dns_multiaddr_detection() { + use std::str::FromStr; + + // Test DNS4 multiaddr (try_resolve_dns returns Some for DNS addresses) + let dns4_addr = Multiaddr::from_str( + "/dns4/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns4_addr).is_some()); + + // Test DNS6 multiaddr + let dns6_addr = Multiaddr::from_str( + "/dns6/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns6_addr).is_some()); + + // Test DNS multiaddr (generic) + let dns_addr = Multiaddr::from_str( + "/dns/example.com/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dns_addr).is_some()); + + // Test dnsaddr multiaddr + let dnsaddr = Multiaddr::from_str( + "/dnsaddr/example.com/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&dnsaddr).is_some()); + + // Test IP4 multiaddr (should NOT be detected as DNS - returns None) + let ip4_addr = Multiaddr::from_str( + "/ip4/127.0.0.1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&ip4_addr).is_none()); + + // Test IP6 multiaddr (should NOT be detected as DNS - returns None) + let ip6_addr = Multiaddr::from_str( + "/ip6/::1/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + assert!(ConnectionGater::try_resolve_dns(&ip6_addr).is_none()); +} + #[test] fn test_dns_multiaddr_can_dial() { use crate::ConnectionGate; @@ -394,3 +507,51 @@ fn test_dns_multiaddr_can_dial() { gater.block_peer(&peer_id); assert!(gater.can_dial(&dns4_addr).is_err()); } + +#[test] +fn test_dns_multiaddr_blocked_by_resolved_ip() { + use crate::{ConnectionGate, DialError}; + use std::{net::IpAddr, str::FromStr}; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // localhost resolves to 127.0.0.1 + let dns_localhost = Multiaddr::from_str( + "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + // Should succeed before blocking + assert!(gater.can_dial(&dns_localhost).is_ok()); + + // Block 127.0.0.1 + gater.block_addr(IpAddr::from_str("127.0.0.1").unwrap()); + + // Should now fail because localhost resolves to blocked IP + let result = gater.can_dial(&dns_localhost); + assert!(matches!(result, Err(DialError::AddressBlocked { .. }))); +} + +#[test] +fn test_dns_multiaddr_blocked_by_subnet() { + use crate::{ConnectionGate, DialError}; + use std::str::FromStr; + + let mut gater = ConnectionGater::new(GaterConfig::default()); + + // localhost resolves to 127.0.0.1 + let dns_localhost = Multiaddr::from_str( + "/dns4/localhost/tcp/9003/p2p/12D3KooWEyoppNCUx8Yx66oV9fJnriXwCcXwDDUA2kj6vnc6iDEp", + ) + .unwrap(); + + // Should succeed before blocking + assert!(gater.can_dial(&dns_localhost).is_ok()); + + // Block the 127.0.0.0/8 subnet + gater.block_subnet("127.0.0.0/8".parse().unwrap()); + + // Should now fail because localhost resolves to IP in blocked subnet + let result = gater.can_dial(&dns_localhost); + assert!(matches!(result, Err(DialError::SubnetBlocked { .. }))); +}