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:
parent
e354ccc388
commit
be253e8001
7 changed files with 266 additions and 9 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue