Our Info panel, hole punch race fix, NAT profiles in relay introduction

- Network Diagnostics: "Our Info" button shows addresses with NAT status,
  device role, UPnP, HTTP capability. Addresses stacked for mobile.
- Hole punch race: re-check for existing connection before and after relay
  introduction to avoid wasting minutes on redundant punch attempts.
- Relay introduction now carries requester/target NAT mapping+filtering
  so hole punch strategy uses fresh profiles instead of stale stored ones.
  Critical for phones that switch between WiFi/cellular/VPN.
- STUN fix: filter DNS results to IPv4 (was resolving to IPv6 first on
  dual-stack, causing silent send failure and "NAT unknown").
- Welcome screen: Ready button with loading bar for instant feed access.
- LAN addresses show just "LAN" (no misleading punchability label).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-04-05 17:57:41 -04:00
parent e354ccc388
commit be253e8001
7 changed files with 266 additions and 9 deletions

View file

@ -1623,6 +1623,120 @@ async fn get_network_summary(state: State<'_, AppNode>) -> Result<NetworkSummary
})
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct OurInfoDto {
node_id: String,
addresses: Vec<AddressInfoDto>,
nat_type: String,
device_role: String,
upnp: bool,
http_capable: bool,
http_addr: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AddressInfoDto {
addr: String,
family: String, // "IPv4" or "IPv6"
status: String, // "Public", "NAT (easy)", "NAT (hard)", "UPnP", "LAN", "Server"
}
#[tauri::command]
async fn get_our_info(state: State<'_, AppNode>) -> Result<OurInfoDto, String> {
let node = get_node(&state).await;
let net = &node.network;
let nat_type = net.conn_handle().nat_type().await;
let has_upnp = net.has_upnp();
let _has_public_v6 = net.has_public_v6();
let bind_addr = net.bind_addr();
let mut addresses = Vec::new();
// Collect bound socket addresses (these are our local interfaces)
let bound: std::collections::HashSet<String> = net.bound_sockets()
.iter()
.map(|s| s.to_string())
.collect();
// Gather bound (local) addresses with classification
for sock in net.bound_sockets() {
if sock.ip().is_loopback() || sock.ip().is_unspecified() { continue; }
let family = if sock.ip().is_ipv4() { "IPv4" } else { "IPv6" };
let status = classify_addr(&sock, &nat_type, has_upnp, bind_addr.is_some(), false);
addresses.push(AddressInfoDto {
addr: sock.to_string(),
family: family.to_string(),
status,
});
}
// Add UPnP external address if different from bound
if let Some(ref mapping) = net.upnp_mapping() {
let ext = mapping.external_addr;
if !addresses.iter().any(|a| a.addr == ext.to_string()) {
addresses.insert(0, AddressInfoDto {
addr: ext.to_string(),
family: if ext.ip().is_ipv4() { "IPv4" } else { "IPv6" }.to_string(),
status: "UPnP external".to_string(),
});
}
}
// Add iroh-discovered addresses not already listed (STUN-observed externals)
for sock in net.endpoint_addr().ip_addrs() {
if sock.ip().is_loopback() || sock.ip().is_unspecified() { continue; }
let s = sock.to_string();
if addresses.iter().any(|a| a.addr == s) { continue; }
let family = if sock.ip().is_ipv4() { "IPv4" } else { "IPv6" };
let is_observed = !bound.contains(&s);
let status = classify_addr(sock, &nat_type, has_upnp, bind_addr.is_some(), is_observed);
addresses.push(AddressInfoDto {
addr: s,
family: family.to_string(),
status,
});
}
Ok(OurInfoDto {
node_id: hex::encode(net.node_id_bytes()),
addresses,
nat_type: nat_type.to_string(),
device_role: format!("{:?}", net.device_role()),
upnp: has_upnp,
http_capable: net.is_http_capable(),
http_addr: net.http_addr(),
})
}
fn classify_addr(sock: &std::net::SocketAddr, nat_type: &itsgoin_core::types::NatType, has_upnp: bool, is_server: bool, is_observed: bool) -> String {
use std::net::IpAddr;
let ip = sock.ip();
let is_v6 = ip.is_ipv6();
let is_pub = match ip {
IpAddr::V4(v4) => !v4.is_private() && !v4.is_loopback() && !v4.is_link_local(),
IpAddr::V6(v6) => !v6.is_loopback() && {
let seg = v6.segments();
seg[0] & 0xfe00 != 0xfe00 && seg[0] & 0xffc0 != 0xfe80
},
};
if is_server { return "Server".to_string(); }
if is_pub {
if is_observed && !is_v6 {
return match nat_type {
itsgoin_core::types::NatType::Public |
itsgoin_core::types::NatType::Easy => "External · easy punch".to_string(),
itsgoin_core::types::NatType::Hard => "External · hard punch (scan)".to_string(),
itsgoin_core::types::NatType::Unknown => "External".to_string(),
};
}
return "Public".to_string();
}
if is_v6 { return "Link-local".to_string(); }
"LAN".to_string()
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ActivityEventDto {
@ -2387,6 +2501,7 @@ pub fn run() {
sync_all,
sync_from_peer,
get_network_summary,
get_our_info,
get_activity_log,
trigger_rebalance,
request_referrals,