use std::{ fmt, net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr, }; use clap::Parser; use thiserror::Error; use crate::DEFAULT_MAX_CLIENTS_PER_ROOM; pub const DEFAULT_RELAY_PORT: u16 = 443; #[derive(Debug, Clone, PartialEq, Eq, Parser)] #[command( name = "lanparty-relay", about = "Public QUIC relay for the LAN party L2 tunnel" )] pub struct RelayArgs { /// UDP listen address. Accepts a socket address or a port shorthand like 443/udp. #[arg(long, default_value_t = ListenEndpoint::default())] listen: ListenEndpoint, /// Maximum number of remote clients allowed in one room. #[arg(long = "max-clients-per-room", default_value_t = DEFAULT_MAX_CLIENTS_PER_ROOM)] max_clients_per_room: usize, } impl RelayArgs { pub fn into_config(self) -> Result { RelayConfig::new(self.listen, self.max_clients_per_room) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RelayConfig { listen: ListenEndpoint, max_clients_per_room: usize, } impl RelayConfig { pub fn new(listen: ListenEndpoint, max_clients_per_room: usize) -> Result { if max_clients_per_room == 0 { return Err(ConfigError::ZeroMaxClientsPerRoom); } Ok(Self { listen, max_clients_per_room, }) } #[must_use] pub const fn listen(&self) -> ListenEndpoint { self.listen } #[must_use] pub const fn max_clients_per_room(&self) -> usize { self.max_clients_per_room } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ListenEndpoint(SocketAddr); impl ListenEndpoint { #[must_use] pub const fn new(addr: SocketAddr) -> Self { Self(addr) } #[must_use] pub const fn socket_addr(self) -> SocketAddr { self.0 } } impl Default for ListenEndpoint { fn default() -> Self { Self(SocketAddr::new( IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_RELAY_PORT, )) } } impl fmt::Display for ListenEndpoint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}/udp", self.0) } } impl FromStr for ListenEndpoint { type Err = ConfigError; fn from_str(value: &str) -> Result { if let Some(value_without_udp) = value.strip_suffix("/udp") { if let Ok(port) = value_without_udp.parse() { return Ok(Self(SocketAddr::new( IpAddr::V4(Ipv4Addr::UNSPECIFIED), port, ))); } if let Ok(addr) = value_without_udp.parse::() { return Ok(Self(addr)); } } if let Ok(addr) = value.parse::() { return Ok(Self(addr)); } Err(ConfigError::InvalidListen { value: value.to_owned(), }) } } #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum ConfigError { #[error("listen endpoint must be a socket address or a port shorthand like 443/udp: {value}")] InvalidListen { value: String }, #[error("max clients per room must be greater than zero")] ZeroMaxClientsPerRoom, } #[cfg(test)] mod tests { use clap::Parser; use super::*; #[test] fn defaults_to_udp_443_on_all_interfaces() { let args = RelayArgs::parse_from(["lanparty-relay"]); let config = args.into_config().unwrap(); assert_eq!( config.listen().socket_addr(), SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_RELAY_PORT) ); assert_eq!(config.max_clients_per_room(), DEFAULT_MAX_CLIENTS_PER_ROOM); } #[test] fn parses_port_shorthand() { let endpoint: ListenEndpoint = "8443/udp".parse().unwrap(); assert_eq!( endpoint.socket_addr(), SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8443) ); } #[test] fn parses_socket_addr() { let endpoint: ListenEndpoint = "127.0.0.1:9443".parse().unwrap(); assert_eq!( endpoint.socket_addr(), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9443) ); } #[test] fn parses_socket_addr_with_udp_suffix() { let endpoint: ListenEndpoint = "127.0.0.1:9443/udp".parse().unwrap(); assert_eq!( endpoint.socket_addr(), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9443) ); } #[test] fn rejects_non_udp_shorthand() { assert!(matches!( "443/tcp".parse::().unwrap_err(), ConfigError::InvalidListen { .. } )); } #[test] fn rejects_bare_port_without_udp_suffix() { assert!(matches!( "443".parse::().unwrap_err(), ConfigError::InvalidListen { .. } )); } #[test] fn rejects_zero_max_clients() { assert_eq!( RelayConfig::new(ListenEndpoint::default(), 0).unwrap_err(), ConfigError::ZeroMaxClientsPerRoom ); } }