diff --git a/CHANGELOG.md b/CHANGELOG.md index b6432b7..6b82943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to the `ant` binary will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- Default network binding changed from IPv4-only to IPv6 dual-stack. Hosts without a working IPv6 stack should pass `--ipv4-only` to avoid advertising unreachable v6 addresses to the DHT (which causes slow connects and junk address records). + ## [0.1.1] - 2026-03-28 ### Added diff --git a/ant-cli/src/cli.rs b/ant-cli/src/cli.rs index 1be88b7..664d815 100644 --- a/ant-cli/src/cli.rs +++ b/ant-cli/src/cli.rs @@ -31,6 +31,12 @@ pub struct Cli { #[arg(long)] pub allow_loopback: bool, + /// Force IPv4-only mode (disable dual-stack). + /// Use on hosts without working IPv6 to avoid advertising + /// unreachable addresses to the DHT. + #[arg(long)] + pub ipv4_only: bool, + /// Timeout for lightweight network operations such as quotes and DHT /// lookups (seconds). #[arg(long, default_value_t = 10)] diff --git a/ant-cli/src/main.rs b/ant-cli/src/main.rs index 69e7267..d292de4 100644 --- a/ant-cli/src/main.rs +++ b/ant-cli/src/main.rs @@ -70,6 +70,7 @@ async fn run() -> anyhow::Result<()> { bootstrap, devnet_manifest, allow_loopback, + ipv4_only, quote_timeout_secs, store_timeout_secs, verbose, @@ -83,6 +84,7 @@ async fn run() -> anyhow::Result<()> { bootstrap, devnet_manifest, allow_loopback, + ipv4_only, quote_timeout_secs, store_timeout_secs, evm_network, @@ -152,6 +154,7 @@ struct DataCliContext { bootstrap: Vec, devnet_manifest: Option, allow_loopback: bool, + ipv4_only: bool, quote_timeout_secs: u64, store_timeout_secs: u64, evm_network: String, @@ -178,11 +181,12 @@ async fn build_data_client( // Connection phase with animated spinner showing peer discovery in real-time let node = if quiet { - create_client_node(bootstrap, ctx.allow_loopback).await? + create_client_node(bootstrap, ctx.allow_loopback, ctx.ipv4_only).await? } else { let spinner = new_spinner("Connecting to autonomi network..."); - let node = match create_client_node_raw(bootstrap, ctx.allow_loopback).await { + let node = match create_client_node_raw(bootstrap, ctx.allow_loopback, ctx.ipv4_only).await + { Ok(n) => n, Err(e) => { spinner.finish_and_clear(); @@ -377,8 +381,9 @@ fn resolve_bootstrap_from( async fn create_client_node( bootstrap: Vec, allow_loopback: bool, + ipv4_only: bool, ) -> anyhow::Result> { - let node = create_client_node_raw(bootstrap, allow_loopback).await?; + let node = create_client_node_raw(bootstrap, allow_loopback, ipv4_only).await?; node.start() .await .map_err(|e| anyhow::anyhow!("Failed to start P2P node: {e}"))?; @@ -389,10 +394,11 @@ async fn create_client_node( async fn create_client_node_raw( bootstrap: Vec, allow_loopback: bool, + ipv4_only: bool, ) -> anyhow::Result> { let mut core_config = CoreNodeConfig::builder() .port(0) - .ipv6(false) + .ipv6(!ipv4_only) .local(allow_loopback) .mode(NodeMode::Client) .max_message_size(MAX_WIRE_MESSAGE_SIZE) diff --git a/ant-core/src/data/client/mod.rs b/ant-core/src/data/client/mod.rs index ccce50d..4cc4c00 100644 --- a/ant-core/src/data/client/mod.rs +++ b/ant-core/src/data/client/mod.rs @@ -78,6 +78,14 @@ pub struct ClientConfig { /// defaults to `false` and threads through to the same /// `CoreNodeConfig::builder().local(...)` call. pub allow_loopback: bool, + /// Bind a dual-stack IPv6 socket (`true`) or an IPv4-only socket + /// (`false`). Defaults to `true`, matching the CLI default. + /// + /// Set to `false` only when running on hosts without a working IPv6 + /// stack, to avoid advertising unreachable v6 addresses to the DHT + /// (which causes slow connects and junk DHT address records). This + /// mirrors the `--ipv4-only` flag in `ant-cli`. + pub ipv6: bool, } impl Default for ClientConfig { @@ -89,6 +97,7 @@ impl Default for ClientConfig { quote_concurrency: DEFAULT_QUOTE_CONCURRENCY, store_concurrency: DEFAULT_STORE_CONCURRENCY, allow_loopback: false, + ipv6: true, } } } @@ -123,9 +132,10 @@ impl Client { /// Create a client connected to bootstrap peers. /// - /// Threads `config.allow_loopback` through to `Network::new`, which - /// controls the saorsa-transport `local` flag on the underlying - /// `CoreNodeConfig`. See `ClientConfig::allow_loopback` for details. + /// Threads `config.allow_loopback` and `config.ipv6` through to + /// `Network::new`, which controls the saorsa-transport `local` and + /// `ipv6` flags on the underlying `CoreNodeConfig`. See + /// `ClientConfig::allow_loopback` and `ClientConfig::ipv6` for details. /// /// # Errors /// @@ -135,11 +145,12 @@ impl Client { config: ClientConfig, ) -> Result { debug!( - "Connecting to Autonomi network with {} bootstrap peers (allow_loopback={})", + "Connecting to Autonomi network with {} bootstrap peers (allow_loopback={}, ipv6={})", bootstrap_peers.len(), config.allow_loopback, + config.ipv6, ); - let network = Network::new(bootstrap_peers, config.allow_loopback).await?; + let network = Network::new(bootstrap_peers, config.allow_loopback, config.ipv6).await?; Ok(Self { config, network, diff --git a/ant-core/src/data/network.rs b/ant-core/src/data/network.rs index da0835a..929a981 100644 --- a/ant-core/src/data/network.rs +++ b/ant-core/src/data/network.rs @@ -25,16 +25,27 @@ impl Network { /// testing. Public Autonomi network peers reject the QUIC handshake /// variant produced when `local = true`, so production callers must pass /// `false` (this is what `ant-cli` does by default — see - /// `ant-cli/src/main.rs::create_client_node_raw`, which builds the same - /// `CoreNodeConfig` directly with `.local(allow_loopback)`). + /// `ant-cli/src/main.rs::create_client_node_raw`, which builds a similar + /// `CoreNodeConfig` directly, with `ipv6` toggled by the `--ipv4-only` + /// flag). + /// + /// `ipv6` controls whether the node binds a dual-stack IPv6 socket + /// (`true`) or an IPv4-only socket (`false`). The default for library + /// callers should be `true` to match the CLI default; set it to `false` + /// only when running on hosts without a working IPv6 stack, to avoid + /// advertising unreachable v6 addresses to the DHT. /// /// # Errors /// /// Returns an error if the P2P node cannot be created or bootstrapping fails. - pub async fn new(bootstrap_peers: &[SocketAddr], allow_loopback: bool) -> Result { + pub async fn new( + bootstrap_peers: &[SocketAddr], + allow_loopback: bool, + ipv6: bool, + ) -> Result { let mut core_config = CoreNodeConfig::builder() .port(0) - .ipv6(true) + .ipv6(ipv6) .local(allow_loopback) .mode(NodeMode::Client) .max_message_size(MAX_WIRE_MESSAGE_SIZE) diff --git a/ant-core/tests/support/mod.rs b/ant-core/tests/support/mod.rs index 166a130..9f3942c 100644 --- a/ant-core/tests/support/mod.rs +++ b/ant-core/tests/support/mod.rs @@ -200,6 +200,9 @@ impl MiniTestnet { // Generate ML-DSA-65 identity for this node let identity = Arc::new(NodeIdentity::generate().expect("generate node identity")); + // IPv4-only is intentional for these loopback-only tests: everything + // binds to 127.0.0.1 on the local host, so dual-stack would add no + // value and pulls in v6 loopback quirks on some CI runners. let mut core_config = CoreNodeConfig::builder() .port(listen_addr.port()) .ipv6(false)