refactor (Opus 4.5): modularize and split
This commit is contained in:
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user