#![cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg(feature = "transport-lan")]
use iroh_mdns_address_lookup::{DiscoveryEvent, MdnsAddressLookup};
#[cfg(feature = "transport-lan")]
use n0_future::StreamExt;
#[cfg(feature = "transport-lan")]
use std::net::IpAddr;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LocalPeerSnapshot {
pub node_id: String,
pub discovered_at_ms: i64,
pub local_reachable: bool,
}
#[derive(Clone)]
pub struct LocalDiscoveryRegistry {
peers: Arc<RwLock<HashMap<String, LocalPeerSnapshot>>>,
}
impl LocalDiscoveryRegistry {
pub fn new() -> Self {
Self {
peers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn list_peers(&self) -> Vec<LocalPeerSnapshot> {
self.peers
.read()
.await
.values()
.cloned()
.collect::<Vec<_>>()
}
pub async fn is_locally_reachable(&self, node_id: &str) -> bool {
self.peers
.read()
.await
.get(node_id)
.map(|peer| peer.local_reachable)
.unwrap_or(false)
}
pub async fn upsert_peer(&self, node_id: String) {
let now = crate::firebase::now_millis_u64().min(i64::MAX as u64) as i64;
self.peers.write().await.insert(
node_id.clone(),
LocalPeerSnapshot {
node_id,
discovered_at_ms: now,
local_reachable: true,
},
);
}
pub async fn remove_peer(&self, node_id: &str) {
self.peers.write().await.remove(node_id);
}
}
impl Default for LocalDiscoveryRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "transport-lan")]
pub fn is_lan_ip_addr(addr: &std::net::SocketAddr) -> bool {
match addr.ip() {
IpAddr::V4(v4) => {
v4.is_private() || v4.is_link_local() || v4.is_loopback() || v4.octets()[0] == 169
}
IpAddr::V6(v6) => v6.is_unique_local() || v6.is_unicast_link_local() || v6.is_loopback(),
}
}
#[cfg(feature = "transport-lan")]
pub fn selected_path_is_direct_lan(connection: &iroh::endpoint::Connection) -> bool {
connection.paths().iter().any(|path| {
if !path.is_selected() || !path.is_ip() {
return false;
}
matches!(path.remote_addr(), iroh::TransportAddr::Ip(addr) if is_lan_ip_addr(addr))
})
}
#[cfg(feature = "transport-lan")]
pub fn classify_iroh_path_kind(
connection: &iroh::endpoint::Connection,
) -> crate::client::IrohPathKind {
let paths = connection.paths();
let has_direct_lan = selected_path_is_direct_lan(connection);
let has_direct = paths.iter().any(|p| p.is_selected() && p.is_ip());
let has_relay = paths.iter().any(|p| p.is_selected() && p.is_relay());
if has_direct_lan {
crate::client::IrohPathKind::DirectLan
} else if has_direct {
crate::client::IrohPathKind::DirectQuic
} else if has_relay {
crate::client::IrohPathKind::Relay
} else {
crate::client::IrohPathKind::Unknown
}
}
#[cfg(feature = "transport-lan")]
pub fn spawn_mdns_discovery_task(
mdns: MdnsAddressLookup,
registry: LocalDiscoveryRegistry,
local_node_id: String,
) {
tokio::spawn(async move {
let mut events = mdns.subscribe().await;
while let Some(event) = events.next().await {
match event {
DiscoveryEvent::Discovered { endpoint_info, .. } => {
let node_id = endpoint_info.endpoint_id.to_string();
if node_id == local_node_id {
continue;
}
registry.upsert_peer(node_id).await;
}
DiscoveryEvent::Expired { endpoint_id } => {
registry.remove_peer(&endpoint_id.to_string()).await;
}
_ => {}
}
}
});
}
#[cfg(all(test, feature = "transport-lan"))]
mod tests {
use super::*;
#[test]
fn lan_ip_classification() {
assert!(is_lan_ip_addr(&"192.168.1.10:4433".parse().unwrap()));
assert!(is_lan_ip_addr(&"10.0.0.5:4433".parse().unwrap()));
assert!(is_lan_ip_addr(&"127.0.0.1:4433".parse().unwrap()));
assert!(!is_lan_ip_addr(&"8.8.8.8:4433".parse().unwrap()));
}
}