diff --git a/CHANGELOG.md b/CHANGELOG.md index d57ea13843c..bf36ad09845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [2151](https://github.com/FuelLabs/fuel-core/pull/2151): Added limitations on gas used during dry_run in API. - [2188](https://github.com/FuelLabs/fuel-core/pull/2188): Added the new variant `V2` for the `ConsensusParameters` which contains the new `block_transaction_size_limit` parameter. - [2163](https://github.com/FuelLabs/fuel-core/pull/2163): Added runnable task for fetching block committer data. +- [2204](https://github.com/FuelLabs/fuel-core/pull/2204): Added `dnsaddr` resolution for TLD without suffixes. ### Changed diff --git a/Cargo.lock b/Cargo.lock index f20a6fe7f1a..20e56680478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3498,6 +3498,7 @@ dependencies = [ "fuel-core-types 0.35.0", "futures", "hex", + "hickory-resolver", "ip_network", "libp2p", "libp2p-mplex", diff --git a/crates/fuel-core/src/p2p_test_helpers.rs b/crates/fuel-core/src/p2p_test_helpers.rs index d5bb4af1976..364ea3976bb 100644 --- a/crates/fuel-core/src/p2p_test_helpers.rs +++ b/crates/fuel-core/src/p2p_test_helpers.rs @@ -134,13 +134,13 @@ pub struct NamedNodes(pub HashMap); impl Bootstrap { /// Spawn a bootstrap node. - pub async fn new(node_config: &Config) -> Self { + pub async fn new(node_config: &Config) -> anyhow::Result { let bootstrap_config = extract_p2p_config(node_config).await; let codec = PostcardCodec::new(bootstrap_config.max_block_size); let (sender, _) = broadcast::channel(bootstrap_config.reserved_nodes.len().saturating_add(1)); - let mut bootstrap = FuelP2PService::new(sender, bootstrap_config, codec); - bootstrap.start().await.unwrap(); + let mut bootstrap = FuelP2PService::new(sender, bootstrap_config, codec).await?; + bootstrap.start().await?; let listeners = bootstrap.multiaddrs(); let (kill, mut shutdown) = broadcast::channel(1); @@ -169,7 +169,7 @@ impl Bootstrap { } }); - Bootstrap { listeners, kill } + Ok(Bootstrap { listeners, kill }) } pub fn listeners(&self) -> Vec { @@ -269,7 +269,9 @@ pub async fn make_nodes( if let Some(BootstrapSetup { pub_key, .. }) = boot { update_signing_key(&mut node_config, pub_key); } - Bootstrap::new(&node_config).await + Bootstrap::new(&node_config) + .await + .expect("Failed to create bootstrap node") } }) .collect() diff --git a/crates/services/p2p/Cargo.toml b/crates/services/p2p/Cargo.toml index 7557b294b44..e7a27877ee0 100644 --- a/crates/services/p2p/Cargo.toml +++ b/crates/services/p2p/Cargo.toml @@ -20,6 +20,7 @@ fuel-core-storage = { workspace = true, features = ["std"] } fuel-core-types = { workspace = true, features = ["std", "serde"] } futures = { workspace = true } hex = { workspace = true } +hickory-resolver = "0.24.1" ip_network = "0.4" libp2p = { version = "0.53.2", default-features = false, features = [ "dns", diff --git a/crates/services/p2p/src/behavior.rs b/crates/services/p2p/src/behavior.rs index 1feac658f59..2b689eb3949 100644 --- a/crates/services/p2p/src/behavior.rs +++ b/crates/services/p2p/src/behavior.rs @@ -65,7 +65,7 @@ pub struct FuelBehaviour { } impl FuelBehaviour { - pub(crate) fn new(p2p_config: &Config, codec: PostcardCodec) -> Self { + pub(crate) fn new(p2p_config: &Config, codec: PostcardCodec) -> anyhow::Result { let local_public_key = p2p_config.keypair.public(); let local_peer_id = PeerId::from_public_key(&local_public_key); @@ -93,7 +93,7 @@ impl FuelBehaviour { let gossipsub = build_gossipsub_behaviour(p2p_config); - let peer_report = peer_report::Behaviour::new(p2p_config); + let peer_report = peer_report::Behaviour::new(&p2p_config.reserved_nodes); let identify = { let identify_config = identify::Config::new( @@ -125,15 +125,16 @@ impl FuelBehaviour { req_res_config, ); - Self { - discovery: discovery_config.finish(), + let discovery = discovery_config.finish()?; + Ok(Self { + discovery, gossipsub, peer_report, request_response, blocked_peer: Default::default(), identify, heartbeat, - } + }) } pub fn add_addresses_to_discovery( diff --git a/crates/services/p2p/src/discovery.rs b/crates/services/p2p/src/discovery.rs index c92d2238119..725024c4939 100644 --- a/crates/services/p2p/src/discovery.rs +++ b/crates/services/p2p/src/discovery.rs @@ -39,6 +39,7 @@ use std::{ use tracing::trace; mod discovery_config; mod mdns_wrapper; + pub use discovery_config::Config; const SIXTY_SECONDS: Duration = Duration::from_secs(60); @@ -265,7 +266,7 @@ mod tests { .with_bootstrap_nodes(bootstrap_nodes) .with_random_walk(Duration::from_millis(500)); - config.finish() + config.finish().expect("Config should be valid") } } diff --git a/crates/services/p2p/src/discovery/discovery_config.rs b/crates/services/p2p/src/discovery/discovery_config.rs index 9800ca7e8ea..88589c9655f 100644 --- a/crates/services/p2p/src/discovery/discovery_config.rs +++ b/crates/services/p2p/src/discovery/discovery_config.rs @@ -97,7 +97,7 @@ impl Config { self } - pub fn finish(self) -> Behaviour { + pub fn finish(self) -> anyhow::Result { let Config { local_peer_id, bootstrap_nodes, @@ -112,9 +112,8 @@ impl Config { let memory_store = MemoryStore::new(local_peer_id.to_owned()); let mut kademlia_config = kad::Config::default(); let network = format!("/fuel/kad/{network_name}/kad/1.0.0"); - kademlia_config.set_protocol_names(vec![ - StreamProtocol::try_from_owned(network).expect("Invalid kad protocol") - ]); + kademlia_config + .set_protocol_names(vec![StreamProtocol::try_from_owned(network)?]); let mut kademlia = kad::Behaviour::with_config(local_peer_id, memory_store, kademlia_config); @@ -167,13 +166,13 @@ impl Config { MdnsWrapper::disabled() }; - Behaviour { + Ok(Behaviour { connected_peers: HashSet::new(), kademlia, next_kad_random_walk, duration_to_next_kad: Duration::from_secs(1), max_peers_connected, mdns, - } + }) } } diff --git a/crates/services/p2p/src/dnsaddr_resolution.rs b/crates/services/p2p/src/dnsaddr_resolution.rs new file mode 100644 index 00000000000..2daae77e76e --- /dev/null +++ b/crates/services/p2p/src/dnsaddr_resolution.rs @@ -0,0 +1,111 @@ +use anyhow::anyhow; +use hickory_resolver::TokioAsyncResolver; +use libp2p::Multiaddr; +use std::pin::Pin; + +/// The prefix for `dnsaddr` protocol TXT record lookups. +const DNSADDR_PREFIX: &str = "_dnsaddr."; +/// The maximum number of DNS lookups when dialing. +/// This limit is for preventing malicious or misconfigured DNS records from causing infinite recursion. +const MAX_DNS_LOOKUPS: usize = 10; + +pub(crate) struct DnsResolver { + resolver: TokioAsyncResolver, +} + +impl DnsResolver { + pub(crate) async fn lookup_dnsaddr( + &self, + addr: &str, + ) -> anyhow::Result> { + self.resolve_recursive(addr, 0).await + } + + pub(crate) async fn new() -> anyhow::Result> { + let resolver = TokioAsyncResolver::tokio_from_system_conf()?; + Ok(Box::new(Self { resolver })) + } + + /// Internal method to handle recursive DNS lookups. + fn resolve_recursive<'a>( + &'a self, + addr: &'a str, + depth: usize, + ) -> Pin< + Box>> + Send + 'a>, + > { + Box::pin(async move { + if depth >= MAX_DNS_LOOKUPS { + return Err(anyhow!("Maximum DNS lookup depth exceeded")); + } + + let mut multiaddrs = vec![]; + let dns_lookup_url = format!("{}{}", DNSADDR_PREFIX, addr); + let txt_records = self.resolver.txt_lookup(dns_lookup_url).await?; + + for record in txt_records { + let parsed = record.to_string(); + if !parsed.starts_with("dnsaddr") { + continue; + } + + let dnsaddr_value = parsed + .split("dnsaddr=") + .nth(1) + .ok_or_else(|| anyhow!("Invalid DNS address: {:?}", parsed))?; + + // Check if the parsed value is a multiaddress or another dnsaddr. + if dnsaddr_value.starts_with("/dnsaddr") { + let nested_dnsaddr = dnsaddr_value + .split('/') + .nth(2) + .ok_or_else(|| anyhow!("Invalid nested dnsaddr"))?; + // Recursively resolve the nested dnsaddr + #[allow(clippy::arithmetic_side_effects)] + let nested_addrs = + self.resolve_recursive(nested_dnsaddr, depth + 1).await?; + multiaddrs.extend(nested_addrs); + } else if let Ok(multiaddr) = dnsaddr_value.parse::() { + multiaddrs.push(multiaddr); + } + } + + Ok(multiaddrs) + }) + } +} + +#[allow(non_snake_case)] +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashSet; + + #[tokio::test] + async fn dns_resolver__parses_all_multiaddresses_from_mainnet_dnsaddr_entry() { + // given + let resolver = DnsResolver::new().await.unwrap(); + let expected_multiaddrs: HashSet = [ + "/dns/p2p-mainnet.fuel.network/tcp/30336/p2p/16Uiu2HAkxjhwNYtwawWUexYn84MsrA9ivFWkNHmiF4hSieoNP7Jd", + "/dns/p2p-mainnet.fuel.network/tcp/30337/p2p/16Uiu2HAmQunK6Dd81BXh3rW2ZsszgviPgGMuHw39vv2XxbkuCfaw", + "/dns/p2p-mainnet.fuel.network/tcp/30333/p2p/16Uiu2HAkuiLZNrfecgDYHJZV5LoEtCXqqRCqHY3yLBqs4LQk8jJg", + "/dns/p2p-mainnet.fuel.network/tcp/30334/p2p/16Uiu2HAkzYNa6yMykppS1ij69mKoKjrZEr11oHGiM5Mpc8nKjVDM", + "/dns/p2p-mainnet.fuel.network/tcp/30335/p2p/16Uiu2HAm5yqpTv1QVk3SepUYzeKXTWMuE2VqMWHq5qQLPR2Udg6s" + ].iter().map(|s| s.parse().unwrap()).collect(); + + // when + // run a `dig +short txt _dnsaddr.mainnet.fuel.network` to get the TXT records + let multiaddrs = resolver + .lookup_dnsaddr("mainnet.fuel.network") + .await + .unwrap(); + // then + for multiaddr in multiaddrs.iter() { + assert!( + expected_multiaddrs.contains(multiaddr), + "Unexpected multiaddr: {:?}", + multiaddr + ); + } + } +} diff --git a/crates/services/p2p/src/lib.rs b/crates/services/p2p/src/lib.rs index 7acb1d27991..f14599612bc 100644 --- a/crates/services/p2p/src/lib.rs +++ b/crates/services/p2p/src/lib.rs @@ -5,6 +5,7 @@ pub mod behavior; pub mod codecs; pub mod config; pub mod discovery; +mod dnsaddr_resolution; pub mod gossipsub; pub mod heartbeat; pub mod heavy_task_processor; @@ -23,6 +24,7 @@ pub use libp2p::{ Multiaddr, PeerId, }; +use tracing::warn; #[cfg(feature = "test-helpers")] pub mod network_service { @@ -38,6 +40,13 @@ impl TryPeerId for Multiaddr { fn try_to_peer_id(&self) -> Option { self.iter().last().and_then(|p| match p { Protocol::P2p(peer_id) => Some(peer_id), + Protocol::Dnsaddr(multiaddr) => { + warn!( + "synchronous recursive dnsaddr resolution is not yet supported: {:?}", + multiaddr + ); + None + } _ => None, }) } diff --git a/crates/services/p2p/src/p2p_service.rs b/crates/services/p2p/src/p2p_service.rs index e1afcad3a60..a01c17b4499 100644 --- a/crates/services/p2p/src/p2p_service.rs +++ b/crates/services/p2p/src/p2p_service.rs @@ -11,6 +11,7 @@ use crate::{ build_transport_function, Config, }, + dnsaddr_resolution::DnsResolver, gossipsub::{ messages::{ GossipTopicTag, @@ -168,20 +169,53 @@ pub enum FuelP2PEvent { }, } +async fn parse_multiaddrs(multiaddrs: Vec) -> anyhow::Result> { + let dnsaddr_urls = multiaddrs + .iter() + .filter_map(|multiaddr| { + if let Protocol::Dnsaddr(dnsaddr_url) = multiaddr.iter().next()? { + Some(dnsaddr_url.clone()) + } else { + None + } + }) + .collect::>(); + + let dns_resolver = DnsResolver::new().await?; + let mut dnsaddr_multiaddrs = vec![]; + + for dnsaddr in dnsaddr_urls { + let multiaddrs = dns_resolver.lookup_dnsaddr(dnsaddr.as_ref()).await?; + dnsaddr_multiaddrs.extend(multiaddrs); + } + + let resolved_multiaddrs = multiaddrs + .into_iter() + .filter(|multiaddr| !multiaddr.iter().any(|p| matches!(p, Protocol::Dnsaddr(_)))) + .chain(dnsaddr_multiaddrs.into_iter()) + .collect(); + Ok(resolved_multiaddrs) +} + impl FuelP2PService { - pub fn new( + pub async fn new( reserved_peers_updates: broadcast::Sender, config: Config, codec: PostcardCodec, - ) -> Self { + ) -> anyhow::Result { let gossipsub_data = GossipsubData::with_topics(GossipsubTopics::new(&config.network_name)); let network_metadata = NetworkMetadata { gossipsub_data }; + let mut config = config; + // override the configuration with the parsed multiaddrs from dnsaddr resolution + config.bootstrap_nodes = parse_multiaddrs(config.bootstrap_nodes).await?; + config.reserved_nodes = parse_multiaddrs(config.reserved_nodes).await?; + // configure and build P2P Service let (transport_function, connection_state) = build_transport_function(&config); let tcp_config = tcp::Config::new().port_reuse(true); - let behaviour = FuelBehaviour::new(&config, codec.clone()); + let behaviour = FuelBehaviour::new(&config, codec.clone())?; let mut swarm = SwarmBuilder::with_existing_identity(config.keypair.clone()) .with_tokio() @@ -190,11 +224,9 @@ impl FuelP2PService { transport_function, libp2p::yamux::Config::default, ) - .unwrap() - .with_dns() - .unwrap() - .with_behaviour(|_| behaviour) - .unwrap() + .map_err(|_| anyhow::anyhow!("Failed to build Swarm"))? + .with_dns()? + .with_behaviour(|_| behaviour)? .with_swarm_config(|cfg| { if let Some(timeout) = config.connection_idle_timeout { cfg.with_idle_connection_timeout(timeout) @@ -218,7 +250,7 @@ impl FuelP2PService { .filter_map(|m| m.try_to_peer_id()) .collect(); - Self { + Ok(Self { local_peer_id, local_address: config.address, tcp_port: config.tcp_port, @@ -234,7 +266,7 @@ impl FuelP2PService { connection_state, config.max_peers_connected as usize, ), - } + }) } pub async fn start(&mut self) -> anyhow::Result<()> { @@ -792,7 +824,9 @@ mod tests { broadcast::channel(p2p_config.reserved_nodes.len().saturating_add(1)); let mut service = - FuelP2PService::new(sender, p2p_config, PostcardCodec::new(max_block_size)); + FuelP2PService::new(sender, p2p_config, PostcardCodec::new(max_block_size)) + .await + .unwrap(); service.start().await.unwrap(); service } diff --git a/crates/services/p2p/src/peer_report.rs b/crates/services/p2p/src/peer_report.rs index 2c4579d7e2b..193395d9f2c 100644 --- a/crates/services/p2p/src/peer_report.rs +++ b/crates/services/p2p/src/peer_report.rs @@ -1,7 +1,4 @@ -use crate::{ - config::Config, - TryPeerId, -}; +use crate::TryPeerId; use libp2p::{ self, core::Endpoint, @@ -75,14 +72,16 @@ pub struct Behaviour { } impl Behaviour { - pub(crate) fn new(_config: &Config) -> Self { + pub(crate) fn new(reserved_nodes_multiaddrs: &[Multiaddr]) -> Self { let mut reserved_nodes_to_connect = VecDeque::new(); - let mut reserved_nodes_multiaddr = BTreeMap::>::new(); + let mut reserved_nodes_multiaddr_map = BTreeMap::>::new(); - for multiaddr in &_config.reserved_nodes { - let peer_id = multiaddr.try_to_peer_id().unwrap(); + for multiaddr in reserved_nodes_multiaddrs { + let peer_id = multiaddr + .try_to_peer_id() + .expect("Multiaddr MUST have a PeerId"); reserved_nodes_to_connect.push_back((Instant::now(), peer_id)); - reserved_nodes_multiaddr + reserved_nodes_multiaddr_map .entry(peer_id) .or_default() .push(multiaddr.clone()); @@ -90,7 +89,7 @@ impl Behaviour { Self { reserved_nodes_to_connect, - reserved_nodes_multiaddr, + reserved_nodes_multiaddr: reserved_nodes_multiaddr_map, connected_reserved_nodes: Default::default(), pending_connections: Default::default(), pending_events: VecDeque::default(), diff --git a/crates/services/p2p/src/service.rs b/crates/services/p2p/src/service.rs index bf875289743..584de8ac13b 100644 --- a/crates/services/p2p/src/service.rs +++ b/crates/services/p2p/src/service.rs @@ -618,7 +618,8 @@ where broadcast.reserved_peers_broadcast.clone(), config, PostcardCodec::new(max_block_size), - ); + ) + .await?; p2p_service.start().await?; let next_check_time = diff --git a/tests/tests/poa.rs b/tests/tests/poa.rs index c64085e15e7..fca05616275 100644 --- a/tests/tests/poa.rs +++ b/tests/tests/poa.rs @@ -274,7 +274,7 @@ mod p2p { update_signing_key(&mut config, pub_key); let bootstrap_config = make_config("Bootstrap".to_string(), config.clone()); - let bootstrap = Bootstrap::new(&bootstrap_config).await; + let bootstrap = Bootstrap::new(&bootstrap_config).await.unwrap(); let make_node_config = |name: &str| { let mut config = make_config(name.to_string(), config.clone());