openrtc 0.2.1

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
//! Native LAN/BLE local peer discovery registry.

#![cfg(all(
    not(target_arch = "wasm32"),
    any(feature = "transport-lan", openrtc_unpublished_ble)
))]

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;

/// A peer discovered on the local network via mDNS.
#[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")]
/// Returns true when the selected iroh path remote address is a private or link-local IP.
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(any(feature = "transport-lan", openrtc_unpublished_ble))]
pub fn selected_path_is_ble(connection: &iroh::endpoint::Connection) -> bool {
    connection
        .paths()
        .iter()
        .any(|path| path.is_selected() && selected_path_remote_is_ble(path.remote_addr()))
}

#[cfg(all(
    openrtc_unpublished_ble,
    any(feature = "transport-lan", openrtc_unpublished_ble)
))]
fn selected_path_remote_is_ble(addr: &iroh::TransportAddr) -> bool {
    crate::transport::ble::is_ble_custom_addr(addr)
}

#[cfg(all(
    not(openrtc_unpublished_ble),
    any(feature = "transport-lan", openrtc_unpublished_ble)
))]
fn selected_path_remote_is_ble(_addr: &iroh::TransportAddr) -> bool {
    false
}

#[cfg(feature = "transport-lan")]
pub fn classify_iroh_path_kind(
    connection: &iroh::endpoint::Connection,
) -> crate::client::IrohPathKind {
    let paths = connection.paths();
    if selected_path_is_ble(connection) {
        return crate::client::IrohPathKind::Ble;
    }

    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()));
    }
}