Skip to content

Commit 6a8ea95

Browse files
committed
feat(p2p): auto-detect public IP if EXTERNAL_ADDR not set
Uses ipify.org, ifconfig.me, icanhazip.com, ipinfo.io to detect public IP. Validates the IP is not private/local before announcing.
1 parent 4d55528 commit 6a8ea95

File tree

1 file changed

+105
-8
lines changed

1 file changed

+105
-8
lines changed

crates/p2p-consensus/src/network.rs

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -746,16 +746,26 @@ impl P2PNetwork {
746746

747747
// Add external addresses for announcement to other peers
748748
// This is crucial for NAT traversal - we announce our public IP instead of internal Docker IPs
749-
for addr_str in &self.config.external_addrs {
750-
match addr_str.parse::<Multiaddr>() {
751-
Ok(addr) => {
752-
swarm.add_external_address(addr.clone());
753-
info!(addr = %addr, "Added external address for announcement");
754-
}
755-
Err(e) => {
756-
error!(addr = %addr_str, error = %e, "Invalid external address");
749+
if !self.config.external_addrs.is_empty() {
750+
for addr_str in &self.config.external_addrs {
751+
match addr_str.parse::<Multiaddr>() {
752+
Ok(addr) => {
753+
swarm.add_external_address(addr.clone());
754+
info!(addr = %addr, "Added external address for announcement");
755+
}
756+
Err(e) => {
757+
error!(addr = %addr_str, error = %e, "Invalid external address");
758+
}
757759
}
758760
}
761+
} else {
762+
// Auto-detect public IP if no external address configured
763+
if let Some(external_addr) = detect_public_ip().await {
764+
swarm.add_external_address(external_addr.clone());
765+
info!(addr = %external_addr, "Auto-detected public IP for announcement");
766+
} else {
767+
warn!("Could not detect public IP. Peers may not be able to connect directly. Set EXTERNAL_ADDR to configure manually.");
768+
}
759769
}
760770

761771
// Connect to bootstrap peers
@@ -1138,6 +1148,93 @@ fn extract_peer_id(addr: &Multiaddr) -> Option<PeerId> {
11381148
})
11391149
}
11401150

1151+
/// Detect public IP address using external services
1152+
/// Returns a multiaddr with the detected IP and default P2P port
1153+
async fn detect_public_ip() -> Option<Multiaddr> {
1154+
use crate::config::DEFAULT_P2P_PORT;
1155+
1156+
// Try multiple services for reliability
1157+
let services = [
1158+
"https://api.ipify.org",
1159+
"https://ifconfig.me/ip",
1160+
"https://icanhazip.com",
1161+
"https://ipinfo.io/ip",
1162+
];
1163+
1164+
for service in services {
1165+
match tokio::time::timeout(
1166+
Duration::from_secs(5),
1167+
fetch_public_ip(service),
1168+
).await {
1169+
Ok(Some(ip)) => {
1170+
// Validate it's a public IP (not private/local)
1171+
if is_public_ip(&ip) {
1172+
let addr_str = format!("/ip4/{}/tcp/{}", ip, DEFAULT_P2P_PORT);
1173+
if let Ok(addr) = addr_str.parse::<Multiaddr>() {
1174+
return Some(addr);
1175+
}
1176+
}
1177+
}
1178+
Ok(None) => continue,
1179+
Err(_) => {
1180+
debug!(service = %service, "Timeout detecting public IP");
1181+
continue;
1182+
}
1183+
}
1184+
}
1185+
1186+
None
1187+
}
1188+
1189+
/// Fetch public IP from a service using curl
1190+
async fn fetch_public_ip(url: &str) -> Option<String> {
1191+
let output = tokio::process::Command::new("curl")
1192+
.args(["-s", "--max-time", "3", url])
1193+
.output()
1194+
.await
1195+
.ok()?;
1196+
1197+
if output.status.success() {
1198+
let ip = String::from_utf8_lossy(&output.stdout).trim().to_string();
1199+
// Validate it looks like an IP
1200+
if ip.parse::<std::net::Ipv4Addr>().is_ok() {
1201+
return Some(ip);
1202+
}
1203+
}
1204+
1205+
None
1206+
}
1207+
1208+
/// Check if an IP address is public (not private/local/reserved)
1209+
fn is_public_ip(ip: &str) -> bool {
1210+
if let Ok(addr) = ip.parse::<std::net::Ipv4Addr>() {
1211+
let octets = addr.octets();
1212+
// Private ranges: 10.x.x.x, 172.16-31.x.x, 192.168.x.x
1213+
// Loopback: 127.x.x.x
1214+
// Link-local: 169.254.x.x
1215+
if octets[0] == 10 {
1216+
return false;
1217+
}
1218+
if octets[0] == 172 && (16..=31).contains(&octets[1]) {
1219+
return false;
1220+
}
1221+
if octets[0] == 192 && octets[1] == 168 {
1222+
return false;
1223+
}
1224+
if octets[0] == 127 {
1225+
return false;
1226+
}
1227+
if octets[0] == 169 && octets[1] == 254 {
1228+
return false;
1229+
}
1230+
if octets[0] == 0 {
1231+
return false;
1232+
}
1233+
return true;
1234+
}
1235+
false
1236+
}
1237+
11411238
/// Network runner that processes swarm events
11421239
pub struct NetworkRunner {
11431240
network: Arc<P2PNetwork>,

0 commit comments

Comments
 (0)