//! Network utilities for QUIC connections and peer communication. use std::{ net::{IpAddr, SocketAddr}, time::Duration, }; use bytes::BytesMut; use futures::{SinkExt, StreamExt}; use if_addrs::{IfAddr, Interface, get_if_addrs}; use lanspread_db::db::GameFileDescription; use lanspread_proto::{Hello, HelloAck, LibraryDelta, Message, Request, Response}; use s2n_quic::{Client as QuicClient, Connection, client::Connect, provider::limits::Limits}; use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; use crate::config::CERT_PEM; /// Establishes a QUIC connection to a peer. pub async fn connect_to_peer(addr: SocketAddr) -> eyre::Result { let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?; let client = QuicClient::builder() .with_tls(CERT_PEM)? .with_io("0.0.0.0:0")? .with_limits(limits)? .start()?; let conn = Connect::new(addr).with_server_name("localhost"); let conn = client.connect(conn).await?; Ok(conn) } /// Performs an initial ping check to verify peer is alive. pub async fn initial_peer_alive_check(conn: &mut Connection) -> bool { let remote_addr = conn.remote_addr().ok(); let stream = match conn.open_bidirectional_stream().await { Ok(stream) => stream, Err(e) => { log::error!("{remote_addr:?} failed to open stream: {e}"); return false; } }; let (rx, tx) = stream.split(); let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new()); let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new()); // send ping if let Err(e) = framed_tx.send(Request::Ping.encode()).await { log::error!("{remote_addr:?} failed to send ping to peer: {e}"); return false; } let _ = framed_tx.close().await; // receive pong if let Some(Ok(response_bytes)) = framed_rx.next().await { let response = Response::decode(response_bytes.freeze()); match response { Response::Pong => { log::trace!("{remote_addr:?} peer is alive"); return true; } _ => { log::error!("{remote_addr:?} peer sent invalid response to ping: {response:?}"); } } } false } /// Pings a peer to check if it's alive. pub async fn ping_peer(peer_addr: SocketAddr) -> eyre::Result { let mut conn = connect_to_peer(peer_addr).await?; let is_alive = initial_peer_alive_check(&mut conn).await; Ok(is_alive) } /// Sends a single request without waiting for a response. pub async fn send_oneway_request(peer_addr: SocketAddr, request: Request) -> eyre::Result<()> { let mut conn = connect_to_peer(peer_addr).await?; let stream = conn.open_bidirectional_stream().await?; let (_, tx) = stream.split(); let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new()); framed_tx.send(request.encode()).await?; let _ = framed_tx.close().await; Ok(()) } /// Performs a hello/ack handshake with a peer. pub async fn exchange_hello(peer_addr: SocketAddr, hello: Hello) -> eyre::Result { let mut conn = connect_to_peer(peer_addr).await?; let stream = conn.open_bidirectional_stream().await?; let (rx, tx) = stream.split(); let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new()); let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new()); framed_tx.send(Request::Hello(hello).encode()).await?; let _ = framed_tx.close().await; let mut data = BytesMut::new(); while let Some(Ok(bytes)) = framed_rx.next().await { data.extend_from_slice(&bytes); } let response = Response::decode(data.freeze()); match response { Response::HelloAck(ack) => Ok(ack), other => eyre::bail!("Unexpected response from peer {peer_addr}: {other:?}"), } } pub async fn send_library_delta( peer_addr: SocketAddr, peer_id: &str, delta: LibraryDelta, ) -> eyre::Result<()> { send_oneway_request( peer_addr, Request::LibraryDelta { peer_id: peer_id.to_string(), delta, }, ) .await } pub async fn send_goodbye(peer_addr: SocketAddr, peer_id: String) -> eyre::Result<()> { send_oneway_request(peer_addr, Request::Goodbye { peer_id }).await } /// Requests game file details from a peer. pub async fn request_game_details_from_peer( peer_addr: SocketAddr, game_id: &str, ) -> eyre::Result<(Vec, Response)> { let mut conn = connect_to_peer(peer_addr).await?; let stream = conn.open_bidirectional_stream().await?; let (rx, tx) = stream.split(); let mut framed_rx = FramedRead::new(rx, LengthDelimitedCodec::new()); let mut framed_tx = FramedWrite::new(tx, LengthDelimitedCodec::new()); framed_tx .send( Request::GetGame { id: game_id.to_string(), } .encode(), ) .await?; framed_tx.close().await?; let mut data = BytesMut::new(); while let Some(Ok(bytes)) = framed_rx.next().await { data.extend_from_slice(&bytes); } let response = Response::decode(data.freeze()); match &response { Response::GetGame { id, file_descriptions, } => { if id != game_id { eyre::bail!("peer {peer_addr} responded with mismatched game id {id}"); } Ok((file_descriptions.clone(), response)) } Response::GameNotFound(_) => { eyre::bail!("peer {peer_addr} does not have game {game_id}") } Response::InternalPeerError(error_msg) => { eyre::bail!("peer {peer_addr} reported internal error: {error_msg}") } _ => eyre::bail!("unexpected response from {peer_addr}: {response:?}"), } } // ============================================================================= // IP address selection for mDNS advertisement // ============================================================================= /// Selects the best IP address to advertise via mDNS. pub 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"); } /// Classifies a network interface for mDNS advertisement priority. 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, } } /// Checks if an interface name suggests it's a virtual interface. 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)) }