refactor (Opus 4.5): modularize and split

This commit is contained in:
2025-11-28 21:10:42 +01:00
parent df01131f8d
commit 53c7fe10ba
11 changed files with 3301 additions and 2729 deletions
+256
View File
@@ -0,0 +1,256 @@
//! 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::{Game, GameFileDescription};
use lanspread_proto::{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<Connection> {
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<bool> {
let mut conn = connect_to_peer(peer_addr).await?;
let is_alive = initial_peer_alive_check(&mut conn).await;
Ok(is_alive)
}
/// Fetches the list of games from a peer.
pub async fn fetch_games_from_peer(peer_addr: SocketAddr) -> eyre::Result<Vec<Game>> {
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());
// Send ListGames request
framed_tx.send(Request::ListGames.encode()).await?;
let _ = framed_tx.close().await;
// Receive response
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());
if let Response::ListGames(games) = response {
Ok(games)
} else {
log::warn!("Unexpected response from peer {peer_addr}: {response:?}");
Ok(Vec::new())
}
}
/// Announces local games to a peer.
pub async fn announce_games_to_peer(peer_addr: SocketAddr, games: Vec<Game>) -> 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());
// Send AnnounceGames request
framed_tx
.send(Request::AnnounceGames(games).encode())
.await?;
let _ = framed_tx.close().await;
Ok(())
}
/// Requests game file details from a peer.
pub async fn request_game_details_from_peer(
peer_addr: SocketAddr,
game_id: &str,
) -> eyre::Result<(Vec<GameFileDescription>, 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<IpAddr> {
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))
}