diff --git a/Cargo.lock b/Cargo.lock index af829ae2..a8719f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e30ab0d3e3c32976f67fc1a96179989e45a69594af42003a6663332f9b0bb9d" +checksum = "f7ea09cffa9ad82f6404e6ab415ea0c41a7674c0f2e2e689cb8683f772b5940d" dependencies = [ "alloy-eips", "alloy-primitives", @@ -239,9 +239,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85157b7be31fc4adf6acfefcb0d4308cba5dbd7a8d8e62bcc02ff37d6131a" +checksum = "691fed81bbafefae0f5a6cedd837ebb3fade46e7d91c5b67a463af12ecf5b11a" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -499,9 +499,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a0d2d5c64881f3723232eaaf6c2d9f4f88b061c63e87194b2db785ff3aa31f" +checksum = "75a755a3cc0297683c2879bbfe2ff22778f35068f07444f0b52b5b87570142b6" dependencies = [ "alloy-primitives", "serde", @@ -668,9 +668,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2183706e24173309b0ab0e34d3e53cf3163b71a419803b2b3b0c1fb7ff7a941" +checksum = "f17272de4df6b8b59889b264f0306eba47a69f23f57f1c08f1366a4617b48c30" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -1155,10 +1155,13 @@ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", + "hyper", + "hyper-util", "itoa", "matchit", "memchr", @@ -1166,10 +1169,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", + "tokio", "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1188,6 +1196,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1756,17 +1765,45 @@ name = "charon-p2p" version = "1.7.1" dependencies = [ "anyhow", + "axum", + "charon-core", "charon-eth2", "charon-k1util", "charon-testutil", + "charon-tracing", "chrono", "clap", "k256", "libp2p", "rand 0.8.5", + "serde", + "serde_json", "tempfile", "thiserror 2.0.17", "tokio", + "tokio-util", + "tracing", + "vise", + "vise-exporter", +] + +[[package]] +name = "charon-relay-server" +version = "1.7.1" +dependencies = [ + "axum", + "bon", + "charon-core", + "charon-eth2", + "charon-p2p", + "charon-tracing", + "k256", + "libp2p", + "rand 0.8.5", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", "vise", "vise-exporter", ] @@ -7450,6 +7487,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 0fa76dac..7776947b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/charon-testutil", "crates/tracing", "crates/eth2api", + "crates/relay-server" ] resolver = "3" @@ -28,9 +29,10 @@ alloy = { version = "1.3", features = ["essentials"] } built = { version = "0.8.0", features = ["git2", "chrono", "cargo-lock"] } blst = "0.3" anyhow = "1" +axum = "0.8.6" cancellation = "0.1.0" chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4.5", features = ["derive", "env", "cargo"] } +clap = { version = "4.5.53", features = ["derive", "env", "cargo"] } crossbeam = "0.8.4" backon = "1.6.0" hex = { version = "0.4.3" } @@ -43,7 +45,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } thiserror = "2.0" tokio = { version = "1", features = ["full"] } -tokio-util = { version = "0.7" } +tokio-util = "0.7.11" libp2p = { version = "0.56", features = ["full", "secp256k1"] } url = "2.5" uuid = { version = "1.19", features = ["serde", "v4"] } @@ -76,6 +78,7 @@ charon-k1util = { path = "crates/charon-k1util" } charon-p2p = { path = "crates/charon-p2p" } charon-testutil = { path = "crates/charon-testutil" } charon-tracing = { path = "crates/tracing" } +charon-relay-server = { path = "crates/relay-server" } eth2api = { path = "crates/eth2api" } [workspace.lints.rust] diff --git a/crates/charon-p2p/Cargo.toml b/crates/charon-p2p/Cargo.toml index aa93703e..88b2d0b0 100644 --- a/crates/charon-p2p/Cargo.toml +++ b/crates/charon-p2p/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true publish.workspace = true [dependencies] +axum.workspace = true chrono.workspace = true libp2p.workspace = true thiserror.workspace = true @@ -15,8 +16,14 @@ charon-eth2.workspace = true charon-k1util.workspace = true vise.workspace = true tokio.workspace = true +tokio-util.workspace = true rand.workspace = true tempfile.workspace = true +charon-tracing.workspace = true +tracing.workspace = true +serde.workspace = true +serde_json.workspace = true +charon-core.workspace = true [dev-dependencies] charon-testutil.workspace = true diff --git a/crates/charon-p2p/examples/p2p.rs b/crates/charon-p2p/examples/p2p.rs index c262b38e..491d1032 100644 --- a/crates/charon-p2p/examples/p2p.rs +++ b/crates/charon-p2p/examples/p2p.rs @@ -12,7 +12,6 @@ use charon_p2p::{ pluto_mdns::{PlutoMdnsBehaviour, PlutoMdnsBehaviourEvent}, }, config::P2PConfig, - gater::ConnGater, p2p::{Node, NodeType}, }; use clap::Parser; @@ -40,7 +39,6 @@ async fn main() -> Result<()> { let mut p2p: Node<_> = Node::new( P2PConfig::default(), key.clone(), - ConnGater::new_open_gater(), false, NodeType::QUIC, PlutoMdnsBehaviour::new, diff --git a/crates/charon-p2p/src/behaviours/pluto.rs b/crates/charon-p2p/src/behaviours/pluto.rs index 31b8e915..a1a46a50 100644 --- a/crates/charon-p2p/src/behaviours/pluto.rs +++ b/crates/charon-p2p/src/behaviours/pluto.rs @@ -1,11 +1,12 @@ //! Pluto behaviour. -use std::time::Duration; +use std::sync::LazyLock; use libp2p::{identify, identity::Keypair, ping, relay, swarm::NetworkBehaviour}; -use crate::gater::ConnGater; +use crate::{config::default_ping_config, gater::ConnGater}; +/// Pluto network behaviour. #[derive(NetworkBehaviour)] pub struct PlutoBehaviour { /// Connection gater behaviour. @@ -19,20 +20,80 @@ pub struct PlutoBehaviour { } impl PlutoBehaviour { - /// Creates a new Pluto behaviour. + /// Creates a new Pluto behaviour with default configuration. pub fn new(key: &Keypair, relay_client: relay::client::Behaviour) -> Self { + PlutoBehaviourBuilder::default().build(key, relay_client) + } + + /// Returns a new builder for configuring a PlutoBehaviour. + pub fn builder() -> PlutoBehaviourBuilder { + PlutoBehaviourBuilder::default() + } +} + +/// The default user agent for the Pluto network. +pub static DEFAULT_USER_AGENT: LazyLock = + LazyLock::new(|| format!("pluto/{}", *charon_core::version::VERSION)); + +/// The default identify protocol for the Pluto network. +pub static DEFAULT_IDENTIFY_PROTOCOL: LazyLock = + LazyLock::new(|| format!("/pluto/{}", *charon_core::version::VERSION)); + +/// Builder for [`PlutoBehaviour`]. +#[derive(Debug, Clone)] +pub struct PlutoBehaviourBuilder { + gater: Option, + identify_protocol: String, + user_agent: String, +} + +impl Default for PlutoBehaviourBuilder { + fn default() -> Self { Self { + gater: None, + identify_protocol: DEFAULT_IDENTIFY_PROTOCOL.clone(), + user_agent: DEFAULT_USER_AGENT.clone(), + } + } +} + +impl PlutoBehaviourBuilder { + /// Creates a new builder with default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Sets the connection gater. + pub fn with_gater(mut self, gater: ConnGater) -> Self { + self.gater = Some(gater); + self + } + + /// Sets the identify protocol string. + pub fn with_identify_protocol(mut self, protocol: impl Into) -> Self { + self.identify_protocol = protocol.into(); + self + } + + /// Sets the user agent string. + pub fn with_user_agent(mut self, user_agent: impl Into) -> Self { + self.user_agent = user_agent.into(); + self + } + + /// Builds the [`PlutoBehaviour`] with the provided keypair and relay + /// client. + pub fn build(self, key: &Keypair, relay_client: relay::client::Behaviour) -> PlutoBehaviour { + PlutoBehaviour { + gater: self + .gater + .unwrap_or_else(|| ConnGater::new_conn_gater(vec![], vec![])), relay: relay_client, - identify: identify::Behaviour::new(identify::Config::new( - "/pluto/1.0.0-alpha".into(), - key.public(), - )), - ping: ping::Behaviour::new( - ping::Config::new() - .with_interval(Duration::from_secs(1)) - .with_timeout(Duration::from_secs(2)), + identify: identify::Behaviour::new( + identify::Config::new(self.identify_protocol, key.public()) + .with_agent_version(self.user_agent), ), - gater: ConnGater::new_conn_gater(vec![], vec![]), + ping: ping::Behaviour::new(default_ping_config()), } } } diff --git a/crates/charon-p2p/src/behaviours/pluto_mdns.rs b/crates/charon-p2p/src/behaviours/pluto_mdns.rs index 27adf186..3d919c2d 100644 --- a/crates/charon-p2p/src/behaviours/pluto_mdns.rs +++ b/crates/charon-p2p/src/behaviours/pluto_mdns.rs @@ -1,9 +1,10 @@ //! Pluto Mdns behaviour. + use libp2p::{identity::Keypair, mdns, relay, swarm::NetworkBehaviour}; -use crate::behaviours::pluto::PlutoBehaviour; +use crate::behaviours::pluto::{PlutoBehaviour, PlutoBehaviourBuilder}; -/// Pluto network behaviour. +/// Pluto network behaviour with mDNS discovery. #[derive(NetworkBehaviour)] pub struct PlutoMdnsBehaviour { /// Pluto behaviour. @@ -13,11 +14,68 @@ pub struct PlutoMdnsBehaviour { } impl PlutoMdnsBehaviour { - /// Creates a new Pluto Mdns behaviour. + /// Creates a new Pluto Mdns behaviour with default configuration. pub fn new(key: &Keypair, relay_client: relay::client::Behaviour) -> Self { - Self { - pluto: PlutoBehaviour::new(key, relay_client), - mdns: mdns::tokio::Behaviour::new(mdns::Config::default(), key.public().to_peer_id()) + PlutoMdnsBehaviourBuilder::default().build(key, relay_client) + } + + /// Returns a new builder for configuring a PlutoMdnsBehaviour. + pub fn builder() -> PlutoMdnsBehaviourBuilder { + PlutoMdnsBehaviourBuilder::default() + } +} + +/// Builder for [`PlutoMdnsBehaviour`]. +#[derive(Default, Debug, Clone)] +pub struct PlutoMdnsBehaviourBuilder { + pluto: PlutoBehaviourBuilder, + mdns_config: mdns::Config, +} + +impl PlutoMdnsBehaviourBuilder { + /// Creates a new builder with default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Replaces the inner [`PlutoBehaviourBuilder`] entirely. + pub fn with_pluto(mut self, pluto: PlutoBehaviourBuilder) -> Self { + self.pluto = pluto; + self + } + + /// Configures the inner [`PlutoBehaviourBuilder`] via a closure. + /// + /// This is ergonomic for inline configuration: + /// ```ignore + /// PlutoMdnsBehaviourBuilder::new() + /// .configure_pluto(|p| p.with_ping_interval(Duration::from_secs(5))) + /// .build(&key, relay_client) + /// ``` + pub fn configure_pluto( + mut self, + f: impl FnOnce(PlutoBehaviourBuilder) -> PlutoBehaviourBuilder, + ) -> Self { + self.pluto = f(self.pluto); + self + } + + /// Sets the mDNS configuration. + pub fn with_mdns_config(mut self, config: mdns::Config) -> Self { + self.mdns_config = config; + self + } + + /// Builds the [`PlutoMdnsBehaviour`] with the provided keypair and relay + /// client. + pub fn build( + self, + key: &Keypair, + relay_client: relay::client::Behaviour, + ) -> PlutoMdnsBehaviour { + PlutoMdnsBehaviour { + pluto: self.pluto.build(key, relay_client), + mdns: mdns::tokio::Behaviour::new(self.mdns_config, key.public().to_peer_id()) .expect("Failed to create mDNS behaviour"), } } diff --git a/crates/charon-p2p/src/config.rs b/crates/charon-p2p/src/config.rs index 10778ef2..c1d0c390 100644 --- a/crates/charon-p2p/src/config.rs +++ b/crates/charon-p2p/src/config.rs @@ -3,9 +3,10 @@ use std::{ net::{IpAddr, SocketAddr}, str::FromStr, + time::Duration, }; -use libp2p::{Multiaddr, multiaddr}; +use libp2p::{Multiaddr, multiaddr, ping}; /// P2P configuration error. #[derive(Debug, thiserror::Error)] @@ -59,10 +60,10 @@ pub struct P2PConfig { pub relays: Vec, /// The external IP address of the node. - pub external_ip: String, + pub external_ip: Option, /// The external host of the node. - pub external_host: String, + pub external_host: Option, /// The TCP addresses of the node. pub tcp_addrs: Vec, @@ -98,6 +99,79 @@ impl P2PConfig { addrs.into_iter().map(multi_addr_from_ip_tcp_port).collect() } + + /// Returns a new builder for configuring a P2P configuration. + pub fn builder() -> P2PConfigBuilder { + P2PConfigBuilder::new() + } +} + +/// Builder for [`P2PConfig`]. +#[derive(Default, Debug, Clone)] +pub struct P2PConfigBuilder { + config: P2PConfig, +} + +impl P2PConfigBuilder { + /// Creates a new builder with default configuration. + pub fn new() -> Self { + Self { + config: P2PConfig::default(), + } + } + + /// Sets the relay multiaddrs. + pub fn with_relays(mut self, relays: Vec) -> Self { + self.config.relays = relays; + self + } + + /// Sets the external IP address. + pub fn with_external_ip(mut self, external_ip: String) -> Self { + self.config.external_ip = Some(external_ip); + self + } + + /// Sets the external host. + pub fn with_external_host(mut self, external_host: String) -> Self { + self.config.external_host = Some(external_host); + self + } + + /// Sets the TCP addresses. + pub fn with_tcp_addrs(mut self, tcp_addrs: Vec) -> Self { + self.config.tcp_addrs = tcp_addrs; + self + } + + /// Sets the UDP addresses. + pub fn with_udp_addrs(mut self, udp_addrs: Vec) -> Self { + self.config.udp_addrs = udp_addrs; + self + } + + /// Sets whether to disable the reuse port. + pub fn with_disable_reuse_port(mut self, disable_reuse_port: bool) -> Self { + self.config.disable_reuse_port = disable_reuse_port; + self + } + + /// Builds the [`P2PConfig`]. + pub fn build(self) -> P2PConfig { + self.config + } +} + +/// The default ping interval. +pub const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(1); +/// The default ping timeout. +pub const DEFAULT_PING_TIMEOUT: Duration = Duration::from_secs(10); + +/// Returns the default ping configuration. +pub fn default_ping_config() -> ping::Config { + ping::Config::new() + .with_interval(DEFAULT_PING_INTERVAL) + .with_timeout(DEFAULT_PING_TIMEOUT) } /// Resolves a TCP address string to a SocketAddr, ensuring the IP is specified. diff --git a/crates/charon-p2p/src/gater/mod.rs b/crates/charon-p2p/src/gater/mod.rs index 1c59345e..9e06d9d4 100644 --- a/crates/charon-p2p/src/gater/mod.rs +++ b/crates/charon-p2p/src/gater/mod.rs @@ -26,7 +26,7 @@ use crate::peer::MutablePeer; mod handler; /// Configuration for the connection gater. -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct Config { peer_ids: HashSet, relays: Vec>, @@ -68,7 +68,7 @@ impl Config { } /// ConnGater filters incoming and outgoing connections by the cluster peers. -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct ConnGater { config: Config, events: VecDeque, diff --git a/crates/charon-p2p/src/metrics.rs b/crates/charon-p2p/src/metrics.rs index bfadff76..e06b3cd9 100644 --- a/crates/charon-p2p/src/metrics.rs +++ b/crates/charon-p2p/src/metrics.rs @@ -1,6 +1,7 @@ use vise::*; -const BUCKETS: [f64; 11] = [ +/// Buckets for the ping latency histogram. +pub const BUCKETS: [f64; 11] = [ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, ]; diff --git a/crates/charon-p2p/src/p2p.rs b/crates/charon-p2p/src/p2p.rs index ca62542a..b6dc8c18 100644 --- a/crates/charon-p2p/src/p2p.rs +++ b/crates/charon-p2p/src/p2p.rs @@ -6,7 +6,7 @@ use libp2p::{ Swarm, SwarmBuilder, identity::Keypair, noise, relay, swarm::NetworkBehaviour, tcp, yamux, }; -use crate::{config::P2PConfig, gater::ConnGater}; +use crate::config::P2PConfig; /// P2P error. #[derive(Debug, thiserror::Error)] @@ -22,6 +22,14 @@ pub enum P2PError { /// Failed to decode the libp2p keypair. #[error("Failed to decode the libp2p keypair: {0}")] FailedToDecodeLibp2pKeypair(#[from] libp2p::identity::DecodingError), + + /// Failed to listen on address. + #[error("Failed to listen on address: {0}")] + FailedToListen(#[from] libp2p::TransportError), + + /// Failed to dial peer. + #[error("Failed to dial peer: {0}")] + FailedToDialPeer(#[from] libp2p::swarm::DialError), } impl P2PError { @@ -52,7 +60,6 @@ impl Node { pub fn new( cfg: P2PConfig, key: k256::SecretKey, - conn_gater: ConnGater, filter_private_addrs: bool, node_type: NodeType, behaviour_fn: F, @@ -61,12 +68,8 @@ impl Node { F: Fn(&Keypair, relay::client::Behaviour) -> B, { match node_type { - NodeType::TCP => { - Self::new_with_tcp(cfg, key, conn_gater, filter_private_addrs, behaviour_fn) - } - NodeType::QUIC => { - Self::new_with_quic(cfg, key, conn_gater, filter_private_addrs, behaviour_fn) - } + NodeType::TCP => Self::new_with_tcp(cfg, key, filter_private_addrs, behaviour_fn), + NodeType::QUIC => Self::new_with_quic(cfg, key, filter_private_addrs, behaviour_fn), } } @@ -82,7 +85,6 @@ impl Node { fn new_with_quic( _cfg: P2PConfig, key: k256::SecretKey, - _conn_gater: ConnGater, _filter_private_addrs: bool, behaviour_fn: F, ) -> Result @@ -117,7 +119,6 @@ impl Node { fn new_with_tcp( _cfg: P2PConfig, key: k256::SecretKey, - _conn_gater: ConnGater, _filter_private_addrs: bool, behaviour_fn: F, ) -> Result @@ -146,4 +147,35 @@ impl Node { Ok(Node { swarm }) } + + /// Creates a new node with relay server. + pub fn new_relay_server( + _cfg: P2PConfig, + key: k256::SecretKey, + _filter_private_addrs: bool, + behaviour_fn: F, + ) -> Result + where + F: Fn(&Keypair) -> B, + { + let mut der = key.to_sec1_der()?; + let keypair = Keypair::secp256k1_from_der(&mut der)?; + + let swarm = SwarmBuilder::with_existing_identity(keypair.clone()) + .with_tokio() + .with_tcp( + Self::default_tcp_config(), + noise::Config::new, + yamux::Config::default, + ) + .map_err(P2PError::failed_to_build_swarm)? + .with_dns() + .map_err(P2PError::failed_to_build_swarm)? + .with_behaviour(behaviour_fn) + .map_err(P2PError::failed_to_build_swarm)? + .with_swarm_config(Self::default_swarm_config) + .build(); + + Ok(Node { swarm }) + } } diff --git a/crates/charon-p2p/src/peer.rs b/crates/charon-p2p/src/peer.rs index 9559c7a4..1a9eba0e 100644 --- a/crates/charon-p2p/src/peer.rs +++ b/crates/charon-p2p/src/peer.rs @@ -127,13 +127,14 @@ pub enum MutablePeerError { } /// MutablePeer is a mutable peer that can be updated. +#[derive(Debug, Clone)] pub struct MutablePeer { /// Inner state of the mutable peer. inner: Arc>, } /// Subscriber is a function that is called when the peer is updated. -pub type Subscriber = Box; +pub type Subscriber = Box; /// MutablePeerInner is the inner state of a MutablePeer. pub struct MutablePeerInner { @@ -144,6 +145,15 @@ pub struct MutablePeerInner { subs: Vec, } +impl std::fmt::Debug for MutablePeerInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MutablePeerInner") + .field("peer", &self.peer) + .field("subs", &format!("[{} subscribers]", self.subs.len())) + .finish() + } +} + type MutablePeerResult = std::result::Result; impl MutablePeer { diff --git a/crates/relay-server/Cargo.toml b/crates/relay-server/Cargo.toml new file mode 100644 index 00000000..7d76e8f6 --- /dev/null +++ b/crates/relay-server/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "charon-relay-server" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true + +[dependencies] +axum.workspace = true +bon.workspace = true +libp2p.workspace = true +thiserror.workspace = true +k256.workspace = true +charon-eth2.workspace = true +vise.workspace = true +tokio.workspace = true +tokio-util.workspace = true +rand.workspace = true +charon-tracing.workspace = true +tracing.workspace = true +charon-p2p.workspace = true +charon-core.workspace = true + +[dev-dependencies] +vise-exporter.workspace = true + +[lints] +workspace = true \ No newline at end of file diff --git a/crates/relay-server/examples/relay_server.rs b/crates/relay-server/examples/relay_server.rs new file mode 100644 index 00000000..62cde9af --- /dev/null +++ b/crates/relay-server/examples/relay_server.rs @@ -0,0 +1,47 @@ +#![allow(missing_docs)] +use std::str::FromStr; + +use charon_p2p::config::P2PConfig; +use charon_relay_server::{config::Config, p2p::run_relay_p2p_node}; +use charon_tracing::TracingConfig; +use k256::SecretKey; +use libp2p::multiaddr; +use rand::rngs::OsRng; +use tokio_util::sync::CancellationToken; +use tracing::info; + +#[tokio::main] +async fn main() { + charon_tracing::init(&TracingConfig::default()).expect("Failed to initialize tracing"); + + let config = Config::builder() + .p2p_config( + P2PConfig::builder() + .with_tcp_addrs(vec![ + multiaddr::Multiaddr::from_str("/ip4/0.0.0.0/tcp/0") + .expect("Failed to parse multiaddress") + .to_string(), + ]) + .build(), + ) + .max_conns(100) + .max_res_per_peer(10) + .http_addr("0.0.0.0:8888".to_string()) + .build(); + let key = SecretKey::random(&mut OsRng); + + let ct = CancellationToken::new(); + + tokio::select! { + result = run_relay_p2p_node(&config, key, ct.child_token()) => { + result.expect("Failed to run relay P2P node"); + } + _ = tokio::signal::ctrl_c() => { + info!("Shutdown signal received, shutting down gracefully..."); + ct.cancel(); + + } + } + + info!("Shutdown complete"); +} diff --git a/crates/relay-server/src/behaviour.rs b/crates/relay-server/src/behaviour.rs new file mode 100644 index 00000000..aa957e24 --- /dev/null +++ b/crates/relay-server/src/behaviour.rs @@ -0,0 +1,108 @@ +#![allow(missing_docs)] // we need to allow missing docs for the derive macro +//! Relay server behaviour. + +use std::sync::LazyLock; + +use libp2p::{identify, identity::Keypair, ping, relay, swarm::NetworkBehaviour}; + +use charon_p2p::gater::ConnGater; + +/// Relay server network behaviour. +#[derive(NetworkBehaviour)] +pub struct RelayServerBehaviour { + /// Relay server. + pub relay: relay::Behaviour, + /// Identify behaviour. + pub identify: identify::Behaviour, + /// Ping behaviour. + pub ping: ping::Behaviour, + /// Gater behaviour. + pub gater: ConnGater, +} + +impl RelayServerBehaviour { + /// Creates a new RelayServerBehaviour with default configuration. + pub fn new(key: &Keypair) -> Self { + RelayServerBehaviourBuilder::default().build(key) + } + + /// Returns a new builder for configuring a RelayServerBehaviour. + pub fn builder() -> RelayServerBehaviourBuilder { + RelayServerBehaviourBuilder::default() + } +} + +/// Builder for [`RelayServerBehaviour`]. +pub struct RelayServerBehaviourBuilder { + gater: Option, + identify_protocol: String, + relay_config: Option, + user_agent: Option, +} + +/// The default identify protocol for the Pluto network. +pub static DEFAULT_IDENTIFY_PROTOCOL: LazyLock = + LazyLock::new(|| format!("/pluto/relay/{}", *charon_core::version::VERSION)); + +impl Default for RelayServerBehaviourBuilder { + fn default() -> Self { + Self { + gater: None, + identify_protocol: DEFAULT_IDENTIFY_PROTOCOL.clone(), + relay_config: None, + user_agent: None, + } + } +} + +impl RelayServerBehaviourBuilder { + /// Creates a new builder with default configuration. + pub fn new() -> Self { + Self::default() + } + + /// Sets the connection gater. + pub fn with_gater(mut self, gater: ConnGater) -> Self { + self.gater = Some(gater); + self + } + + /// Sets the identify protocol string. + pub fn with_identify_protocol(mut self, protocol: impl Into) -> Self { + self.identify_protocol = protocol.into(); + self + } + + /// Sets the relay server configuration. + pub fn with_relay_config(mut self, config: relay::Config) -> Self { + self.relay_config = Some(config); + self + } + + /// Sets the user agent string. + pub fn with_user_agent(mut self, user_agent: impl Into) -> Self { + self.user_agent = Some(user_agent.into()); + self + } + + /// Builds the [`RelayServerBehaviour`] with the provided keypair. + pub fn build(self, key: &Keypair) -> RelayServerBehaviour { + RelayServerBehaviour { + relay: relay::Behaviour::new( + key.public().to_peer_id(), + self.relay_config.unwrap_or_default(), + ), + identify: identify::Behaviour::new( + identify::Config::new(self.identify_protocol, key.public()).with_agent_version( + self.user_agent.unwrap_or_else(|| { + charon_p2p::behaviours::pluto::DEFAULT_USER_AGENT.clone() + }), + ), + ), + ping: ping::Behaviour::new(charon_p2p::config::default_ping_config()), + gater: self + .gater + .unwrap_or_else(|| ConnGater::new_conn_gater(vec![], vec![])), + } + } +} diff --git a/crates/relay-server/src/config.rs b/crates/relay-server/src/config.rs new file mode 100644 index 00000000..5a0d374b --- /dev/null +++ b/crates/relay-server/src/config.rs @@ -0,0 +1,62 @@ +use std::{path::PathBuf, time::Duration}; + +use bon::Builder; +use charon_p2p::config::P2PConfig; +use charon_tracing::TracingConfig; +use libp2p::relay; + +/// One hour in seconds. +pub const ONE_HOUR_SECONDS: u64 = 60 * 60; +/// One minute in seconds. +pub const ONE_MINUTE_SECONDS: u64 = 60; +/// 32 MB in bytes. +pub const MB_32: u64 = 32 * 1024 * 1024; +/// External host resolve interval. +pub const EXTERNAL_HOST_RESOLVE_INTERVAL: Duration = Duration::from_secs(5 * 60); + +/// Configuration for the relay P2P layer. +#[derive(Default, Debug, Clone, Builder)] +pub struct Config { + /// The directory to store the relay data. + pub data_dir: Option, + /// The HTTP address to listen on. + pub http_addr: Option, + /// The monitoring address to listen on. + pub monitoring_addr: Option, + /// The debug address to listen on. + pub debug_addr: Option, + /// The P2P configuration. + pub p2p_config: P2PConfig, + /// The logging configuration. + pub log_config: Option, + /// Whether to automatically generate a P2P key. + #[builder(default = false)] + pub auto_p2p_key: bool, + /// The maximum number of resources per peer. + pub max_res_per_peer: usize, + /// The maximum number of connections. + pub max_conns: usize, + /// Whether to filter private addresses. + #[builder(default = false)] + pub filter_private_addrs: bool, + /// LibP2PLogLevel. + #[builder(default = "Info".to_string())] + pub libp2p_log_level: String, +} + +pub(crate) fn create_relay_config(config: &Config) -> relay::Config { + relay::Config { + max_reservations: config.max_conns, + max_reservations_per_peer: config.max_res_per_peer, + reservation_duration: Duration::from_secs(ONE_HOUR_SECONDS), + reservation_rate_limiters: vec![], + // todo(varex83): check if this is correct, since it's aligned with the original + // implementation, but I'm not sure if it's the correct way to do it. + // Would it be better to use max_res_per_peer * max_conns? + max_circuits: config.max_res_per_peer, + max_circuits_per_peer: config.max_res_per_peer, + max_circuit_duration: Duration::from_secs(ONE_MINUTE_SECONDS), + max_circuit_bytes: MB_32, + circuit_src_rate_limiters: vec![], + } +} diff --git a/crates/relay-server/src/error.rs b/crates/relay-server/src/error.rs new file mode 100644 index 00000000..d2994712 --- /dev/null +++ b/crates/relay-server/src/error.rs @@ -0,0 +1,34 @@ +use libp2p::multiaddr; + +use charon_p2p::p2p::P2PError; + +/// Relay P2P error. +#[derive(Debug, thiserror::Error)] +pub enum RelayP2PError { + /// Failed to load private key. + #[error("Failed to load private key")] + FailedToLoadPrivateKey(#[from] charon_p2p::k1::K1Error), + + /// P2P error. + #[error("P2P error: {0}")] + P2PError(#[from] P2PError), + + /// Failed to bind HTTP listener. + #[error("Failed to bind HTTP listener: {0}")] + FailedToBindHttpListener(String), + + /// Failed to serve HTTP. + #[error("Failed to serve HTTP: {0}")] + FailedToServeHTTP(std::io::Error), + + /// Failed to listen on address. + #[error("Failed to listen on address: {0}")] + FailedToListenOnAddress(libp2p::TransportError), + + /// Failed to parse multiaddress. + #[error("Failed to parse multiaddress: {0}")] + FailedToParseMultiaddr(#[from] multiaddr::Error), +} + +/// Relay P2P result. +pub(crate) type Result = std::result::Result; diff --git a/crates/relay-server/src/lib.rs b/crates/relay-server/src/lib.rs new file mode 100644 index 00000000..3b21d859 --- /dev/null +++ b/crates/relay-server/src/lib.rs @@ -0,0 +1,26 @@ +//! Everything related to relay client / server. + +/// P2P. +pub mod p2p; + +/// Config. +pub mod config; + +/// Metrics. +pub mod metrics; + +/// Web. +pub(crate) mod web; + +/// Error. +pub mod error; + +/// Utils. +pub mod utils; + +/// Behaviour. +pub mod behaviour; + +pub use error::RelayP2PError; + +pub(crate) use error::Result; diff --git a/crates/relay-server/src/metrics.rs b/crates/relay-server/src/metrics.rs new file mode 100644 index 00000000..77c5fd24 --- /dev/null +++ b/crates/relay-server/src/metrics.rs @@ -0,0 +1,35 @@ +use vise::*; + +use charon_p2p::metrics::BUCKETS; + +/// Metrics for the relay P2P layer. +#[derive(Debug, Metrics)] +#[metrics(prefix = "relay_p2p")] +pub struct RelayMetrics { + /// Total number of new connections by peer and cluster. + connection_total: Family, + + /// Current number of active connections by peer and cluster. + active_connections: Family, + + /// Total number of network bytes sent to the peer and cluster. + network_sent_bytes_total: Family, + + /// Total number of network bytes received from the peer and cluster. + network_received_bytes_total: Family, + + /// Ping latency by peer and cluster. + #[metrics(buckets = &BUCKETS)] + ping_latency: Family, +} + +/// Labels for peer with peer cluster. +#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)] +pub struct PeerWithPeerClusterLabels { + peer: String, + peer_cluster: String, +} + +/// Global metrics for the relay P2P layer. +#[vise::register] +pub static RELAY_METRICS: Global = Global::new(); diff --git a/crates/relay-server/src/p2p.rs b/crates/relay-server/src/p2p.rs new file mode 100644 index 00000000..9ddc00f3 --- /dev/null +++ b/crates/relay-server/src/p2p.rs @@ -0,0 +1,122 @@ +#![allow(missing_docs)] + +use std::{sync::Arc, time::Duration}; + +use k256::SecretKey; +use libp2p::{futures::StreamExt, swarm::SwarmEvent}; +use tokio::sync::{RwLock, mpsc}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, instrument, warn}; + +use crate::{ + Result, + behaviour::RelayServerBehaviour, + config::{Config, create_relay_config}, + error::RelayP2PError, + web::enr_server, +}; +use charon_p2p::{gater::ConnGater, p2p::Node}; + +/// Runs a relay P2P node. +#[instrument(skip(config, key, ct))] +pub async fn run_relay_p2p_node( + config: &Config, + key: SecretKey, + ct: CancellationToken, +) -> Result> { + let mut node = Node::new_relay_server(config.p2p_config.clone(), key.clone(), false, |key| { + RelayServerBehaviour::builder() + .with_gater(ConnGater::new_open_gater()) + .with_relay_config(create_relay_config(config)) + .build(key) + })?; + + // todo: change to version::log_info + info!("Charon relay starting"); + + // todo: monitor connections + + for tcp_addr in config.p2p_config.tcp_addrs.iter() { + debug!("Listening on TCP address {}", tcp_addr); + node.swarm + .listen_on(tcp_addr.parse()?) + .map_err(RelayP2PError::FailedToListenOnAddress)?; + } + for udp_addr in config.p2p_config.udp_addrs.iter() { + debug!("Listening on UDP address {}", udp_addr); + node.swarm + .listen_on(udp_addr.parse()?) + .map_err(RelayP2PError::FailedToListenOnAddress)?; + } + + let (server_errors, mut server_errors_receiver) = mpsc::channel(3); + + let listeners = Arc::new(RwLock::new(Vec::new())); + + let enr_server_handle = tokio::spawn(enr_server( + server_errors.clone(), + config.clone(), + key.clone(), + *node.swarm.local_peer_id(), + listeners.clone(), + ct.child_token(), + )); + + if let Some(http_addr) = config.http_addr.clone() { + info!("Runtime multiaddrs available via http at {http_addr}",); + } else { + info!("Runtime multiaddrs not available via http, since http-address flag is not set"); + } + + loop { + tokio::select! { + biased; + _ = ct.cancelled() => { + info!("Relay server shutdown signal received, shutting down gracefully"); + break; + }, + error = server_errors_receiver.recv() => { + if let Some(error) = error { + warn!("Server error: {}", error); + return Err(error); + } + }, + event = node.swarm.select_next_some() => { + // todo: handle swarm events + debug!(?event, "Swarm event"); + + match event { + SwarmEvent::NewListenAddr { address, .. } => { + let mut listeners = listeners.write().await; + listeners.push(address); + } + SwarmEvent::ListenerClosed { addresses, .. } => { + let mut listeners = listeners.write().await; + listeners.retain(|addr| !addresses.contains(addr)); + } + SwarmEvent::ExpiredListenAddr { address, .. } => { + let mut listeners = listeners.write().await; + listeners.retain(|addr| *addr != address); + } + _ => {} + } + } + } + } + + ct.cancel(); + + match tokio::time::timeout(Duration::from_secs(2), enr_server_handle).await { + Ok(Ok(())) => { + info!("ENR server shutdown complete"); + } + Ok(Err(e)) => { + warn!("ENR server shutdown error: {}", e); + } + Err(_) => { + warn!("ENR server shutdown timeout"); + } + } + + Ok(node) +} diff --git a/crates/relay-server/src/utils.rs b/crates/relay-server/src/utils.rs new file mode 100644 index 00000000..5c039d8f --- /dev/null +++ b/crates/relay-server/src/utils.rs @@ -0,0 +1,71 @@ +use std::net::Ipv4Addr; + +use libp2p::{Multiaddr, multiaddr::Protocol}; + +/// Returns true if the multiaddr is TCP. +pub(crate) fn is_tcp_addr(addr: &Multiaddr) -> bool { + addr.iter().any(|p| matches!(p, Protocol::Tcp(_))) +} + +/// Returns true if the multiaddr is QUIC or QUIC-v1. +pub(crate) fn is_quic_addr(addr: &Multiaddr) -> bool { + addr.iter() + .any(|p| matches!(p, Protocol::Quic | Protocol::QuicV1)) +} + +/// Returns true if the multiaddr is a public address. +pub(crate) fn is_public_addr(addr: &Multiaddr) -> bool { + for protocol in addr.iter() { + match protocol { + Protocol::Ip4(ip) => { + return !ip.is_private() + && !ip.is_loopback() + && !ip.is_link_local() + && !ip.is_unspecified(); + } + Protocol::Ip6(ip) => { + return !ip.is_loopback() && !ip.is_unspecified(); + } + _ => continue, + } + } + false +} + +/// Extracts IP and TCP port from a multiaddr. +pub(crate) fn extract_ip_and_tcp_port(addr: &Multiaddr) -> Option<(Ipv4Addr, u16)> { + let mut ip: Option = None; + let mut port: Option = None; + + for protocol in addr.iter() { + match protocol { + Protocol::Ip4(i) => ip = Some(i), + Protocol::Tcp(p) => port = Some(p), + _ => {} + } + } + + match (ip, port) { + (Some(i), Some(p)) => Some((i, p)), + _ => None, + } +} + +/// Extracts IP and UDP port from a QUIC multiaddr. +pub(crate) fn extract_ip_and_udp_port(addr: &Multiaddr) -> Option<(Ipv4Addr, u16)> { + let mut ip: Option = None; + let mut port: Option = None; + + for protocol in addr.iter() { + match protocol { + Protocol::Ip4(i) => ip = Some(i), + Protocol::Udp(p) => port = Some(p), + _ => {} + } + } + + match (ip, port) { + (Some(i), Some(p)) => Some((i, p)), + _ => None, + } +} diff --git a/crates/relay-server/src/web.rs b/crates/relay-server/src/web.rs new file mode 100644 index 00000000..7e1fd80c --- /dev/null +++ b/crates/relay-server/src/web.rs @@ -0,0 +1,322 @@ +use std::{ + net::{IpAddr, Ipv4Addr}, + sync::Arc, +}; + +use crate::utils; +use axum::{ + Json, Router, + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, +}; +use k256::SecretKey; +use libp2p::{Multiaddr, PeerId, multiaddr}; +use tokio::{ + net::TcpListener, + sync::{RwLock, mpsc}, +}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, instrument, warn}; + +use crate::{ + config::{Config, EXTERNAL_HOST_RESOLVE_INTERVAL}, + error::RelayP2PError, +}; +use charon_p2p::{config::P2PConfig, name::peer_name}; + +/// Shared application state for HTTP handlers. +#[derive(Clone)] +pub struct AppState { + /// The P2P configuration. + p2p_config: P2PConfig, + /// The secret key for signing ENR records. + secret_key: SecretKey, + /// The peer ID of this node. + peer_id: PeerId, + /// The addresses of this node. + addrs: Arc>>, + /// The resolved external host IP (if configured). + external_host_ip: Arc>>, +} + +impl AppState { + /// Creates a new AppState. + pub fn new( + p2p_config: P2PConfig, + secret_key: SecretKey, + peer_id: PeerId, + addrs: Arc>>, + ) -> Self { + Self { + p2p_config, + secret_key, + peer_id, + addrs, + external_host_ip: Arc::new(RwLock::new(None)), + } + } + + /// Gets the external host IP if set. + async fn get_external_host_ip(&self) -> Option { + *self.external_host_ip.read().await + } + + /// Sets the external host IP. + async fn set_external_host_ip(&self, ip: Option) { + let mut ext_ip = self.external_host_ip.write().await; + *ext_ip = ip; + } +} + +/// Starts the ENR HTTP server. +#[instrument(skip(server_errors, config, secret_key, peer_id, addrs, ct))] +pub async fn enr_server( + server_errors: mpsc::Sender, + config: Config, + secret_key: SecretKey, + peer_id: PeerId, + addrs: Arc>>, + ct: CancellationToken, +) { + let Some(http_addr) = config.http_addr.clone() else { + warn!("HTTP address is not set, skipping ENR server"); + return; + }; + + info!("Starting ENR server"); + + let state = AppState::new(config.p2p_config.clone(), secret_key, peer_id, addrs); + let state_arc = Arc::new(state); + + // Start external host resolver task if configured + let resolver_handle = if let Some(external_host) = config.p2p_config.external_host { + let state_clone = state_arc.clone(); + let ct_clone = ct.child_token(); + Some(tokio::spawn(async move { + resolve_external_host_periodically(state_clone, external_host, ct_clone).await; + })) + } else { + None + }; + + let router = Router::new() + .route("/", get(multiaddr_handler)) + .route("/enr", get(enr_handler)) + .with_state(state_arc); + + let Ok(listener) = TcpListener::bind(&http_addr).await else { + warn!("Failed to bind HTTP listener to {}", http_addr); + let _ = server_errors + .send(RelayP2PError::FailedToBindHttpListener(http_addr)) + .await; + return; + }; + + info!( + "Relay started {peer_name} on {tcp_addrs} and {udp_addrs}", + peer_name = peer_name(&peer_id), + tcp_addrs = config.p2p_config.tcp_addrs.join(", "), + udp_addrs = config.p2p_config.udp_addrs.join(", "), + ); + + let ct_clone = ct.child_token(); + if let Err(e) = axum::serve(listener, router) + .with_graceful_shutdown(async move { + ct_clone.cancelled().await; + info!("ENR server shutdown complete"); + }) + .await + { + warn!("HTTP server error: {}", e); + let _ = server_errors + .send(RelayP2PError::FailedToServeHTTP(e)) + .await; + } + + ct.cancel(); + + if let Some(resolver_handle) = resolver_handle { + let _ = resolver_handle.await; + } +} + +/// Error response for HTTP handlers. +pub struct HandlerError { + status: StatusCode, + message: String, +} + +impl IntoResponse for HandlerError { + fn into_response(self) -> Response { + (self.status, self.message).into_response() + } +} + +/// Handler that returns the node's ENR. +#[instrument(skip(state))] +pub async fn enr_handler( + State(state): State>, +) -> std::result::Result { + debug!("Getting ENR for node {}", state.peer_id); + + let addrs = state.addrs.read().await; + + // Sort addresses with public addresses first + let mut sorted_addrs: Vec = addrs.clone(); + drop(addrs); + + if sorted_addrs.is_empty() { + return Err(HandlerError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "no addresses".to_string(), + }); + } + + sorted_addrs.sort_by(|a, b| { + let a_public = utils::is_public_addr(a); + let b_public = utils::is_public_addr(b); + // Public addresses should come first + b_public.cmp(&a_public) + }); + + // Find TCP and UDP addresses + let mut tcp_addr: Option<(Ipv4Addr, u16)> = None; + let mut udp_addr: Option<(Ipv4Addr, u16)> = None; + + for addr in &sorted_addrs { + if tcp_addr.is_none() + && utils::is_tcp_addr(addr) + && let Some((ip, port)) = utils::extract_ip_and_tcp_port(addr) + { + tcp_addr = Some((apply_ip_override(&state, ip).await, port)); + } + + if udp_addr.is_none() + && utils::is_quic_addr(addr) + && let Some((ip, port)) = utils::extract_ip_and_udp_port(addr) + { + udp_addr = Some((apply_ip_override(&state, ip).await, port)); + } + + if tcp_addr.is_some() && udp_addr.is_some() { + break; + } + } + + // Determine final IP, TCP port, and UDP port + let (ip, tcp_port, udp_port) = match (tcp_addr, udp_addr) { + (Some((tcp_ip, tcp_p)), Some((udp_ip, udp_p))) => { + if tcp_ip != udp_ip { + return Err(HandlerError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("conflicting IP addresses: tcp={}, udp={}", tcp_ip, udp_ip), + }); + } + (tcp_ip, tcp_p, udp_p) + } + (Some((ip, tcp_p)), None) => (ip, tcp_p, 9999), // Dummy UDP port + (None, Some((ip, udp_p))) => (ip, 9999, udp_p), // Dummy TCP port + (None, None) => { + return Err(HandlerError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: "no udp or tcp addresses provided".to_string(), + }); + } + }; + + // Create ENR record + let record = charon_eth2::enr::Record::new( + state.secret_key.clone(), + vec![ + charon_eth2::enr::with_ip_impl(ip), + charon_eth2::enr::with_tcp_impl(tcp_port), + charon_eth2::enr::with_udp_impl(udp_port), + ], + ) + .map_err(|e| HandlerError { + status: StatusCode::INTERNAL_SERVER_ERROR, + message: format!("failed to create ENR: {}", e), + })?; + + Ok(record.to_string()) +} + +/// Applies IP override from config (external_ip or resolved external_host). +async fn apply_ip_override(state: &AppState, original_ip: Ipv4Addr) -> Ipv4Addr { + // First check external_ip config + if let Some(external_ip) = &state.p2p_config.external_ip + && let Ok(ip) = external_ip.parse::() + { + return ip; + } + + // Then check resolved external_host + if let Some(ip) = state.get_external_host_ip().await { + return ip; + } + + original_ip +} + +/// Handler that returns the node's multiaddrs as JSON. +#[instrument(skip(state))] +pub async fn multiaddr_handler( + State(state): State>, +) -> std::result::Result>, HandlerError> { + debug!("Getting multiaddrs for node {}", state.peer_id); + + let addrs = state.addrs.read().await.clone(); + + // Encapsulate peer ID into each address + let full_addrs: Vec = addrs + .into_iter() + .map(|addr| addr.with(multiaddr::Protocol::P2p(state.peer_id))) + .map(|addr| addr.to_string()) + .collect(); + + Ok(Json(full_addrs)) +} + +/// Periodically resolves the external host to an IP address. +#[instrument(skip(state, ct))] +async fn resolve_external_host_periodically( + state: Arc, + external_host: String, + ct: CancellationToken, +) { + info!("Starting external host resolver"); + + let mut interval = tokio::time::interval(EXTERNAL_HOST_RESOLVE_INTERVAL); + + loop { + tokio::select! { + biased; + _ = ct.cancelled() => { + info!("External host resolver shutdown complete"); + break; + } + _ = interval.tick() => { + resolve_external_host(state.clone(), &external_host).await; + } + } + } +} + +/// 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() + { + debug!("Resolved external host {external_host} to {ipv4}"); + state.set_external_host_ip(Some(ipv4)).await; + } + } + Err(e) => { + warn!("Failed to resolve external host {}: {}", external_host, e); + } + } +} diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index b9c93282..542d6171 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -17,3 +17,5 @@ pub mod layers; pub mod metrics; pub use config::{ConsoleConfig, LokiConfig, TracingConfig, TracingConfigBuilder}; + +pub use init::init;