diff --git a/Cargo.lock b/Cargo.lock index a994b28..b8ac7b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1948,6 +1948,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "if-addrs" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624c5448ba529e74f594c65b7024f31b2de7b64a9b228b8df26796bbb6e32c36" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "if-addrs" version = "0.14.0" @@ -2206,6 +2216,7 @@ dependencies = [ "bytes", "eyre", "gethostname", + "if-addrs 0.11.1", "lanspread-compat", "lanspread-db", "lanspread-mdns", @@ -2425,7 +2436,7 @@ checksum = "3426fcc57a3b93e136cbc83861d30ccbc6e6eb8788bd09b6eb92565d29841029" dependencies = [ "fastrand", "flume", - "if-addrs", + "if-addrs 0.14.0", "log", "mio", "socket-pktinfo", diff --git a/Cargo.toml b/Cargo.toml index 36d3e00..37f899e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1", features = ["v7"] } walkdir = "2" +if-addrs = "0.11" windows = { version = "0.62", features = [ "Win32", "Win32_UI", diff --git a/crates/lanspread-peer/Cargo.toml b/crates/lanspread-peer/Cargo.toml index df60321..ad7bda5 100644 --- a/crates/lanspread-peer/Cargo.toml +++ b/crates/lanspread-peer/Cargo.toml @@ -29,3 +29,4 @@ tokio = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } walkdir = { workspace = true } +if-addrs = { workspace = true } diff --git a/crates/lanspread-peer/src/lib.rs b/crates/lanspread-peer/src/lib.rs index d0f43b8..ffaa9e7 100644 --- a/crates/lanspread-peer/src/lib.rs +++ b/crates/lanspread-peer/src/lib.rs @@ -5,13 +5,14 @@ mod peer; use std::{ collections::{HashMap, VecDeque}, - net::SocketAddr, + net::{IpAddr, SocketAddr}, path::{Path, PathBuf}, sync::Arc, time::{Duration, Instant}, }; use bytes::BytesMut; +use if_addrs::{IfAddr, Interface, get_if_addrs}; use lanspread_db::db::{Game, GameDB, GameFileDescription}; use lanspread_mdns::{LANSPREAD_SERVICE_TYPE, MdnsAdvertiser, discover_service}; use lanspread_proto::{Message, Request, Response}; @@ -1269,6 +1270,10 @@ async fn run_server_component( let server_addr = server.local_addr()?; log::info!("Peer server listening on {server_addr}"); + let advertise_ip = select_advertise_ip()?; + let advertise_addr = SocketAddr::new(advertise_ip, server_addr.port()); + log::info!("Advertising peer via mDNS from {advertise_addr}"); + // Start mDNS advertising for peer discovery let peer_id = Uuid::now_v7().simple().to_string(); let hostname = gethostname::gethostname(); @@ -1289,7 +1294,7 @@ async fn run_server_component( }; let mdns = tokio::task::spawn_blocking(move || { - MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, &combined_str, server_addr) + MdnsAdvertiser::new(LANSPREAD_SERVICE_TYPE, &combined_str, advertise_addr) }) .await??; @@ -1935,6 +1940,84 @@ async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result { Ok(is_alive) } +fn select_advertise_ip() -> eyre::Result { + let mut best_candidate: Option<(u8, IpAddr)> = None; + let mut loopback_fallback = None; + + for interface in get_if_addrs()? { + if interface.is_loopback() { + loopback_fallback.get_or_insert(interface.ip()); + continue; + } + + if let Some(candidate) = classify_interface(&interface) + && best_candidate + .as_ref() + .is_none_or(|(rank, _)| candidate.0 < *rank) + { + best_candidate = Some(candidate); + } + } + + if let Some((_, ip)) = best_candidate { + return Ok(ip); + } + + if let Some(ip) = loopback_fallback { + log::warn!( + "No non-loopback interface suitable for mDNS advertisement; falling back to {ip}" + ); + return Ok(ip); + } + + eyre::bail!("No usable network interface found for mDNS advertisement"); +} + +fn classify_interface(interface: &Interface) -> Option<(u8, IpAddr)> { + match interface.addr { + IfAddr::V4(ref v4) => { + let ip = v4.ip; + + if ip.is_unspecified() || ip.is_link_local() { + return None; + } + + let mut rank = if ip.is_private() { 0 } else { 2 }; + + if is_virtual_interface(&interface.name) { + rank += 2; + } + + Some((rank, IpAddr::V4(ip))) + } + IfAddr::V6(_) => None, + } +} + +fn is_virtual_interface(name: &str) -> bool { + const VIRTUAL_HINTS: &[&str] = &[ + "awdl", + "br-", + "bridge", + "docker", + "ham", + "llw", + "tap", + "tailscale", + "tun", + "utun", + "vbox", + "veth", + "virbr", + "vmnet", + "wg", + "zt", + ]; + + let lower = name.to_ascii_lowercase(); + VIRTUAL_HINTS.iter().any(|hint| lower.contains(hint)) +} + async fn get_game_file_descriptions( game_id: &str, game_dir: &str,