diff --git a/components/addressmanager/src/dyndns_extender.rs b/components/addressmanager/src/dyndns_extender.rs new file mode 100644 index 0000000000..ce283b881d --- /dev/null +++ b/components/addressmanager/src/dyndns_extender.rs @@ -0,0 +1,116 @@ +use std::{net::IpAddr, sync::{Arc}, time::Duration}; +use parking_lot::Mutex; +use kaspa_core::{info, debug, warn, trace, task::{tick::{TickService, TickReason}, service::{AsyncService, AsyncServiceFuture}}}; +use crate::{NetAddress, AddressManager}; +use kaspa_utils::networking::IpAddress; +use kaspa_consensus_core::config::{Config, IpVersionMode}; +use std::net::ToSocketAddrs; + +pub const SERVICE_NAME: &str = "dyndns-extender"; + +// Simplistic resolver trait to allow mocking in tests later +trait DynResolver: Send + Sync { + fn resolve(&self, host: &str) -> std::io::Result>; +} + +struct DefaultResolver; +impl DynResolver for DefaultResolver { + fn resolve(&self, host: &str) -> std::io::Result> { Ok((host, 0).to_socket_addrs()?.map(|sa| sa.ip()).collect()) } +} + +pub struct DynDnsExtender { + tick_service: Arc, + address_manager: Arc>, + host: String, + min_refresh: Duration, + max_refresh: Duration, + ip_mode: IpVersionMode, + resolver: Box, + last_ip: Arc>>, +} + +impl DynDnsExtender { + pub fn new(config: Arc, am: Arc>, tick_service: Arc, initial_external_ip: Option) -> Option { + let host = config.external_dyndns_host.clone()?; // only build if host provided + + let instance = Self { + tick_service, + address_manager: am.clone(), + min_refresh: Duration::from_secs(config.external_dyndns_min_refresh_sec), + max_refresh: Duration::from_secs(config.external_dyndns_max_refresh_sec), + ip_mode: config.external_dyndns_ip_version, + host, + resolver: Box::new(DefaultResolver), + last_ip: Arc::new(Mutex::new(initial_external_ip)), + }; + + Some(instance) + } + + fn pick_ip(&self, mut ips: Vec) -> Option { + // filter publics + ips.retain(|ip| IpAddress::new(*ip).is_publicly_routable()); + if ips.is_empty() { return None; } + match self.ip_mode { + IpVersionMode::Ipv4 => ips.into_iter().find(|ip| matches!(ip, IpAddr::V4(_))), + IpVersionMode::Ipv6 => ips.into_iter().find(|ip| matches!(ip, IpAddr::V6(_))), + IpVersionMode::Auto => { + if let Some(v4) = ips.iter().cloned().find(|ip| matches!(ip, IpAddr::V4(_))) { return Some(v4); } + ips.into_iter().next() + } + } + } + + async fn worker(&self) { + info!("[DynDNS] Starting dyndns resolver for host {}", self.host); + let mut interval = self.min_refresh; // adaptive later + loop { + match self.tick_service.tick(interval).await { + TickReason::Shutdown => break, + TickReason::Wakeup => {} + } + match self.resolver.resolve(&self.host) { + Ok(ips) => { + debug!("[DynDNS] Resolved {} -> {:?}", self.host, ips); + let picked = self.pick_ip(ips); + if let Some(new_ip) = picked { + let mut last_guard = self.last_ip.lock(); + if Some(new_ip) != *last_guard { + let old_ip = *last_guard; + *last_guard = Some(new_ip); + drop(last_guard); + self.apply_new_ip(new_ip, old_ip); + } + interval = self.min_refresh; // reset + } else { + warn!("[DynDNS] No public IP obtained for {}", self.host); + interval = std::cmp::min(interval * 2, self.max_refresh); + } + } + Err(e) => { + warn!("[DynDNS] Resolve failed for {}: {e}", self.host); + interval = std::cmp::min(interval * 2, self.max_refresh); + } + } + } + trace!("{SERVICE_NAME} worker exiting"); + } + + fn apply_new_ip(&self, new_ip: IpAddr, old_ip: Option) { + info!("[DynDNS] External IP changed {:?} -> {}", old_ip, new_ip); + let mut am = self.address_manager.lock(); + let port = am.best_local_address().map(|a| a.port).unwrap_or_else(|| am.config.default_p2p_port()); + let net = NetAddress::new(new_ip.into(), port); + am.set_best_local_address(net); + let sinks = am.clone_external_ip_change_sinks(); + drop(am); + for sink in sinks { let s = sink.clone(); tokio::spawn(async move { s.on_external_ip_changed(new_ip, old_ip); }); } + } +} + +impl AsyncService for DynDnsExtender { + fn ident(self: Arc) -> &'static str { SERVICE_NAME } + fn start(self: Arc) -> AsyncServiceFuture { Box::pin(async move { self.worker().await; Ok(()) }) } + fn signal_exit(self: Arc) { trace!("sending an exit signal to {}", SERVICE_NAME); } + fn stop(self: Arc) -> AsyncServiceFuture { Box::pin(async move { trace!("{} stopped", SERVICE_NAME); Ok(()) }) } +} diff --git a/components/addressmanager/src/lib.rs b/components/addressmanager/src/lib.rs index b220a8ab1b..79cb42ebb3 100644 --- a/components/addressmanager/src/lib.rs +++ b/components/addressmanager/src/lib.rs @@ -1,3 +1,4 @@ +mod dyndns_extender; mod port_mapping_extender; mod stores; extern crate self as address_manager; @@ -5,6 +6,7 @@ extern crate self as address_manager; use std::{collections::HashSet, iter, net::SocketAddr, sync::Arc, time::Duration}; use address_manager::port_mapping_extender::Extender; +use dyndns_extender::DynDnsExtender; use igd_next::{ self as igd, aio::tokio::Tokio, AddAnyPortError, AddPortError, Gateway, GetExternalIpError, GetGenericPortMappingEntryError, SearchError, @@ -13,7 +15,7 @@ use itertools::{ Either::{Left, Right}, Itertools, }; -use kaspa_consensus_core::config::Config; +use kaspa_consensus_core::config::{Config, IpVersionMode}; use kaspa_core::{debug, info, task::tick::TickService, time::unix_now, warn}; use kaspa_database::prelude::{CachePolicy, StoreResultExtensions, DB}; use kaspa_utils::networking::IpAddress; @@ -24,6 +26,10 @@ use thiserror::Error; pub use stores::NetAddress; +pub trait ExternalIpChangeSink: Send + Sync { + fn on_external_ip_changed(&self, new_ip: std::net::IpAddr, old_ip: Option); +} + const MAX_ADDRESSES: usize = 4096; const MAX_CONNECTION_FAILED_COUNT: u64 = 3; @@ -56,27 +62,104 @@ pub struct AddressManager { address_store: address_store_with_cache::Store, config: Arc, local_net_addresses: Vec, + external_ip_change_sinks: Vec>, } impl AddressManager { - pub fn new(config: Arc, db: Arc, tick_service: Arc) -> (Arc>, Option) { - let mut instance = Self { + pub fn new( + config: Arc, + db: Arc, + tick_service: Arc, + ) -> (Arc>, Option, Option) { + let instance = Self { banned_address_store: DbBannedAddressesStore::new(db.clone(), CachePolicy::Count(MAX_ADDRESSES)), address_store: address_store_with_cache::new(db), local_net_addresses: Vec::new(), + external_ip_change_sinks: Vec::new(), config, }; - let extender = instance.init_local_addresses(tick_service); + let am = Arc::new(Mutex::new(instance)); + + let extender = Self::init_local_addresses(&am, tick_service.clone()); + let dyndns_extender = if extender.is_none() { + Self::try_initial_dyndns_resolve(&am, tick_service.clone()) + } else { + debug!("[AddrMan] UPnP extender active; DynDNS skipped"); + None + }; + // Note: initial DynDNS seed handled above when applicable + debug!( + "[AddrMan] Exit AddressManager::new (upnp_extender={} dyndns_extender={})", + extender.is_some(), + dyndns_extender.is_some() + ); + (am, extender, dyndns_extender) + } + + fn try_initial_dyndns_resolve(am: &Arc>, tick_service: Arc) -> Option { + // Only attempt if a DynDNS host is configured + let host_opt = am.lock().config.external_dyndns_host.clone(); + let Some(host) = host_opt else { return None }; + let ip_mode = am.lock().config.external_dyndns_ip_version; + let mut initial_ip: Option = None; + + debug!("[AddrMan] Performing minimal initial DynDNS resolve for host {}", host); + // DNS resolution outside lock + let result: std::io::Result> = (|| { + use std::net::ToSocketAddrs; + (host.as_str(), 0).to_socket_addrs().map(|it| it.map(|sa| sa.ip()).collect()) + })(); + match result { + Ok(mut ips) => { + ips.retain(|ip| IpAddress::new(*ip).is_publicly_routable()); + let selected = match ip_mode { + IpVersionMode::Ipv4 => ips.iter().cloned().find(|ip| matches!(ip, std::net::IpAddr::V4(_))), + IpVersionMode::Ipv6 => ips.iter().cloned().find(|ip| matches!(ip, std::net::IpAddr::V6(_))), + IpVersionMode::Auto => { + if let Some(v4) = ips.iter().cloned().find(|ip| matches!(ip, std::net::IpAddr::V4(_))) { + Some(v4) + } else { + ips.get(0).cloned() + } + } + }; + if let Some(ip) = selected { + initial_ip = Some(ip); + let mut guard = am.lock(); + let port = guard.config.default_p2p_port(); + guard.set_best_local_address(NetAddress::new(ip.into(), port)); + debug!("[AddrMan] Initial DynDNS external IP set to {}:{}", ip, port); + } else { + debug!("[AddrMan] Initial DynDNS resolve returned no public addresses"); + } + } + Err(e) => debug!("[AddrMan] Initial DynDNS resolve failed: {e}"), + } + + let extender = DynDnsExtender::new(am.lock().config.clone(), am.clone(), tick_service, initial_ip); + if extender.is_some() { + debug!("[AddrMan] DynDnsExtender constructed"); + } else { + debug!("[AddrMan] DynDnsExtender NOT constructed (unexpected None)"); + } + extender + } + + pub fn register_external_ip_change_sink(&mut self, sink: Arc) { + self.external_ip_change_sinks.push(sink); + } - (Arc::new(Mutex::new(instance)), extender) + pub fn clone_external_ip_change_sinks(&self) -> Vec> { + self.external_ip_change_sinks.clone() } - fn init_local_addresses(&mut self, tick_service: Arc) -> Option { - self.local_net_addresses = self.local_addresses().collect(); + fn init_local_addresses(this: &Arc>, tick_service: Arc) -> Option { + let mut me = this.lock(); + me.local_net_addresses = me.local_addresses().collect(); - let extender = if self.local_net_addresses.is_empty() && !self.config.disable_upnp { - let (net_address, ExtendHelper { gateway, local_addr, external_port }) = match self.upnp() { + let extender = if me.local_net_addresses.is_empty() && !me.config.disable_upnp { + let (net_address, ExtendHelper { gateway, local_addr, external_port }) = match me.upnp() { Err(err) => { warn!("[UPnP] Error adding port mapping: {err}"); return None; @@ -84,7 +167,7 @@ impl AddressManager { Ok(None) => return None, Ok(Some((net_address, extend_helper))) => (net_address, extend_helper), }; - self.local_net_addresses.push(net_address); + me.local_net_addresses.push(net_address); let gateway: igd_next::aio::Gateway = igd_next::aio::Gateway { addr: gateway.addr, @@ -101,12 +184,14 @@ impl AddressManager { gateway, external_port, local_addr, + Arc::clone(this), + Some(net_address.ip.into()), )) } else { None }; - self.local_net_addresses.iter().for_each(|net_addr| { + me.local_net_addresses.iter().for_each(|net_addr| { info!("Publicly routable local address {} added to store", net_addr); }); extender @@ -142,12 +227,24 @@ impl AddressManager { return Left(Right(iter::empty())); }; // TODO: Add Check IPv4 or IPv6 match from Go code - Right(network_interfaces.into_iter().map(|(_, ip)| IpAddress::from(ip)).filter(|&ip| ip.is_publicly_routable()).map( - |ip| { - info!("Publicly routable local address found: {}", ip); - NetAddress::new(ip, self.config.default_p2p_port()) - }, - )) + Right( + network_interfaces + .into_iter() + .map(|(_, ip)| IpAddress::from(ip)) + .filter(|ip| { + if self.config.disable_ipv6_interface_discovery { + // Skip IPv6 during automatic discovery if the flag is set + !matches!(**ip, std::net::IpAddr::V6(_)) + } else { + true + } + }) + .filter(|&ip| ip.is_publicly_routable()) + .map(|ip| { + info!("Publicly routable local address found: {}", ip); + NetAddress::new(ip, self.config.default_p2p_port()) + }), + ) } else { Left(Right(iter::empty())) } @@ -251,6 +348,14 @@ impl AddressManager { } } + pub fn set_best_local_address(&mut self, address: NetAddress) { + if self.local_net_addresses.is_empty() { + self.local_net_addresses.push(address); + } else { + self.local_net_addresses[0] = address; + } + } + pub fn add_address(&mut self, address: NetAddress) { if address.ip.is_loopback() || address.ip.is_unspecified() { debug!("[Address manager] skipping local address {}", address.ip); @@ -548,7 +653,7 @@ mod address_store_with_cache { let db = create_temp_db!(ConnBuilder::default().with_files_limit(10)); let config = Config::new(SIMNET_PARAMS); - let (am, _) = AddressManager::new(Arc::new(config), db.1, Arc::new(TickService::default())); + let (am, _, _) = AddressManager::new(Arc::new(config), db.1, Arc::new(TickService::default())); let mut am_guard = am.lock(); diff --git a/components/addressmanager/src/port_mapping_extender.rs b/components/addressmanager/src/port_mapping_extender.rs index 11651bab93..0b8436beb2 100644 --- a/components/addressmanager/src/port_mapping_extender.rs +++ b/components/addressmanager/src/port_mapping_extender.rs @@ -10,9 +10,13 @@ use kaspa_core::{ use std::{net::SocketAddr, sync::Arc, time::Duration}; use crate::UPNP_REGISTRATION_NAME; +use crate::{AddressManager, NetAddress}; +use kaspa_utils::networking::IpAddress; +use parking_lot::Mutex; pub const SERVICE_NAME: &str = "port-mapping-extender"; +#[derive(Clone)] pub struct Extender { tick_service: Arc, fetch_interval: Duration, @@ -20,6 +24,8 @@ pub struct Extender { gateway: igd_next::aio::Gateway, external_port: u16, local_addr: SocketAddr, + address_manager: Arc>, + last_known_external_ip: Arc>>, } impl Extender { @@ -30,14 +36,31 @@ impl Extender { gateway: igd_next::aio::Gateway, external_port: u16, local_addr: SocketAddr, + address_manager: Arc>, + initial_external_ip: Option, ) -> Self { - Self { tick_service, fetch_interval, deadline_sec, gateway, external_port, local_addr } + // Log the initial IP for debugging + if let Some(initial_ip) = initial_external_ip { + debug!("[UPnP] Extender initialized with initial external IP: {}", initial_ip); + } + + Self { + tick_service, + fetch_interval, + deadline_sec, + gateway, + external_port, + local_addr, + address_manager, + last_known_external_ip: Arc::new(Mutex::new(initial_external_ip)), + } } } impl Extender { pub async fn worker(&self) -> Result<(), AddPortError> { while let TickReason::Wakeup = self.tick_service.tick(self.fetch_interval).await { + if let Err(e) = self .gateway .add_port( @@ -53,12 +76,62 @@ impl Extender { } else { debug!("[UPnP] Extend external ip mapping"); } + + let external_ip_result = self.gateway.get_external_ip().await; + if let Err(e) = &external_ip_result { + warn!("[UPnP] Fetch external ip err: {e:?}"); + } else { + debug!("[UPnP] Fetched external ip"); + } + + if let Ok(current_ip) = external_ip_result { + // Check if IP has changed + let ip_changed = { + let mut last_ip_guard = self.last_known_external_ip.lock(); + if *last_ip_guard != Some(current_ip) { + let old_ip = *last_ip_guard; + *last_ip_guard = Some(current_ip); + Some((current_ip, old_ip)) + } else { + None + } + }; // MutexGuard is dropped here + + if let Some((new_ip, old_ip)) = ip_changed { + self.handle_ip_change(new_ip, old_ip).await; + } + } + + } // Let the system print final logs before exiting tokio::time::sleep(Duration::from_millis(500)).await; trace!("{SERVICE_NAME} worker exiting"); Ok(()) } + + async fn handle_ip_change(&self, new_ip: std::net::IpAddr, old_ip: Option) { + info!("[UPnP] External IP changed from {:?} to {}", old_ip, new_ip); + + // Update best_local_address + let mut am_guard = self.address_manager.lock(); + let ip = IpAddress::new(new_ip); + let net_addr = NetAddress { ip, port: self.external_port }; + am_guard.set_best_local_address(net_addr); + debug!("[UPnP] Updated best local address to {}", net_addr); + + // Notify registered sinks (fire-and-forget). We offload each sync callback into its + // own lightweight task so the extender loop isn't delayed by sink logic. + let sinks = am_guard.clone_external_ip_change_sinks(); + drop(am_guard); + for sink in sinks { + let s = sink.clone(); + tokio::spawn(async move { + // Trait is sync; we just invoke inside an async task (fire-and-forget). + s.on_external_ip_changed(new_ip, old_ip); + }); + } + } } impl AsyncService for Extender { diff --git a/components/connectionmanager/src/lib.rs b/components/connectionmanager/src/lib.rs index 2146ec62d1..b96e417794 100644 --- a/components/connectionmanager/src/lib.rs +++ b/components/connectionmanager/src/lib.rs @@ -9,7 +9,7 @@ use std::{ use duration_string::DurationString; use futures_util::future::{join_all, try_join_all}; use itertools::Itertools; -use kaspa_addressmanager::{AddressManager, NetAddress}; +use kaspa_addressmanager::{AddressManager, NetAddress, ExternalIpChangeSink}; use kaspa_core::{debug, info, warn}; use kaspa_p2p_lib::{common::ProtocolError, ConnectionError, Peer}; use kaspa_utils::triggers::SingleTrigger; @@ -93,6 +93,46 @@ impl ConnectionManager { }); } + /// Synchronously trigger a staggered outbound reconnect (terminates peers one by one with 15s delays) + pub fn trigger_outbound_reconnect(&self) { + let outbound_peers: Vec<_> = self.p2p_adaptor.active_peers() + .into_iter() + .filter(|p| p.is_outbound()) + .collect(); + + if outbound_peers.is_empty() { + info!("No outbound peers to reconnect"); + return; + } + + let peer_count = outbound_peers.len(); + info!("Starting staggered outbound reconnect: {} peers will be renewed with 15s delays", peer_count); + + // Spawn async task for staggered renewal + let p2p_adaptor = self.p2p_adaptor.clone(); + let force_sender = self.force_next_iteration.clone(); + + tokio::spawn(async move { + for (i, peer) in outbound_peers.into_iter().enumerate() { + // Terminate peer + p2p_adaptor.terminate(peer.key()).await; + info!("Terminated outbound peer {} ({}/{})", peer.net_address(), i+1, peer_count); + + // Trigger reconnection (except for the last peer) + if i < peer_count - 1 { + force_sender.send(()).unwrap(); + + // Wait 15 seconds + tokio::time::sleep(Duration::from_secs(15)).await; + } + } + + // Final trigger for the last renewal + force_sender.send(()).unwrap(); + info!("Staggered outbound reconnect completed"); + }); + } + async fn handle_event(self: Arc) { debug!("Starting connection loop iteration"); let peers = self.p2p_adaptor.active_peers(); @@ -338,3 +378,9 @@ impl ConnectionManager { self.connection_requests.lock().await.iter().any(|(address, request)| request.is_permanent && address.ip() == ip) } } + +impl ExternalIpChangeSink for ConnectionManager { + fn on_external_ip_changed(&self, _new_ip: std::net::IpAddr, _old_ip: Option) { + self.trigger_outbound_reconnect(); + } +} diff --git a/consensus/core/src/config/mod.rs b/consensus/core/src/config/mod.rs index bc41cde562..a875daee3f 100644 --- a/consensus/core/src/config/mod.rs +++ b/consensus/core/src/config/mod.rs @@ -17,6 +17,13 @@ use { params::Params, }; +#[derive(Clone, Copy, Debug)] +pub enum IpVersionMode { + Auto, + Ipv4, + Ipv6, +} + /// Various consensus configurations all bundled up under a single struct. Use `Config::new` for directly building from /// a `Params` instance. For anything more complex it is recommended to use `ConfigBuilder`. NOTE: this struct can be /// implicitly de-refed into `Params` @@ -66,6 +73,15 @@ pub struct Config { pub disable_upnp: bool, + /// Disable IPv6 during automatic local address discovery (explicit IPv6 in config is still honored) + pub disable_ipv6_interface_discovery: bool, + // DynDNS based external IP resolution (optional hostname). If set and UPnP fails to yield a public IP, + // a dyn dns resolver service will periodically resolve this host and update the external address on change. + pub external_dyndns_host: Option, + pub external_dyndns_min_refresh_sec: u64, + pub external_dyndns_max_refresh_sec: u64, + pub external_dyndns_ip_version: IpVersionMode, + /// A scale factor to apply to memory allocation bounds pub ram_scale: f64, @@ -97,6 +113,11 @@ impl Config { #[cfg(feature = "devnet-prealloc")] initial_utxo_set: Default::default(), disable_upnp: false, + disable_ipv6_interface_discovery: false, + external_dyndns_host: None, + external_dyndns_min_refresh_sec: 30, + external_dyndns_max_refresh_sec: 300, + external_dyndns_ip_version: IpVersionMode::Auto, ram_scale: 1.0, retention_period_days: None, } diff --git a/kaspad/src/args.rs b/kaspad/src/args.rs index 9a58bc6dbf..ae22c31eaa 100644 --- a/kaspad/src/args.rs +++ b/kaspad/src/args.rs @@ -85,6 +85,12 @@ pub struct Args { pub prealloc_amount: u64, pub disable_upnp: bool, + pub disable_ipv6_interface_discovery: bool, + // DynDNS external IP resolution arguments + pub external_dyndns_host: Option, + pub external_dyndns_min_refresh_sec: u64, + pub external_dyndns_max_refresh_sec: u64, + pub external_dyndns_ip_version: String, // parsed into enum later (auto|ipv4|ipv6) #[serde(rename = "nodnsseed")] pub disable_dns_seeding: bool, #[serde(rename = "nogrpc")] @@ -97,6 +103,10 @@ impl Default for Args { fn default() -> Self { Self { appdir: None, + external_dyndns_host: None, + external_dyndns_min_refresh_sec: 30, + external_dyndns_max_refresh_sec: 300, + external_dyndns_ip_version: "auto".to_string(), no_log_files: false, rpclisten_borsh: None, rpclisten_json: None, @@ -138,6 +148,7 @@ impl Default for Args { prealloc_amount: 10_000_000_000, disable_upnp: false, + disable_ipv6_interface_discovery: false, disable_dns_seeding: false, disable_grpc: false, ram_scale: 1.0, @@ -150,6 +161,7 @@ impl Args { pub fn apply_to_config(&self, config: &mut Config) { config.utxoindex = self.utxoindex; config.disable_upnp = self.disable_upnp; + config.disable_ipv6_interface_discovery = self.disable_ipv6_interface_discovery; config.unsafe_rpc = self.unsafe_rpc; config.enable_unsynced_mining = self.enable_unsynced_mining; config.enable_mainnet_mining = self.enable_mainnet_mining; @@ -162,6 +174,14 @@ impl Args { config.externalip = self.externalip.map(|v| v.normalize(config.default_p2p_port())); config.ram_scale = self.ram_scale; config.retention_period_days = self.retention_period_days; + config.external_dyndns_host = self.external_dyndns_host.clone(); + config.external_dyndns_min_refresh_sec = self.external_dyndns_min_refresh_sec; + config.external_dyndns_max_refresh_sec = self.external_dyndns_max_refresh_sec; + config.external_dyndns_ip_version = match self.external_dyndns_ip_version.as_str() { + "ipv4" => kaspa_consensus_core::config::IpVersionMode::Ipv4, + "ipv6" => kaspa_consensus_core::config::IpVersionMode::Ipv6, + _ => kaspa_consensus_core::config::IpVersionMode::Auto, + }; #[cfg(feature = "devnet-prealloc")] if let Some(num_prealloc_utxos) = self.num_prealloc_utxos { @@ -362,6 +382,29 @@ Setting to 0 prevents the preallocation and sets the maximum to {}, leading to 0 .help("Interval in seconds for performance metrics collection."), ) .arg(arg!(--"disable-upnp" "Disable upnp")) + .arg(arg!(--"disable-ipv6-interface-discovery" "Disable IPv6 during automatic local address discovery (explicit IPv6 in config is still honored)")) + .arg(arg!(--"external-dyndns-host" "DynDNS host to resolve periodically as fallback external IP source when UPnP fails").require_equals(true)) + .arg( + Arg::new("external-dyndns-min-refresh") + .long("external-dyndns-min-refresh") + .require_equals(true) + .value_parser(clap::value_parser!(u64)) + .help("Minimum refresh interval (seconds) for DynDNS resolution (default 30)") + ) + .arg( + Arg::new("external-dyndns-max-refresh") + .long("external-dyndns-max-refresh") + .require_equals(true) + .value_parser(clap::value_parser!(u64)) + .help("Maximum refresh interval (seconds) for DynDNS resolution (default 300)") + ) + .arg( + Arg::new("external-dyndns-ip-version") + .long("external-dyndns-ip-version") + .require_equals(true) + .value_parser(["auto","ipv4","ipv6"]) + .help("IP version selection for DynDNS resolution: auto|ipv4|ipv6 (default auto)") + ) .arg(arg!(--"nodnsseed" "Disable DNS seeding for peers")) .arg(arg!(--"nogrpc" "Disable gRPC server")) .arg( @@ -455,10 +498,15 @@ impl Args { // Note: currently used programmatically by benchmarks and not exposed to CLI users block_template_cache_lifetime: defaults.block_template_cache_lifetime, disable_upnp: arg_match_unwrap_or::(&m, "disable-upnp", defaults.disable_upnp), + disable_ipv6_interface_discovery: arg_match_unwrap_or::(&m, "disable-ipv6-interface-discovery", defaults.disable_ipv6_interface_discovery), disable_dns_seeding: arg_match_unwrap_or::(&m, "nodnsseed", defaults.disable_dns_seeding), disable_grpc: arg_match_unwrap_or::(&m, "nogrpc", defaults.disable_grpc), ram_scale: arg_match_unwrap_or::(&m, "ram-scale", defaults.ram_scale), retention_period_days: m.get_one::("retention-period-days").cloned().or(defaults.retention_period_days), + external_dyndns_host: m.get_one::("external-dyndns-host").cloned().or(defaults.external_dyndns_host), + external_dyndns_min_refresh_sec: arg_match_unwrap_or::(&m, "external-dyndns-min-refresh", defaults.external_dyndns_min_refresh_sec), + external_dyndns_max_refresh_sec: arg_match_unwrap_or::(&m, "external-dyndns-max-refresh", defaults.external_dyndns_max_refresh_sec), + external_dyndns_ip_version: arg_match_unwrap_or::(&m, "external-dyndns-ip-version", defaults.external_dyndns_ip_version.clone()), #[cfg(feature = "devnet-prealloc")] num_prealloc_utxos: m.get_one::("num-prealloc-utxos").cloned(), diff --git a/kaspad/src/daemon.rs b/kaspad/src/daemon.rs index 943a2b98ac..6374ec0d84 100644 --- a/kaspad/src/daemon.rs +++ b/kaspad/src/daemon.rs @@ -557,7 +557,7 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm None }; - let (address_manager, port_mapping_extender_svc) = AddressManager::new(config.clone(), meta_db, tick_service.clone()); + let (address_manager, port_mapping_extender_svc, dyndns_extender_svc) = AddressManager::new(config.clone(), meta_db, tick_service.clone()); let mining_manager = MiningManagerProxy::new(Arc::new(MiningManager::new_with_extended_config( config.target_time_per_block(), @@ -611,7 +611,7 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm notify_service.notifier(), index_service.as_ref().map(|x| x.notifier()), mining_manager, - flow_context, + flow_context.clone(), subscription_context, index_service.as_ref().map(|x| x.utxoindex().unwrap()), config.clone(), @@ -646,13 +646,17 @@ do you confirm? (answer y/n or pass --yes to the Kaspad command line to confirm if let Some(index_service) = index_service { async_runtime.register(index_service) }; + if let Some(port_mapping_extender_svc) = port_mapping_extender_svc { async_runtime.register(Arc::new(port_mapping_extender_svc)) }; + if let Some(dyndns_extender_svc) = dyndns_extender_svc { + async_runtime.register(Arc::new(dyndns_extender_svc)); + } async_runtime.register(rpc_core_service.clone()); if let Some(grpc_service) = grpc_service { async_runtime.register(grpc_service) - } + }; async_runtime.register(p2p_service); async_runtime.register(consensus_monitor); async_runtime.register(mining_monitor); diff --git a/protocol/flows/src/service.rs b/protocol/flows/src/service.rs index 1633e26db0..3d5dc8a0da 100644 --- a/protocol/flows/src/service.rs +++ b/protocol/flows/src/service.rs @@ -80,6 +80,12 @@ impl AsyncService for P2pService { self.flow_context.address_manager.clone(), ); + // Register as sink for external IP changes + self.flow_context + .address_manager + .lock() + .register_external_ip_change_sink(connection_manager.clone()); + self.flow_context.set_connection_manager(connection_manager.clone()); self.flow_context.start_async_services();