diff --git a/Cargo.lock b/Cargo.lock index 6c3ab0175..08da8e2bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3845,6 +3845,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if-addrs" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78a89907582615b19f6f0da1af18abf6ff08be259395669b834b057a7ee92d8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "impl-codec" version = "0.6.0" @@ -7435,6 +7445,7 @@ name = "reth-net-nat" version = "1.0.6" dependencies = [ "futures-util", + "if-addrs", "reqwest", "reth-tracing", "serde_with", diff --git a/Cargo.toml b/Cargo.toml index 2e1eeb8b7..aca84d650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -536,6 +536,7 @@ tower-http = "0.5" # p2p discv5 = "0.7.0" +if-addrs = "0.13" # rpc jsonrpsee = "0.24" diff --git a/book/cli/reth/debug/execution.md b/book/cli/reth/debug/execution.md index 9935bd2d7..c7ffc05e8 100644 --- a/book/cli/reth/debug/execution.md +++ b/book/cli/reth/debug/execution.md @@ -228,6 +228,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + --to The maximum block height diff --git a/book/cli/reth/debug/in-memory-merkle.md b/book/cli/reth/debug/in-memory-merkle.md index 972abef59..c4814a5f9 100644 --- a/book/cli/reth/debug/in-memory-merkle.md +++ b/book/cli/reth/debug/in-memory-merkle.md @@ -228,6 +228,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + --retries The number of retries per request diff --git a/book/cli/reth/debug/merkle.md b/book/cli/reth/debug/merkle.md index 322d180c2..cdb6d0ff3 100644 --- a/book/cli/reth/debug/merkle.md +++ b/book/cli/reth/debug/merkle.md @@ -228,6 +228,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + --retries The number of retries per request diff --git a/book/cli/reth/debug/replay-engine.md b/book/cli/reth/debug/replay-engine.md index 5d21ac738..8c1dd3277 100644 --- a/book/cli/reth/debug/replay-engine.md +++ b/book/cli/reth/debug/replay-engine.md @@ -228,6 +228,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + --engine-api-store The path to read engine API messages from diff --git a/book/cli/reth/node.md b/book/cli/reth/node.md index 1ee2097ea..0438f67ee 100644 --- a/book/cli/reth/node.md +++ b/book/cli/reth/node.md @@ -220,6 +220,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + RPC: --http Enable the HTTP-RPC server diff --git a/book/cli/reth/p2p.md b/book/cli/reth/p2p.md index 48447cc1b..5a9078689 100644 --- a/book/cli/reth/p2p.md +++ b/book/cli/reth/p2p.md @@ -205,6 +205,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + Datadir: --datadir The path to the data dir for all reth files and subdirectories. diff --git a/book/cli/reth/stage/run.md b/book/cli/reth/stage/run.md index d50178ebf..1ec80064a 100644 --- a/book/cli/reth/stage/run.md +++ b/book/cli/reth/stage/run.md @@ -271,6 +271,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + Logging: --log.stdout.format The format to use for logs written to stdout diff --git a/book/cli/reth/stage/unwind.md b/book/cli/reth/stage/unwind.md index 86c92ab70..b0d3bd300 100644 --- a/book/cli/reth/stage/unwind.md +++ b/book/cli/reth/stage/unwind.md @@ -233,6 +233,11 @@ Networking: [default: 25600] + --net-if.experimental + Name of network interface used to communicate with peers. + + If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + --offline If this is enabled, then all stages except headers, bodies, and sender recovery will be unwound diff --git a/crates/net/nat/Cargo.toml b/crates/net/nat/Cargo.toml index 8ab78d46a..99f209616 100644 --- a/crates/net/nat/Cargo.toml +++ b/crates/net/nat/Cargo.toml @@ -17,6 +17,7 @@ reqwest.workspace = true serde_with = { workspace = true, optional = true } thiserror.workspace = true tokio = { workspace = true, features = ["time"] } +if-addrs.workspace = true [dev-dependencies] reth-tracing.workspace = true diff --git a/crates/net/nat/src/lib.rs b/crates/net/nat/src/lib.rs index 8f7579089..e58edae05 100644 --- a/crates/net/nat/src/lib.rs +++ b/crates/net/nat/src/lib.rs @@ -12,6 +12,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +pub mod net_if; + +pub use net_if::{NetInterfaceError, DEFAULT_NET_IF_NAME}; + use std::{ fmt, future::{poll_fn, Future}, diff --git a/crates/net/nat/src/net_if.rs b/crates/net/nat/src/net_if.rs new file mode 100644 index 000000000..d93dd8dd5 --- /dev/null +++ b/crates/net/nat/src/net_if.rs @@ -0,0 +1,55 @@ +//! IP resolution on non-host Docker network. + +#![cfg(not(target_os = "windows"))] + +use std::{io, net::IpAddr}; + +/// The 'eth0' interface tends to be the default interface that docker containers use to +/// communicate with each other. +pub const DEFAULT_NET_IF_NAME: &str = "eth0"; + +/// Errors resolving network interface IP. +#[derive(Debug, thiserror::Error)] +pub enum NetInterfaceError { + /// Error reading OS interfaces. + #[error("failed to read OS interfaces: {0}")] + Io(io::Error), + /// No interface found with given name. + #[error("interface not found: {0}, found other interfaces: {1:?}")] + IFNotFound(String, Vec), +} + +/// Reads IP of OS interface with given name, if exists. +#[cfg(not(target_os = "windows"))] +pub fn resolve_net_if_ip(if_name: &str) -> Result { + match if_addrs::get_if_addrs() { + Ok(ifs) => { + let ip = ifs.iter().find(|i| i.name == if_name).map(|i| i.ip()); + match ip { + Some(ip) => Ok(ip), + None => { + let ifs = ifs.into_iter().map(|i| i.name.as_str().into()).collect(); + Err(NetInterfaceError::IFNotFound(if_name.into(), ifs)) + } + } + } + Err(err) => Err(NetInterfaceError::Io(err)), + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use super::*; + + #[test] + fn read_docker_if_addr() { + const LOCALHOST_IF: [&str; 2] = ["lo0", "lo"]; + + let ip = resolve_net_if_ip(LOCALHOST_IF[0]) + .unwrap_or_else(|_| resolve_net_if_ip(LOCALHOST_IF[1]).unwrap()); + + assert_eq!(ip, Ipv4Addr::LOCALHOST); + } +} diff --git a/crates/node/core/src/args/network.rs b/crates/node/core/src/args/network.rs index 45aef0b2b..b033be4ac 100644 --- a/crates/node/core/src/args/network.rs +++ b/crates/node/core/src/args/network.rs @@ -15,7 +15,7 @@ use reth_discv5::{ discv5::ListenConfig, DEFAULT_COUNT_BOOTSTRAP_LOOKUPS, DEFAULT_DISCOVERY_V5_PORT, DEFAULT_SECONDS_BOOTSTRAP_LOOKUP_INTERVAL, DEFAULT_SECONDS_LOOKUP_INTERVAL, }; -use reth_net_nat::NatResolver; +use reth_net_nat::{NatResolver, DEFAULT_NET_IF_NAME}; use reth_network::{ transactions::{ constants::{ @@ -35,6 +35,7 @@ use reth_network::{ }; use reth_network_peers::{mainnet_nodes, TrustedPeer}; use secp256k1::SecretKey; +use tracing::error; use crate::version::P2P_CLIENT_VERSION; @@ -148,9 +149,38 @@ pub struct NetworkArgs { /// Max capacity of cache of hashes for transactions pending fetch. #[arg(long = "max-tx-pending-fetch", value_name = "COUNT", default_value_t = DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, verbatim_doc_comment)] pub max_capacity_cache_txns_pending_fetch: u32, + + /// Name of network interface used to communicate with peers. + /// + /// If flag is set, but no value is passed, the default interface for docker `eth0` is tried. + #[cfg(not(target_os = "windows"))] + #[arg(long = "net-if.experimental", conflicts_with = "addr", value_name = "IF_NAME")] + pub net_if: Option, } impl NetworkArgs { + /// Returns the resolved IP address. + pub fn resolved_addr(&self) -> IpAddr { + #[cfg(not(target_os = "windows"))] + if let Some(ref if_name) = self.net_if { + let if_name = if if_name.is_empty() { DEFAULT_NET_IF_NAME } else { if_name }; + return match reth_net_nat::net_if::resolve_net_if_ip(if_name) { + Ok(addr) => addr, + Err(err) => { + error!(target: "reth::cli", + if_name, + %err, + "Failed to read network interface IP" + ); + + DEFAULT_DISCOVERY_ADDR + } + } + } + + self.addr + } + /// Returns the resolved bootnodes if any are provided. pub fn resolved_bootnodes(&self) -> Option> { self.bootnodes.clone().map(|bootnodes| { @@ -176,6 +206,7 @@ impl NetworkArgs { secret_key: SecretKey, default_peers_file: PathBuf, ) -> NetworkConfigBuilder { + let addr = self.resolved_addr(); let chain_bootnodes = self .resolved_bootnodes() .unwrap_or_else(|| chain_spec.bootnodes().unwrap_or_else(mainnet_nodes)); @@ -224,11 +255,11 @@ impl NetworkArgs { }) // apply discovery settings .apply(|builder| { - let rlpx_socket = (self.addr, self.port).into(); + let rlpx_socket = (addr, self.port).into(); self.discovery.apply_to_builder(builder, rlpx_socket, chain_bootnodes) }) .listener_addr(SocketAddr::new( - self.addr, // set discovery port based on instance number + addr, // set discovery port based on instance number self.port, )) .discovery_addr(SocketAddr::new( @@ -303,6 +334,7 @@ impl Default for NetworkArgs { max_pending_pool_imports: DEFAULT_MAX_COUNT_PENDING_POOL_IMPORTS, max_seen_tx_history: DEFAULT_MAX_COUNT_TRANSACTIONS_SEEN_BY_PEER, max_capacity_cache_txns_pending_fetch: DEFAULT_MAX_CAPACITY_CACHE_PENDING_FETCH, + net_if: None, } } }