Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 1 addition & 153 deletions src/p2p_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
//! ```

use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};

Expand Down Expand Up @@ -1154,158 +1154,6 @@ impl P2pEndpoint {
.cloned()
}

/// Connect to a peer using dual-stack strategy (tries both IPv4 and IPv6 in parallel)
///
/// This method implements the user requirement: **"connect on ip4 and 6 we do both"**
///
/// **Strategy**:
/// 1. Separates addresses by family (IPv4 vs IPv6)
/// 2. Tries both families in parallel using `tokio::join!`
/// 3. Handles all scenarios:
/// - **Both work**: Keeps dual connections for redundancy (BEST CASE)
/// - **IPv4-only**: Uses IPv4 connection, graceful degradation
///
/// This method implements the user requirement: **"connect on ip4 and 6 we do both"**
///
/// **Strategy**:
/// 1. Separates addresses by family (IPv4 vs IPv6)
/// 2. Tries both families in parallel using `tokio::join!`
/// 3. Handles all scenarios:
/// - **Both work**: Keeps dual connections for redundancy (BEST CASE)
/// - **IPv4-only**: Uses IPv4 connection, graceful degradation
/// - **IPv6-only**: Uses IPv6 connection, graceful degradation
/// - **Neither**: Returns error (try NAT traversal next)
///
/// # Arguments
/// * `addresses` - List of candidate addresses (mix of IPv4 and IPv6)
///
/// # Returns
/// Primary connection (IPv6 preferred if both succeed)
///
/// # Dual-Connection Behavior
/// When both IPv4 AND IPv6 succeed, BOTH connections are stored in `connected_peers`.
/// The system maintains redundant connections for maximum reliability.
pub async fn connect_dual_stack(
&self,
addresses: &[SocketAddr],
) -> Result<PeerConnection, EndpointError> {
if self.shutdown.is_cancelled() {
return Err(EndpointError::ShuttingDown);
}

// Separate addresses by family
let ipv4_addrs: Vec<SocketAddr> = addresses
.iter()
.filter(|addr| matches!(addr.ip(), IpAddr::V4(_)))
.copied()
.collect();

let ipv6_addrs: Vec<SocketAddr> = addresses
.iter()
.filter(|addr| matches!(addr.ip(), IpAddr::V6(_)))
.copied()
.collect();

info!(
"Dual-stack connect: {} IPv4, {} IPv6 addresses",
ipv4_addrs.len(),
ipv6_addrs.len(),
);

// Use "peer" as SNI for all P2P connections
// Raw Public Key authentication validates the peer's public key directly,
// so we don't need/use SNI for authentication. A fixed SNI avoids
// "invalid server name" errors from hex peer IDs being too long.
let (ipv4_result, ipv6_result) = tokio::join!(
self.try_connect_family(&ipv4_addrs, "IPv4"),
self.try_connect_family(&ipv6_addrs, "IPv6"),
);

// Handle all possible outcomes
match (ipv4_result, ipv6_result) {
(Some(v4_conn), Some(v6_conn)) => {
// πŸŽ‰ BEST CASE: Both IPv4 AND IPv6 work - keep both!
info!(
"βœ“βœ“ Dual-stack success! IPv4: {}, IPv6: {} (maintaining both connections)",
v4_conn.remote_addr, v6_conn.remote_addr
);

// Both connections already stored by try_connect_family
// Return IPv6 as primary (modern internet best practice)
Ok(v6_conn)
}

(Some(v4_conn), None) => {
// IPv4-only network (v6 unavailable or failed)
info!(
"IPv4-only connection established to {}",
v4_conn.remote_addr
);
Ok(v4_conn)
}

(None, Some(v6_conn)) => {
// IPv6-only network (v4 unavailable or failed)
info!(
"IPv6-only connection established to {}",
v6_conn.remote_addr
);
Ok(v6_conn)
}

(None, None) => {
// Neither direct connection works - try NAT traversal next
warn!("Both IPv4 and IPv6 direct connections failed");
Err(EndpointError::Connection(
"Dual-stack connection failed for both address families".to_string(),
))
}
}
}

/// Try to connect using addresses from one family (IPv4 or IPv6)
///
async fn try_connect_family(
&self,
addresses: &[SocketAddr],
family_name: &str,
) -> Option<PeerConnection> {
if addresses.is_empty() {
debug!("{}: No addresses to try", family_name);
return None;
}

debug!("Trying {} {} addresses", addresses.len(), family_name);

for (idx, addr) in addresses.iter().enumerate() {
debug!(
" {} attempt {}/{}: {}",
family_name,
idx + 1,
addresses.len(),
addr
);

match timeout(Duration::from_secs(5), self.connect(*addr)).await {
Ok(Ok(peer_conn)) => {
info!("βœ“ {} connection successful to {}", family_name, addr);
return Some(peer_conn);
}
Ok(Err(e)) => {
debug!(" {} to {} failed: {}", family_name, addr, e);
// Try next address
}
Err(_) => {
debug!(" {} to {} timed out (5s)", family_name, addr);
// Try next address
}
}
}

debug!("{}: All {} addresses failed", family_name, addresses.len());
None
}

/// Connect with automatic fallback: IPv4 β†’ IPv6 β†’ HolePunch β†’ Relay
///
/// This method implements a progressive connection strategy that automatically
Expand Down
Loading