diff --git a/Cargo.toml b/Cargo.toml index e95a690..5a46dbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,3 +32,22 @@ thiserror = "2" tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] } tracing = "0.1" windows-sys = "0.61.2" + +[profile.release] +debug = true +strip = false +debug-assertions = true +overflow-checks = true +lto = false +panic = "unwind" +incremental = true + +[profile.production] +inherits = "release" +debug = false +strip = true +debug-assertions = false +overflow-checks = false +lto = true +incremental = false +codegen-units = 1 diff --git a/README.md b/README.md index 60d14d8..ab639b1 100644 --- a/README.md +++ b/README.md @@ -123,17 +123,36 @@ Public relay binary and relay-owned room state: ## Build And Local Checks ```bash -cargo fmt --check -cargo test --workspace -cargo clippy --workspace --all-targets -- -D warnings -cargo build --release -p lanparty-relay -p lanparty-gateway +just fmt +just test +just clippy +just build-release git diff --check ``` +`just clippy` must stay completely clean: every crate turns on +`clippy::pedantic`, `clippy::todo`, and `clippy::unwrap_used`, and the recipe +runs with `-D warnings` over all targets and features. Crates that need OS FFI +(`lanparty-gateway`, `lanparty-client-tap`, `lanparty-client-route`) set +`unsafe_code = "deny"` instead of `"forbid"`, and the single module holding +their FFI opts back in with `#![allow(unsafe_code)]`; every other crate forbids +`unsafe` outright. + These checks cover the local Rust code and the real client/relay/gateway session paths that can run without Windows TAP or LAN hardware. For the Windows client build and the manual MVP end-to-end proof, see [TESTING.md](TESTING.md). +### Build Profiles + +| Profile | Command | Purpose | +| ------------ | ----------------------- | ----------------------------------------------------------------------------------------- | +| `dev` | `just build` | Everyday development. | +| `release` | `just build-release` | Optimized but debuggable: debug info, debug assertions and overflow checks on, no LTO. | +| `production` | `just build-production` | Shipping builds: no debug info, stripped, no debug assertions, fat LTO, one codegen unit. | + +Use `release` when reproducing a bug at close-to-real speed, and `production` +for anything handed to someone else. + ## Relay ```bash diff --git a/crates/lanparty-client-core/Cargo.toml b/crates/lanparty-client-core/Cargo.toml index 679cabc..58da2d7 100644 --- a/crates/lanparty-client-core/Cargo.toml +++ b/crates/lanparty-client-core/Cargo.toml @@ -18,3 +18,11 @@ serde_json.workspace = true [dev-dependencies] rcgen.workspace = true tokio.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-client-core/src/lib.rs b/crates/lanparty-client-core/src/lib.rs index 91fac5b..7495519 100644 --- a/crates/lanparty-client-core/src/lib.rs +++ b/crates/lanparty-client-core/src/lib.rs @@ -4,6 +4,7 @@ //! crate owns the shared relay-facing state machine: connect to the relay, //! announce the client's virtual MAC, and exchange Ethernet frames as QUIC //! datagrams after the control-plane welcome. +#![cfg_attr(test, allow(clippy::unwrap_used))] use std::{ fs, @@ -57,6 +58,12 @@ pub struct ClientIdentity { } impl ClientIdentity { + /// Adopts `virtual_mac` as this client's identity on the virtual LAN. + /// + /// # Errors + /// + /// Returns an error unless `virtual_mac` is a locally administered unicast + /// address, which is what the relay requires of a client. pub fn new(virtual_mac: MacAddr) -> Result { if !virtual_mac.is_valid_client_identity() { bail!("client virtual MAC must be locally administered unicast"); @@ -65,6 +72,11 @@ impl ClientIdentity { Ok(Self { virtual_mac }) } + /// Generates a fresh random locally administered unicast identity. + /// + /// # Errors + /// + /// Returns an error if the OS random source is unavailable. pub fn generate() -> Result { let mut octets = [0_u8; 6]; getrandom::fill(&mut octets).context("failed to generate client virtual MAC")?; @@ -85,6 +97,11 @@ pub struct ClientIdentityStore { } impl ClientIdentityStore { + /// Points the store at the file that persists the client identity. + /// + /// # Errors + /// + /// Returns an error if `path` is empty. pub fn new(path: impl Into) -> Result { let path = path.into(); if path.as_os_str().is_empty() { @@ -99,6 +116,12 @@ impl ClientIdentityStore { &self.path } + /// Loads the stored identity, generating and persisting one on first run. + /// + /// # Errors + /// + /// Returns an error if the file exists but cannot be read or parsed, or if + /// a newly generated identity cannot be written. pub fn load_or_create(&self) -> Result { match fs::read(&self.path) { Ok(bytes) => read_identity(&bytes) @@ -169,6 +192,12 @@ pub struct ClientSessionConfig { } impl ClientSessionConfig { + /// Assembles everything the client needs to open a relay session. + /// + /// # Errors + /// + /// Returns an error if the server name or CA certificate is empty, or if + /// `virtual_mac` and `max_datagram_size` would not produce a valid hello. pub fn new( relay_addr: SocketAddr, server_name: impl Into, @@ -327,26 +356,61 @@ impl ClientSession { ) } + /// Sends one Ethernet frame from the TAP device to the relay. + /// + /// # Errors + /// + /// Returns an error if the frame is rejected or the datagram cannot be + /// sent; see [`ClientRelayIo::send_ethernet`]. pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> { self.relay_io().send_ethernet(frame) } + /// Sends one Ethernet frame, reporting whether it was dropped instead of + /// failing on frames the client itself filters. + /// + /// # Errors + /// + /// Returns an error only if the QUIC datagram send fails. pub fn send_ethernet_with_outcome(&self, frame: &[u8]) -> Result { self.relay_io().send_ethernet_with_outcome(frame) } + /// Waits for the next Ethernet frame the client accepts, skipping filtered + /// ones. + /// + /// # Errors + /// + /// Returns an error if the QUIC connection fails while reading. pub async fn recv_ethernet(&self) -> Result { self.relay_io().recv_ethernet().await } + /// Waits for the next Ethernet frame, also reporting filtered ones so + /// callers can account for them. + /// + /// # Errors + /// + /// Returns an error if the QUIC connection fails while reading. pub async fn recv_ethernet_outcome(&self) -> Result { self.relay_io().recv_ethernet_outcome().await } + /// Waits for the next control-plane message from the relay. + /// + /// # Errors + /// + /// Returns an error if the control stream fails or carries a frame that + /// cannot be decoded. pub async fn recv_control_event(&self) -> Result { recv_control_event(&self.connection).await } + /// Reports the current tunnel counters to the relay. + /// + /// # Errors + /// + /// Returns an error if the control stream write fails. pub async fn send_stats_snapshot(&self) -> Result<()> { self.relay_io().send_stats_snapshot().await } @@ -408,6 +472,13 @@ impl ClientRelayIo { self.virtual_mac } + /// Sends one Ethernet frame, turning a drop into an error. + /// + /// # Errors + /// + /// Returns an error if the frame is malformed, has an unusable or foreign + /// source MAC, exceeds the negotiated datagram budget, is filtered by the + /// LAN safety rules, or the datagram send fails. pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> { match self.send_ethernet_with_outcome(frame)? { ClientSendOutcome::Sent => Ok(()), @@ -423,13 +494,16 @@ impl ClientRelayIo { } } + /// Sends one Ethernet frame, returning the drop reason instead of an error + /// for frames the client filters itself. + /// + /// # Errors + /// + /// Returns an error only if the QUIC datagram send fails. pub fn send_ethernet_with_outcome(&self, frame: &[u8]) -> Result { - let ethernet_frame = match EthernetFrame::parse(frame) { - Ok(frame) => frame, - Err(_) => { - self.stats.record_malformed_frame(); - return Ok(ClientSendOutcome::Dropped(DropReason::Malformed)); - } + let Ok(ethernet_frame) = EthernetFrame::parse(frame) else { + self.stats.record_malformed_frame(); + return Ok(ClientSendOutcome::Dropped(DropReason::Malformed)); }; if !ethernet_frame.source().is_valid_unicast() { self.stats.record_dropped_frame(); @@ -476,6 +550,11 @@ impl ClientRelayIo { Ok(ClientSendOutcome::Sent) } + /// Waits for the next accepted Ethernet frame, discarding filtered ones. + /// + /// # Errors + /// + /// Returns an error if the QUIC connection fails while reading. pub async fn recv_ethernet(&self) -> Result { loop { match self.recv_ethernet_outcome().await? { @@ -485,6 +564,12 @@ impl ClientRelayIo { } } + /// Waits for the next Ethernet frame and reports whether it was accepted or + /// filtered. + /// + /// # Errors + /// + /// Returns an error if the QUIC connection fails while reading. pub async fn recv_ethernet_outcome(&self) -> Result { loop { let datagram = self.connection.read_datagram().await?; @@ -501,12 +586,9 @@ impl ClientRelayIo { self.stats.record_dropped_frame(); continue; } - let ethernet_frame = match EthernetFrame::parse(packet.payload()) { - Ok(frame) => frame, - Err(_) => { - self.stats.record_malformed_frame(); - continue; - } + let Ok(ethernet_frame) = EthernetFrame::parse(packet.payload()) else { + self.stats.record_malformed_frame(); + continue; }; self.stats.record_ethernet_rx(ethernet_frame); @@ -550,6 +632,11 @@ impl ClientRelayIo { self.stats.snapshot() } + /// Reports the current tunnel counters to the relay. + /// + /// # Errors + /// + /// Returns an error if the control stream write fails. pub async fn send_stats_snapshot(&self) -> Result<()> { let stats = self.stats.snapshot(); send_control_event(&self.connection, ControlMessage::Stats(stats)).await @@ -618,6 +705,14 @@ impl ClientTunnelStats { } } +/// Connects to the relay, announces the client hello, and returns the session +/// once the relay answers with a welcome. +/// +/// # Errors +/// +/// Returns an error if the QUIC endpoint cannot be created or connected, the +/// relay does not negotiate DATAGRAM support, the control handshake fails, or +/// the relay rejects the hello. pub async fn connect_client(config: ClientSessionConfig) -> Result { let client_config = relay_client_config(config.relay_ca_cert_der())?; let mut endpoint = Endpoint::client(client_bind_addr(config.relay_addr())) @@ -661,7 +756,7 @@ pub async fn connect_client(config: ClientSessionConfig) -> Result u16 { - usize::from(configured).min(peer).min(usize::from(u16::MAX)) as u16 + u16::try_from(peer).unwrap_or(u16::MAX).min(configured) } fn relay_client_config(relay_ca_cert_der: &[u8]) -> Result { @@ -867,6 +962,9 @@ mod tests { } #[tokio::test] + // One end-to-end handshake: a scripted relay plus the client side of the + // exchange only make sense read together. + #[allow(clippy::too_many_lines)] async fn connects_to_relay_control_stream_as_client() { let (server_config, certificate) = test_server_config(); let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap(); @@ -1179,10 +1277,10 @@ mod tests { #[test] fn snapshots_client_tunnel_stats() { let stats = ClientTunnelStats::default(); - let broadcast_tx_bytes = broadcast_ethernet_frame(b"broadcast tx"); - let broadcast_rx_bytes = broadcast_ethernet_frame(b"broadcast rx"); - let broadcast_tx = EthernetFrame::parse(&broadcast_tx_bytes).unwrap(); - let broadcast_rx = EthernetFrame::parse(&broadcast_rx_bytes).unwrap(); + let sent_frame_bytes = broadcast_ethernet_frame(b"broadcast tx"); + let received_frame_bytes = broadcast_ethernet_frame(b"broadcast rx"); + let broadcast_tx = EthernetFrame::parse(&sent_frame_bytes).unwrap(); + let broadcast_rx = EthernetFrame::parse(&received_frame_bytes).unwrap(); stats.record_ethernet_tx(broadcast_tx); stats.record_datagram_rx(); diff --git a/crates/lanparty-client-route/Cargo.toml b/crates/lanparty-client-route/Cargo.toml index b5148d0..74976d6 100644 --- a/crates/lanparty-client-route/Cargo.toml +++ b/crates/lanparty-client-route/Cargo.toml @@ -16,3 +16,13 @@ windows-sys = { "Win32_Networking_WinSock", ] } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +# `deny` rather than `forbid`: this crate needs OS FFI, so single +# modules opt back in with `#![allow(unsafe_code)]`. +unsafe_code = "deny" diff --git a/crates/lanparty-client-route/src/lib.rs b/crates/lanparty-client-route/src/lib.rs index 0ae6a19..599dcd9 100644 --- a/crates/lanparty-client-route/src/lib.rs +++ b/crates/lanparty-client-route/src/lib.rs @@ -3,6 +3,7 @@ //! The client binary uses this crate to keep Win32 route/metric calls out of //! the relay session code. The crate can snapshot the current relay route and //! install scoped route/interface overrides that are restored when dropped. +#![cfg_attr(test, allow(clippy::unwrap_used))] use std::net::IpAddr; @@ -297,6 +298,13 @@ pub use windows::{ set_scoped_interface_mtu, }; +/// Returns the route Windows would currently use to reach +/// `destination`, so the relay path can be pinned to it. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn best_route_to(_destination: IpAddr) -> Result { bail!("Windows route inspection is only available on Windows"); @@ -308,11 +316,26 @@ pub struct PinnedRelayRoute { _private: (), } +/// Installs a host route that keeps relay traffic on `route`'s +/// interface, restoring the previous state when the returned guard is +/// dropped. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn pin_relay_route(_route: &RouteSnapshot) -> Result { bail!("Windows route pinning is only available on Windows"); } +/// Resolves a Windows interface GUID string to the LUID and index the +/// IP Helper API expects. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn interface_identity_from_guid(_interface_guid: &str) -> Result { bail!("Windows interface identity lookup is only available on Windows"); @@ -324,6 +347,12 @@ pub struct ScopedInterfaceMetric { _private: (), } +/// Reads the current routing metric of an interface. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn interface_metric( _identity: NetworkInterfaceIdentity, @@ -332,6 +361,13 @@ pub fn interface_metric( bail!("Windows interface metric lookup is only available on Windows"); } +/// Overrides an interface's routing metric until the returned guard is +/// dropped. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn set_scoped_interface_metric( _identity: NetworkInterfaceIdentity, @@ -347,6 +383,13 @@ pub struct ScopedDefaultRoutes { _private: (), } +/// Enables or disables an interface's default routes until the returned +/// guard is dropped. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn set_scoped_default_routes_disabled( _identity: NetworkInterfaceIdentity, @@ -356,6 +399,12 @@ pub fn set_scoped_default_routes_disabled( bail!("Windows interface default-route updates are only available on Windows"); } +/// Reads the current MTU of an interface. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn interface_mtu( _identity: NetworkInterfaceIdentity, @@ -364,6 +413,12 @@ pub fn interface_mtu( bail!("Windows interface MTU lookup is only available on Windows"); } +/// Lists the unicast IP addresses currently assigned to an interface. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn interface_unicast_addresses( _identity: NetworkInterfaceIdentity, @@ -377,6 +432,12 @@ pub struct ScopedInterfaceMtu { _private: (), } +/// Overrides an interface's MTU until the returned guard is dropped. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; this crate only implements the +/// Windows IP Helper backend. #[cfg(not(windows))] pub fn set_scoped_interface_mtu( _identity: NetworkInterfaceIdentity, diff --git a/crates/lanparty-client-route/src/windows.rs b/crates/lanparty-client-route/src/windows.rs index a0a75ac..234292e 100644 --- a/crates/lanparty-client-route/src/windows.rs +++ b/crates/lanparty-client-route/src/windows.rs @@ -1,3 +1,9 @@ +//! Windows IP Helper (`iphlpapi`) route and interface manipulation. +//! +//! All Win32 FFI is confined to this module, which is why it opts back into +//! `unsafe`. +#![allow(unsafe_code)] + use std::{ fmt, io, @@ -58,6 +64,13 @@ use crate::{ RouteSnapshot, }; +/// Resolves a Windows interface GUID string to the LUID and index the +/// IP Helper API expects. +/// +/// # Errors +/// +/// Returns an error if `interface_guid` is not a well-formed GUID or +/// does not name an existing interface. pub fn interface_identity_from_guid(interface_guid: &str) -> Result { let guid = parse_interface_guid(interface_guid)?; let mut luid = NET_LUID_LH::default(); @@ -83,6 +96,11 @@ pub fn interface_identity_from_guid(interface_guid: &str) -> Result Result> { @@ -128,6 +156,13 @@ pub fn interface_unicast_addresses( .collect()) } +/// Overrides an interface's routing metric until the returned guard is +/// dropped. +/// +/// # Errors +/// +/// Returns an error if the previous metric cannot be read or the new +/// one cannot be applied. pub fn set_scoped_interface_metric( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, @@ -146,6 +181,12 @@ pub fn set_scoped_interface_metric( }) } +/// Enables or disables an interface's default routes until the returned +/// guard is dropped. +/// +/// # Errors +/// +/// Returns an error if the interface row cannot be read or updated. pub fn set_scoped_default_routes_disabled( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, @@ -164,6 +205,12 @@ pub fn set_scoped_default_routes_disabled( }) } +/// Overrides an interface's MTU until the returned guard is dropped. +/// +/// # Errors +/// +/// Returns an error if `mtu` is zero, or if the interface row cannot be +/// read or updated. pub fn set_scoped_interface_mtu( identity: NetworkInterfaceIdentity, family: IpInterfaceFamily, @@ -299,6 +346,12 @@ impl Drop for ScopedInterfaceMtu { } } +/// Returns the route Windows would currently use to reach +/// `destination`, so the relay path can be pinned to it. +/// +/// # Errors +/// +/// Returns an error if the Windows best-route lookup fails. pub fn best_route_to(destination: IpAddr) -> Result { let destination_sockaddr = sockaddr_from_ip(destination); let mut route = MIB_IPFORWARD_ROW2::default(); @@ -481,6 +534,13 @@ const fn address_family(family: IpInterfaceFamily) -> u16 { } } +/// Installs a host route that keeps relay traffic on `route`'s +/// interface, restoring the previous state when the returned guard is +/// dropped. +/// +/// # Errors +/// +/// Returns an error if the route cannot be installed. pub fn pin_relay_route(route: &RouteSnapshot) -> Result { let mut pinned = pinned_route_row(route); let status = unsafe { diff --git a/crates/lanparty-client-tap/Cargo.toml b/crates/lanparty-client-tap/Cargo.toml index 9d45520..cf50a21 100644 --- a/crates/lanparty-client-tap/Cargo.toml +++ b/crates/lanparty-client-tap/Cargo.toml @@ -18,3 +18,13 @@ windows-sys = { "Win32_System_Registry", ] } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +# `deny` rather than `forbid`: this crate needs OS FFI, so single +# modules opt back in with `#![allow(unsafe_code)]`. +unsafe_code = "deny" diff --git a/crates/lanparty-client-tap/src/lib.rs b/crates/lanparty-client-tap/src/lib.rs index 403e1f4..709c24c 100644 --- a/crates/lanparty-client-tap/src/lib.rs +++ b/crates/lanparty-client-tap/src/lib.rs @@ -3,6 +3,7 @@ //! This crate deliberately stays below the relay session layer. It only knows //! how to find and open an installed TAP-Windows6 Ethernet adapter; the Windows //! client binary owns when to connect it to QUIC and how to protect routes. +#![cfg_attr(test, allow(clippy::unwrap_used))] use anyhow::{Context, Result, bail}; use lanparty_proto::{EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN, MacAddr}; @@ -31,6 +32,12 @@ pub struct TapAdapterInfo { } impl TapAdapterInfo { + /// Describes an adapter that was identified outside the registry scan. + /// + /// # Errors + /// + /// Returns an error if `instance_id` is blank or `component_id` is not a + /// supported TAP-Windows6 component id. pub fn new(instance_id: impl Into, component_id: impl Into) -> Result { Self::from_parts(instance_id, component_id, None) } @@ -104,6 +111,13 @@ pub fn tap_device_path(instance_id: &str) -> String { format!("{TAP_DEVICE_PREFIX}{instance_id}{TAP_DEVICE_SUFFIX}") } +/// Checks that a buffer read from the TAP device is a well-formed, +/// non-oversized Ethernet frame. +/// +/// # Errors +/// +/// Returns an error if the buffer is too short to be an Ethernet frame or is +/// longer than [`MAX_STANDARD_ETHERNET_FRAME_LEN`]. pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> { let frame = EthernetFrame::parse(frame).context("TAP Ethernet frame is malformed")?; if frame.is_jumbo() { @@ -117,13 +131,20 @@ pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> { Ok(()) } +/// Renders a MAC address the way the TAP-Windows6 driver's `NetworkAddress` +/// registry value expects it: twelve upper-case hex digits, no separators. +/// +/// # Errors +/// +/// Returns an error if `mac` is not a locally administered unicast address, as +/// the driver rejects anything else. pub fn tap_network_address_value(mac: MacAddr) -> Result { if !mac.is_valid_client_identity() { bail!("TAP MAC {mac} is not a locally administered unicast address"); } - let [a, b, c, d, e, f] = mac.octets(); - Ok(format!("{a:02X}{b:02X}{c:02X}{d:02X}{e:02X}{f:02X}")) + let [o0, o1, o2, o3, o4, o5] = mac.octets(); + Ok(format!("{o0:02X}{o1:02X}{o2:02X}{o3:02X}{o4:02X}{o5:02X}")) } #[must_use] @@ -152,6 +173,11 @@ mod windows; #[cfg(windows)] pub use windows::{TapAdapter, available_adapters, configure_adapter_mac, open_first_adapter}; +/// Enumerates the installed TAP-Windows6 adapters. +/// +/// # Errors +/// +/// Always fails on non-Windows targets; TAP-Windows6 is a Windows driver. #[cfg(not(windows))] pub fn available_adapters() -> Result> { bail!("TAP-Windows6 adapter discovery is only available on Windows"); diff --git a/crates/lanparty-client-tap/src/windows.rs b/crates/lanparty-client-tap/src/windows.rs index 4bb7693..41f0d45 100644 --- a/crates/lanparty-client-tap/src/windows.rs +++ b/crates/lanparty-client-tap/src/windows.rs @@ -1,3 +1,9 @@ +//! TAP-Windows6 device and registry access. +//! +//! All Win32 FFI is confined to this module, which is why it opts back into +//! `unsafe`. +#![allow(unsafe_code)] + use std::{ ffi::c_void, io::{self, ErrorKind}, @@ -62,6 +68,12 @@ pub struct TapAdapter { } impl TapAdapter { + /// Opens the adapter's device file for synchronous frame I/O. + /// + /// # Errors + /// + /// Returns an error if the device cannot be opened, e.g. because the + /// adapter is gone or the process lacks the required privileges. pub fn open(info: TapAdapterInfo) -> Result { let path = info.device_path(); let wide_path = wide_null(&path); @@ -89,6 +101,12 @@ impl TapAdapter { &self.info } + /// Reports the adapter's media state to Windows, which decides whether the + /// interface counts as connected. + /// + /// # Errors + /// + /// Returns an error if the driver rejects the IOCTL. pub fn set_media_connected(&self, connected: bool) -> Result<()> { let mut status = u32::from(connected); self.device_io_control( @@ -103,6 +121,11 @@ impl TapAdapter { Ok(()) } + /// Reads the MAC address the driver currently presents. + /// + /// # Errors + /// + /// Returns an error if the driver rejects the IOCTL. pub fn driver_mac(&self) -> Result { let mut bytes = [0_u8; 6]; self.device_io_control( @@ -117,6 +140,11 @@ impl TapAdapter { Ok(MacAddr::new(bytes)) } + /// Reads the MTU the driver currently presents. + /// + /// # Errors + /// + /// Returns an error if the driver rejects the IOCTL. pub fn driver_mtu(&self) -> Result { let mut mtu = 0_u32; self.device_io_control( @@ -131,6 +159,12 @@ impl TapAdapter { Ok(mtu) } + /// Reads one raw frame into `buffer`, returning its length. + /// + /// # Errors + /// + /// Returns an error if `buffer` is larger than a Win32 read can express or + /// the device read fails. pub fn read_frame(&self, buffer: &mut [u8]) -> Result { let mut bytes_read = 0_u32; let ok = unsafe { @@ -154,6 +188,12 @@ impl TapAdapter { Ok(bytes_read as usize) } + /// Reads one frame and validates it as a standard-sized Ethernet frame. + /// + /// # Errors + /// + /// Returns an error if the read fails or the frame fails + /// [`validate_tap_ethernet_frame`]. pub fn read_ethernet_frame(&self, buffer: &mut [u8]) -> Result { let len = self.read_frame(buffer)?; validate_tap_ethernet_frame(&buffer[..len])?; @@ -161,6 +201,12 @@ impl TapAdapter { Ok(len) } + /// Writes one raw frame, returning how many bytes the driver accepted. + /// + /// # Errors + /// + /// Returns an error if `frame` is larger than a Win32 write can express or + /// the device write fails. pub fn write_frame(&self, frame: &[u8]) -> Result { let mut bytes_written = 0_u32; let ok = unsafe { @@ -181,6 +227,12 @@ impl TapAdapter { Ok(bytes_written as usize) } + /// Validates `frame` and writes it in full. + /// + /// # Errors + /// + /// Returns an error if the frame fails [`validate_tap_ethernet_frame`], the + /// write fails, or the driver accepts only part of the frame. pub fn write_ethernet_frame(&self, frame: &[u8]) -> Result<()> { validate_tap_ethernet_frame(frame)?; let written = self.write_frame(frame)?; @@ -222,6 +274,12 @@ impl TapAdapter { } } +/// Opens the first installed TAP-Windows6 adapter. +/// +/// # Errors +/// +/// Returns an error if adapter enumeration fails, no adapter is installed, or +/// the adapter cannot be opened. pub fn open_first_adapter() -> Result { let mut adapters = available_adapters()?; let info = adapters @@ -232,6 +290,13 @@ pub fn open_first_adapter() -> Result { TapAdapter::open(info) } +/// Persists `mac` as the adapter's `NetworkAddress`, which the driver picks up +/// on its next restart. +/// +/// # Errors +/// +/// Returns an error if the adapter was not discovered from the registry, `mac` +/// is not a valid client identity, or the registry write fails. pub fn configure_adapter_mac(info: &TapAdapterInfo, mac: MacAddr) -> Result<()> { let driver_key_name = info .driver_key_name() @@ -245,6 +310,12 @@ pub fn configure_adapter_mac(info: &TapAdapterInfo, mac: MacAddr) -> Result<()> Ok(()) } +/// Enumerates the installed TAP-Windows6 adapters by scanning the network class +/// registry key. +/// +/// # Errors +/// +/// Returns an error if the registry cannot be read. pub fn available_adapters() -> Result> { let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY) .context("failed to open TAP adapter registry key")?; diff --git a/crates/lanparty-client-win/Cargo.toml b/crates/lanparty-client-win/Cargo.toml index 2f2abc2..837a0c3 100644 --- a/crates/lanparty-client-win/Cargo.toml +++ b/crates/lanparty-client-win/Cargo.toml @@ -16,3 +16,11 @@ tokio.workspace = true [target."cfg(windows)".dependencies] lanparty-client-route = { path = "../lanparty-client-route" } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 1dc6bf3..fc82e4e 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(test, allow(clippy::unwrap_used))] #[cfg(any(windows, test))] use std::collections::BTreeMap; #[cfg(any(windows, test))] @@ -384,6 +385,8 @@ async fn run_client( } #[cfg(not(windows))] +// Mirrors the Windows signature so the call site needs no `cfg`. +#[allow(clippy::unused_async)] async fn run_client(_session: &ClientSession) -> Result<()> { unreachable!("ensure_supported_platform rejects non-Windows before tunnel setup") } @@ -756,24 +759,19 @@ fn client_frame_log_line( }; let source_mac = log .source_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let destination_mac = log .destination_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let ethertype_or_len = log .ethertype_or_len() - .map(|value| format!("0x{value:04x}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}")); let peer_id = log .peer_id() - .map(|peer_id| peer_id.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string()); let drop_reason = log .drop_reason() - .map(|reason| format!("{reason:?}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}")); format!( "client frame direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}", @@ -934,8 +932,7 @@ impl ControlEventFormatter { Role::Client => { let mac = peer .mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + .map_or_else(|| "unknown".to_string(), |mac| mac.to_string()); format!( "relay event: client peer {} with MAC {} left ({reason:?})", peer.peer_id(), @@ -961,8 +958,7 @@ fn format_peer_joined(peer: &PeerInfo) -> String { Role::Client => { let mac = peer .mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + .map_or_else(|| "unknown".to_string(), |mac| mac.to_string()); format!( "relay event: client peer {} joined with MAC {}", peer.peer_id(), diff --git a/crates/lanparty-ctrl/Cargo.toml b/crates/lanparty-ctrl/Cargo.toml index 1c580a4..357f441 100644 --- a/crates/lanparty-ctrl/Cargo.toml +++ b/crates/lanparty-ctrl/Cargo.toml @@ -9,3 +9,11 @@ lanparty-proto = { path = "../lanparty-proto" } serde.workspace = true serde_json.workspace = true thiserror.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-ctrl/src/codec.rs b/crates/lanparty-ctrl/src/codec.rs index db57691..b5f5054 100644 --- a/crates/lanparty-ctrl/src/codec.rs +++ b/crates/lanparty-ctrl/src/codec.rs @@ -23,24 +23,49 @@ pub enum ControlCodecError { InvalidMessage(#[from] ControlError), } +/// Encodes a control message as a length-prefixed JSON frame. +/// +/// # Errors +/// +/// Returns [`ControlCodecError::InvalidMessage`] if `message` does not pass its +/// own validation, [`ControlCodecError::Json`] if serialization fails, or +/// [`ControlCodecError::MessageTooLarge`] if the payload exceeds +/// [`MAX_CONTROL_MESSAGE_LEN`]. pub fn encode_control_message(message: &ControlMessage) -> Result, ControlCodecError> { message.validate()?; let payload = serde_json::to_vec(message)?; let payload_len = payload.len(); - if payload_len > MAX_CONTROL_MESSAGE_LEN { + // The wire prefix is a u32, and MAX_CONTROL_MESSAGE_LEN keeps it in range, + // so both conditions collapse into the same "too large" error. + let Some(prefix) = u32::try_from(payload_len) + .ok() + .filter(|_| payload_len <= MAX_CONTROL_MESSAGE_LEN) + else { return Err(ControlCodecError::MessageTooLarge { len: payload_len, max: MAX_CONTROL_MESSAGE_LEN, }); - } + }; let mut frame = Vec::with_capacity(CONTROL_LENGTH_PREFIX_LEN + payload_len); - frame.extend_from_slice(&(payload_len as u32).to_be_bytes()); + frame.extend_from_slice(&prefix.to_be_bytes()); frame.extend_from_slice(&payload); Ok(frame) } +/// Decodes exactly one control frame, which must contain one whole message and +/// nothing else. +/// +/// # Errors +/// +/// Returns [`ControlCodecError::FrameTooShort`] or +/// [`ControlCodecError::IncompletePayload`] if `frame` does not hold a complete +/// message, [`ControlCodecError::TrailingBytes`] if it holds more than one, +/// [`ControlCodecError::MessageTooLarge`] if the declared length is over the +/// limit, [`ControlCodecError::Json`] if the payload is not valid JSON, or +/// [`ControlCodecError::InvalidMessage`] if the decoded message fails +/// validation. pub fn decode_control_frame(frame: &[u8]) -> Result { let Some(total_len) = complete_control_frame_len(frame)? else { return Err(incomplete_frame_error(frame)); @@ -59,6 +84,14 @@ pub fn decode_control_frame(frame: &[u8]) -> Result Result, ControlCodecError> { if buffer.len() < CONTROL_LENGTH_PREFIX_LEN { return Ok(None); @@ -81,18 +114,14 @@ pub fn complete_control_frame_len(buffer: &[u8]) -> Result, Contro } fn declared_payload_len(buffer: &[u8]) -> Result { - if buffer.len() < CONTROL_LENGTH_PREFIX_LEN { + let Some(prefix) = buffer.first_chunk::() else { return Err(ControlCodecError::FrameTooShort { actual: buffer.len(), minimum: CONTROL_LENGTH_PREFIX_LEN, }); - } + }; - Ok(u32::from_be_bytes( - buffer[0..CONTROL_LENGTH_PREFIX_LEN] - .try_into() - .expect("length prefix slice has exact size"), - ) as usize) + Ok(u32::from_be_bytes(*prefix) as usize) } fn incomplete_frame_error(frame: &[u8]) -> ControlCodecError { @@ -174,7 +203,8 @@ mod tests { #[test] fn rejects_oversized_declared_length() { let mut frame = [0; CONTROL_LENGTH_PREFIX_LEN]; - frame.copy_from_slice(&((MAX_CONTROL_MESSAGE_LEN as u32) + 1).to_be_bytes()); + let oversized = u32::try_from(MAX_CONTROL_MESSAGE_LEN).unwrap() + 1; + frame.copy_from_slice(&oversized.to_be_bytes()); assert!(matches!( complete_control_frame_len(&frame).unwrap_err(), @@ -185,11 +215,10 @@ mod tests { #[test] fn validates_decoded_messages() { let json = format!( - r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{}}}}}"#, - MIN_USEFUL_TAP_MTU + r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}}}"# ); let mut frame = Vec::new(); - frame.extend_from_slice(&(json.len() as u32).to_be_bytes()); + frame.extend_from_slice(&u32::try_from(json.len()).unwrap().to_be_bytes()); frame.extend_from_slice(json.as_bytes()); assert!(matches!( diff --git a/crates/lanparty-ctrl/src/lib.rs b/crates/lanparty-ctrl/src/lib.rs index 7dce594..182d6f8 100644 --- a/crates/lanparty-ctrl/src/lib.rs +++ b/crates/lanparty-ctrl/src/lib.rs @@ -3,6 +3,7 @@ //! QUIC streams carry these messages as length-prefixed JSON frames. The crate //! defines the typed handshake/status model and the small framing layer needed //! by client, relay, and gateway stream handlers. +#![cfg_attr(test, allow(clippy::unwrap_used))] use std::{fmt, str::FromStr}; @@ -54,6 +55,12 @@ pub enum ControlError { pub struct RoomCode(String); impl RoomCode { + /// Validates and wraps a room code. + /// + /// # Errors + /// + /// Returns a [`ControlError`] variant describing which room-code rule the + /// value breaks (length or allowed characters). pub fn new(value: impl Into) -> Result { let value = value.into(); validate_room_code(&value)?; @@ -135,6 +142,13 @@ pub struct EndpointHello { } impl EndpointHello { + /// Builds the hello a tunnel client sends. + /// + /// # Errors + /// + /// Returns an error if `announced_mac` is not a locally administered + /// unicast address or `max_datagram_size` is too small to carry a useful + /// TAP MTU. pub fn client( room: RoomCode, announced_mac: MacAddr, @@ -152,6 +166,13 @@ impl EndpointHello { Ok(hello) } + /// Builds the hello the LAN gateway sends. A gateway announces no MAC + /// because it bridges the whole LAN rather than one identity. + /// + /// # Errors + /// + /// Returns an error if `max_datagram_size` is too small to carry a useful + /// TAP MTU. pub fn gateway(room: RoomCode, max_datagram_size: u16) -> Result { let hello = Self { protocol_version: CONTROL_PROTOCOL_VERSION, @@ -165,6 +186,14 @@ impl EndpointHello { Ok(hello) } + /// Re-checks a decoded [`EndpointHello`], which serde may have built + /// without going through the constructors. + /// + /// # Errors + /// + /// Returns an error if the protocol version is not supported, the datagram + /// size cannot carry a useful TAP MTU, or the role and announced MAC do not + /// match. pub fn validate(&self) -> Result<(), ControlError> { if self.protocol_version != CONTROL_PROTOCOL_VERSION { return Err(ControlError::UnsupportedVersion { @@ -225,6 +254,13 @@ pub struct ServerWelcome { } impl ServerWelcome { + /// Builds the welcome the relay sends once a peer is admitted to a room. + /// + /// # Errors + /// + /// Returns [`ControlError::InvalidPeerId`] if `peer_id` is 0, or + /// [`ControlError::EffectiveMtuTooSmall`] if `effective_tap_mtu` is below + /// [`MIN_USEFUL_TAP_MTU`]. pub fn new(room_id: u64, peer_id: u32, effective_tap_mtu: u16) -> Result { if peer_id == 0 { return Err(ControlError::InvalidPeerId); @@ -272,6 +308,13 @@ impl ServerWelcome { self } + /// Re-checks a decoded [`ServerWelcome`], which serde may have built + /// without going through [`ServerWelcome::new`]. + /// + /// # Errors + /// + /// Returns an error if the protocol version is not supported, a peer id is + /// 0, or the effective TAP MTU is below [`MIN_USEFUL_TAP_MTU`]. pub fn validate(&self) -> Result<(), ControlError> { if self.protocol_version != CONTROL_PROTOCOL_VERSION { return Err(ControlError::UnsupportedVersion { @@ -342,6 +385,12 @@ pub struct PeerInfo { } impl PeerInfo { + /// Describes one peer of a room. + /// + /// # Errors + /// + /// Returns an error if `peer_id` is 0, a client has no or an unusable MAC, + /// or a gateway announces one. pub fn new(peer_id: u32, role: Role, mac: Option) -> Result { if peer_id == 0 { return Err(ControlError::InvalidPeerId); @@ -364,6 +413,12 @@ impl PeerInfo { } } + /// Re-checks a decoded [`PeerInfo`], which serde may have built without + /// going through [`PeerInfo::new`]. + /// + /// # Errors + /// + /// Returns an error if the peer id is 0 or the role and MAC do not match. pub fn validate(&self) -> Result<(), ControlError> { if self.peer_id == 0 { return Err(ControlError::InvalidPeerId); @@ -463,14 +518,21 @@ pub enum ControlMessage { } impl ControlMessage { + /// Validates a decoded control message by delegating to the payload type. + /// + /// # Errors + /// + /// Returns the [`ControlError`] reported by the contained payload, or + /// [`ControlError::InvalidPeerId`] for a peer-left notice about peer 0. pub fn validate(&self) -> Result<(), ControlError> { match self { Self::Hello(hello) => hello.validate(), Self::Welcome(welcome) => welcome.validate(), - Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } => Ok(()), Self::PeerJoined(peer) => peer.validate(), Self::PeerLeft { peer_id, .. } if *peer_id == 0 => Err(ControlError::InvalidPeerId), - Self::PeerLeft { .. } => Ok(()), + Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } | Self::PeerLeft { .. } => { + Ok(()) + } } } } @@ -559,15 +621,15 @@ mod tests { #[test] fn server_welcome_rejects_reserved_peer_id_and_tiny_mtu() { assert_eq!( - ServerWelcome::new(1, 0, MIN_USEFUL_TAP_MTU as u16).unwrap_err(), + ServerWelcome::new(1, 0, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap_err(), ControlError::InvalidPeerId ); assert!(matches!( - ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(), + ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU - 1).unwrap()).unwrap_err(), ControlError::EffectiveMtuTooSmall { .. } )); assert_eq!( - ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16) + ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()) .unwrap() .with_gateway_peer_id(Some(0)) .validate() @@ -578,7 +640,7 @@ mod tests { #[test] fn server_welcome_reports_gateway_presence() { - let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16).unwrap(); + let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap(); assert_eq!(welcome.mode(), ConnectionMode::Relay); assert!(!welcome.gateway_connected()); @@ -595,7 +657,7 @@ mod tests { #[test] fn server_welcome_reports_connection_mode() { - let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16) + let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()) .unwrap() .with_mode(ConnectionMode::DirectFailedRelayFallback); @@ -619,8 +681,7 @@ mod tests { #[test] fn server_welcome_defaults_missing_mode_to_relay() { let json = format!( - r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{}}}"#, - MIN_USEFUL_TAP_MTU + r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}"# ); let welcome: ServerWelcome = serde_json::from_str(&json).unwrap(); diff --git a/crates/lanparty-gateway/Cargo.toml b/crates/lanparty-gateway/Cargo.toml index d502e94..74a147b 100644 --- a/crates/lanparty-gateway/Cargo.toml +++ b/crates/lanparty-gateway/Cargo.toml @@ -18,3 +18,13 @@ tokio.workspace = true [dev-dependencies] rcgen.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +# `deny` rather than `forbid`: this crate needs OS FFI, so single +# modules opt back in with `#![allow(unsafe_code)]`. +unsafe_code = "deny" diff --git a/crates/lanparty-gateway/src/lib.rs b/crates/lanparty-gateway/src/lib.rs index 9cb391b..118c3ba 100644 --- a/crates/lanparty-gateway/src/lib.rs +++ b/crates/lanparty-gateway/src/lib.rs @@ -1,7 +1,8 @@ //! Linux LAN gateway control-plane connection. //! -//! This crate owns the gateway binary's relay connection and Linux AF_PACKET +//! This crate owns the gateway binary's relay connection and Linux `AF_PACKET` //! bridge loop that moves Ethernet frames between the relay and wired LAN. +#![cfg_attr(test, allow(clippy::unwrap_used))] #[cfg(target_os = "linux")] mod packet; @@ -62,7 +63,7 @@ use tokio::io::unix::AsyncFd; const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN; const DISCONNECT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250); #[cfg(target_os = "linux")] -const CAM_REFRESH_INTERVAL: Duration = Duration::from_secs(60); +const CAM_REFRESH_INTERVAL: Duration = Duration::from_mins(1); #[cfg(target_os = "linux")] const GATEWAY_STATS_INTERVAL: Duration = Duration::from_secs(10); #[cfg(target_os = "linux")] @@ -102,7 +103,7 @@ pub struct GatewayArgs { #[arg(long)] room: RoomCode, - /// Wired LAN interface that will later be opened with AF_PACKET. + /// Wired LAN interface that will later be opened with `AF_PACKET`. #[arg(long, visible_alias = "iface")] interface: String, @@ -112,6 +113,13 @@ pub struct GatewayArgs { } impl GatewayArgs { + /// Resolves the parsed arguments into a validated [`GatewayConfig`], + /// reading the relay CA certificate from disk. + /// + /// # Errors + /// + /// Returns an error if the CA certificate cannot be read, the relay + /// endpoint cannot be resolved, or the resulting configuration is invalid. pub fn into_config(self) -> Result { let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| { format!( @@ -147,6 +155,12 @@ pub struct GatewayConfig { } impl GatewayConfig { + /// Assembles everything the gateway needs to bridge one LAN into one room. + /// + /// # Errors + /// + /// Returns an error if the server name, CA certificate, or interface name is + /// empty, or if `max_datagram_size` cannot carry a useful TAP MTU. pub fn new( relay_addr: SocketAddr, server_name: impl Into, @@ -270,6 +284,13 @@ impl GatewayConnection { self.quic_max_datagram_size } + /// Forwards one LAN Ethernet frame to the relay. + /// + /// # Errors + /// + /// Returns an error if the frame is malformed, filtered by the LAN safety + /// rules, exceeds the negotiated datagram budget, or the datagram send + /// fails. pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> { match send_gateway_ethernet( &self.connection, @@ -288,10 +309,21 @@ impl GatewayConnection { } } + /// Waits for the next Ethernet frame a remote client sent into the room. + /// + /// # Errors + /// + /// Returns an error if the QUIC connection fails while reading. pub async fn recv_ethernet(&self) -> Result { recv_gateway_ethernet(&self.connection, &self.welcome, &self.stats).await } + /// Waits for the next control-plane message from the relay. + /// + /// # Errors + /// + /// Returns an error if the control stream fails or carries a frame that + /// cannot be decoded. pub async fn recv_control_event(&self) -> Result { recv_gateway_control_event(&self.connection).await } @@ -301,11 +333,26 @@ impl GatewayConnection { self.stats.snapshot() } + /// Reports the current tunnel counters to the relay. + /// + /// # Errors + /// + /// Returns an error if the control stream write fails. pub async fn send_stats_snapshot(&self) -> Result<()> { send_gateway_stats(&self.connection, self.stats.snapshot()).await } + /// Runs the LAN-to-relay bridge until the connection closes or the process + /// is asked to shut down. + /// + /// # Errors + /// + /// Returns an error if the LAN socket or the relay connection fails in a + /// way the bridge cannot recover from. #[cfg(target_os = "linux")] + // The bridge is one `select!` loop over every event source; splitting arms + // into helpers would hide the shared state they all mutate. + #[allow(clippy::too_many_lines)] pub async fn bridge_until_shutdown(self, packet_socket: PacketSocket) -> Result<()> { let mut remote_clients = RemoteClientTable::new(packet_socket.interface_mac()); let mut cam_refresh_tick = tokio::time::interval_at( @@ -596,12 +643,9 @@ async fn recv_gateway_ethernet_outcome( stats.record_dropped_frame(); continue; } - let ethernet_frame = match EthernetFrame::parse(packet.payload()) { - Ok(frame) => frame, - Err(_) => { - stats.record_malformed_frame(); - continue; - } + let Ok(ethernet_frame) = EthernetFrame::parse(packet.payload()) else { + stats.record_malformed_frame(); + continue; }; stats.record_ethernet_rx(ethernet_frame); @@ -757,8 +801,7 @@ fn format_gateway_control_event(event: &ControlMessage) -> String { ControlMessage::PeerJoined(peer) if peer.role() == Role::Client => { let mac = peer .mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + .map_or_else(|| "unknown".to_string(), |mac| mac.to_string()); format!( "gateway control event: client peer {} joined with MAC {}", peer.peer_id(), @@ -796,24 +839,19 @@ fn gateway_frame_log_line( }; let source_mac = log .source_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let destination_mac = log .destination_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let ethertype_or_len = log .ethertype_or_len() - .map(|value| format!("0x{value:04x}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}")); let peer_id = log .peer_id() - .map(|peer_id| peer_id.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string()); let drop_reason = log .drop_reason() - .map(|reason| format!("{reason:?}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}")); format!( "gateway frame interface={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}", @@ -853,7 +891,7 @@ async fn read_lan_ethernet(packet_socket: &AsyncFd) -> Result return Err(error).context("failed to read LAN Ethernet frame"), - Err(_would_block) => continue, + Err(_would_block) => {} } } } @@ -870,7 +908,7 @@ async fn write_lan_ethernet(packet_socket: &AsyncFd, frame: &[u8]) Ok(Ok(sent)) if sent == frame.len() => return Ok(()), Ok(Ok(sent)) => bail!("partial LAN Ethernet frame write: {sent}/{}", frame.len()), Ok(Err(error)) => return Err(error).context("failed to write LAN Ethernet frame"), - Err(_would_block) => continue, + Err(_would_block) => {} } } } @@ -996,6 +1034,14 @@ fn cam_refresh_frame(source: MacAddr, destination: MacAddr) -> Vec { frame } +/// Connects to the relay, announces the gateway hello, and returns the +/// connection once the relay answers with a welcome. +/// +/// # Errors +/// +/// Returns an error if the QUIC endpoint cannot be created or connected, the +/// relay does not negotiate DATAGRAM support, the control handshake fails, or +/// the relay rejects the hello. pub async fn connect_gateway(config: GatewayConfig) -> Result { let client_config = relay_client_config(config.relay_ca_cert_der())?; let mut endpoint = Endpoint::client(client_bind_addr(config.relay_addr())) @@ -1009,9 +1055,9 @@ pub async fn connect_gateway(config: GatewayConfig) -> Result let peer_datagram_size = connection .max_datagram_size() .context("relay did not negotiate QUIC DATAGRAM support")?; - let hello_datagram_size = usize::from(config.max_datagram_size()) - .min(peer_datagram_size) - .min(usize::from(u16::MAX)) as u16; + let hello_datagram_size = u16::try_from(peer_datagram_size) + .unwrap_or(u16::MAX) + .min(config.max_datagram_size()); let hello = EndpointHello::gateway(config.room().clone(), hello_datagram_size) .context("failed to build gateway hello")?; let response = request_control_message(&connection, ControlMessage::Hello(hello)).await?; @@ -1167,6 +1213,9 @@ mod tests { } #[tokio::test] + // One end-to-end handshake: a scripted relay plus the gateway side of the + // exchange only make sense read together. + #[allow(clippy::too_many_lines)] async fn connects_to_relay_control_stream_as_gateway() { let (server_config, certificate) = test_server_config(); let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap(); @@ -1339,10 +1388,10 @@ mod tests { #[test] fn snapshots_gateway_broadcast_stats() { let stats = GatewayTunnelStats::default(); - let broadcast_tx_bytes = broadcast_ethernet_frame(b"broadcast tx"); - let broadcast_rx_bytes = broadcast_ethernet_frame(b"broadcast rx"); - let broadcast_tx = EthernetFrame::parse(&broadcast_tx_bytes).unwrap(); - let broadcast_rx = EthernetFrame::parse(&broadcast_rx_bytes).unwrap(); + let sent_frame_bytes = broadcast_ethernet_frame(b"broadcast tx"); + let received_frame_bytes = broadcast_ethernet_frame(b"broadcast rx"); + let broadcast_tx = EthernetFrame::parse(&sent_frame_bytes).unwrap(); + let broadcast_rx = EthernetFrame::parse(&received_frame_bytes).unwrap(); stats.record_ethernet_tx(broadcast_tx); stats.record_datagram_rx(); diff --git a/crates/lanparty-gateway/src/main.rs b/crates/lanparty-gateway/src/main.rs index 7783c3f..72736eb 100644 --- a/crates/lanparty-gateway/src/main.rs +++ b/crates/lanparty-gateway/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(test, allow(clippy::unwrap_used))] use clap::Parser; use lanparty_gateway::GatewayArgs; #[cfg(target_os = "linux")] diff --git a/crates/lanparty-gateway/src/packet.rs b/crates/lanparty-gateway/src/packet.rs index a6219e3..b8662c4 100644 --- a/crates/lanparty-gateway/src/packet.rs +++ b/crates/lanparty-gateway/src/packet.rs @@ -1,3 +1,20 @@ +//! Raw `AF_PACKET` socket plumbing for the Linux gateway. +//! +//! Everything that talks to the kernel through libc lives here, which is why +//! this is the one gateway module that opts back into `unsafe`. +#![allow(unsafe_code)] +// The libc bindings type kernel ABI fields as whatever C type the header uses +// (`c_char`, `c_int`, `c_ushort`, ...). Moving values between those and the Rust +// types used here is reinterpretation of a value that is already known to fit - +// interface indices, `ETH_P_ALL`, MAC octets, and the byte counts returned by +// send/recv after their error check - so a fallible conversion would only add +// unreachable branches. +#![allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss +)] + use std::{ ffi::CString, fs, @@ -22,6 +39,14 @@ pub struct PacketSocket { } impl PacketSocket { + /// Opens a promiscuous `AF_PACKET` socket bound to `interface`. + /// + /// # Errors + /// + /// Returns an error if the interface name is unusable or unknown, the + /// interface is wireless or has no carrier, the interface is not Ethernet, + /// or any of the socket, bind, promiscuous-membership, or MAC-lookup calls + /// fails - typically for lack of `CAP_NET_RAW`. pub fn open(interface: &str) -> io::Result { let interface_index = interface_index(interface)?; reject_wireless_interface(interface)?; @@ -58,7 +83,7 @@ impl PacketSocket { // matches that struct. fd remains owned by this function across the call. libc::bind( fd.as_raw_fd(), - (&address as *const libc::sockaddr_ll).cast::(), + (&raw const address).cast::(), std::mem::size_of::() as libc::socklen_t, ) }; @@ -91,6 +116,13 @@ impl PacketSocket { self.interface_mac } + /// Sends one Ethernet frame on the LAN, returning the number of bytes the + /// kernel accepted. + /// + /// # Errors + /// + /// Returns the OS error if the send fails, including `WouldBlock` on this + /// non-blocking socket. pub fn send_frame(&self, frame: &[u8]) -> io::Result { let sent = unsafe { // SAFETY: frame.as_ptr() is valid for frame.len() bytes for the duration of send, @@ -109,6 +141,13 @@ impl PacketSocket { Ok(sent as usize) } + /// Receives one inbound Ethernet frame into `buffer`, skipping the copies + /// of our own outgoing frames that `AF_PACKET` also delivers. + /// + /// # Errors + /// + /// Returns the OS error if the receive fails, including `WouldBlock` on this + /// non-blocking socket. pub fn recv_frame(&self, buffer: &mut [u8]) -> io::Result { loop { let mut address = unsafe { @@ -126,8 +165,8 @@ impl PacketSocket { buffer.as_mut_ptr().cast::(), buffer.len(), 0, - (&mut address as *mut libc::sockaddr_ll).cast::(), - &mut address_len, + (&raw mut address).cast::(), + &raw mut address_len, ) }; if received < 0 { @@ -146,6 +185,12 @@ impl AsRawFd for PacketSocket { } } +/// Resolves a network interface name to its kernel index. +/// +/// # Errors +/// +/// Returns an error if the name is empty, contains a path separator or NUL, is +/// longer than `IFNAMSIZ`, or does not name an existing interface. pub fn interface_index(interface: &str) -> io::Result { let name = interface_name(interface)?; @@ -170,7 +215,7 @@ fn enable_promiscuous_membership(fd: RawFd, interface_index: u32) -> io::Result< fd, libc::SOL_PACKET, libc::PACKET_ADD_MEMBERSHIP, - (&membership as *const libc::packet_mreq).cast::(), + (&raw const membership).cast::(), std::mem::size_of::() as libc::socklen_t, ) }; diff --git a/crates/lanparty-net/Cargo.toml b/crates/lanparty-net/Cargo.toml index 150e748..078ffd5 100644 --- a/crates/lanparty-net/Cargo.toml +++ b/crates/lanparty-net/Cargo.toml @@ -5,3 +5,11 @@ edition.workspace = true [dependencies] thiserror.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-net/src/lib.rs b/crates/lanparty-net/src/lib.rs index a469b1d..cfaabb2 100644 --- a/crates/lanparty-net/src/lib.rs +++ b/crates/lanparty-net/src/lib.rs @@ -1,4 +1,5 @@ //! Shared network endpoint parsing for LAN party tunnel binaries. +#![cfg_attr(test, allow(clippy::unwrap_used))] use std::{ fmt, @@ -17,6 +18,12 @@ pub struct RelayEndpoint { } impl RelayEndpoint { + /// Builds an endpoint from an already split host and port. + /// + /// # Errors + /// + /// Returns [`RelayEndpointError::EmptyHost`] if `host` is empty or only + /// whitespace, or [`RelayEndpointError::InvalidPort`] if `port` is zero. pub fn new(host: impl Into, port: u16) -> Result { let host = host.into(); let host = host.trim(); @@ -45,6 +52,14 @@ impl RelayEndpoint { self.port } + /// Resolves the endpoint to its first socket address, performing a DNS + /// lookup when the host is not an IP literal. + /// + /// # Errors + /// + /// Returns [`RelayEndpointError::ResolveFailed`] if resolution itself + /// fails, or [`RelayEndpointError::NoResolvedAddress`] if it succeeds but + /// yields no address. pub fn resolve(&self) -> Result { let mut addrs = (self.host.as_str(), self.port) .to_socket_addrs() diff --git a/crates/lanparty-obs/Cargo.toml b/crates/lanparty-obs/Cargo.toml index 3af2440..156df97 100644 --- a/crates/lanparty-obs/Cargo.toml +++ b/crates/lanparty-obs/Cargo.toml @@ -9,3 +9,11 @@ serde.workspace = true [dev-dependencies] serde_json.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-obs/src/lib.rs b/crates/lanparty-obs/src/lib.rs index 168d540..b5f3a87 100644 --- a/crates/lanparty-obs/src/lib.rs +++ b/crates/lanparty-obs/src/lib.rs @@ -2,6 +2,7 @@ //! //! Runtime crates can convert these values into `tracing` fields, JSON logs, or //! user-facing status lines without each component inventing its own vocabulary. +#![cfg_attr(test, allow(clippy::unwrap_used))] use std::net::IpAddr; diff --git a/crates/lanparty-proto/Cargo.toml b/crates/lanparty-proto/Cargo.toml index a6f92c5..ec2e3d3 100644 --- a/crates/lanparty-proto/Cargo.toml +++ b/crates/lanparty-proto/Cargo.toml @@ -6,3 +6,11 @@ edition.workspace = true [dependencies] serde.workspace = true thiserror.workspace = true + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-proto/src/ethernet.rs b/crates/lanparty-proto/src/ethernet.rs index 077c336..bfac505 100644 --- a/crates/lanparty-proto/src/ethernet.rs +++ b/crates/lanparty-proto/src/ethernet.rs @@ -12,6 +12,13 @@ pub struct EthernetFrame<'a> { } impl<'a> EthernetFrame<'a> { + /// Borrows `bytes` as an Ethernet frame after checking that it is long + /// enough to contain a complete Ethernet header. + /// + /// # Errors + /// + /// Returns [`ProtoError::EthernetFrameTooShort`] if `bytes` is shorter than + /// [`MIN_ETHERNET_FRAME_LEN`]. pub fn parse(bytes: &'a [u8]) -> Result { if bytes.len() < MIN_ETHERNET_FRAME_LEN { return Err(ProtoError::EthernetFrameTooShort { diff --git a/crates/lanparty-proto/src/lib.rs b/crates/lanparty-proto/src/lib.rs index 4d8320d..cee82f6 100644 --- a/crates/lanparty-proto/src/lib.rs +++ b/crates/lanparty-proto/src/lib.rs @@ -3,6 +3,7 @@ //! This crate intentionally contains no socket, TAP, QUIC, or OS-specific //! behavior. It is the small contract that the Windows client, Linux gateway, //! and relay must all agree on. +#![cfg_attr(test, allow(clippy::unwrap_used))] mod ethernet; mod mac; diff --git a/crates/lanparty-proto/src/mac.rs b/crates/lanparty-proto/src/mac.rs index 31f6228..fe3334f 100644 --- a/crates/lanparty-proto/src/mac.rs +++ b/crates/lanparty-proto/src/mac.rs @@ -91,8 +91,8 @@ impl From for [u8; 6] { impl fmt::Display for MacAddr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let [a, b, c, d, e, g] = self.0; - write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}") + let [o0, o1, o2, o3, o4, o5] = self.0; + write!(f, "{o0:02x}:{o1:02x}:{o2:02x}:{o3:02x}:{o4:02x}:{o5:02x}") } } diff --git a/crates/lanparty-proto/src/mtu.rs b/crates/lanparty-proto/src/mtu.rs index 65fe15c..faada2f 100644 --- a/crates/lanparty-proto/src/mtu.rs +++ b/crates/lanparty-proto/src/mtu.rs @@ -17,6 +17,14 @@ pub enum MtuError { }, } +/// Returns the largest TAP MTU that still fits into the negotiated QUIC +/// datagram budget, after subtracting overlay and Ethernet framing overhead +/// plus `safety_margin`. +/// +/// # Errors +/// +/// Returns [`MtuError::DatagramTooSmall`] if the budget cannot even carry +/// [`MIN_USEFUL_TAP_MTU`] bytes of payload on top of that overhead. pub fn max_tap_mtu_for_datagram( quic_max_datagram_size: usize, safety_margin: usize, @@ -34,6 +42,13 @@ pub fn max_tap_mtu_for_datagram( Ok(quic_max_datagram_size - overhead) } +/// Returns the TAP MTU to configure: [`DEFAULT_TAP_MTU`], clamped down when the +/// negotiated QUIC datagram budget cannot carry it. +/// +/// # Errors +/// +/// Returns [`MtuError::DatagramTooSmall`] if the budget is too small to be +/// useful at all; see [`max_tap_mtu_for_datagram`]. pub fn recommended_tap_mtu(quic_max_datagram_size: usize) -> Result { let max = max_tap_mtu_for_datagram(quic_max_datagram_size, DEFAULT_DATAGRAM_SAFETY_MARGIN)?; diff --git a/crates/lanparty-proto/src/overlay.rs b/crates/lanparty-proto/src/overlay.rs index d70dbb9..efc0d60 100644 --- a/crates/lanparty-proto/src/overlay.rs +++ b/crates/lanparty-proto/src/overlay.rs @@ -19,6 +19,12 @@ impl FrameType { self as u8 } + /// Decodes the on-wire frame type discriminant. + /// + /// # Errors + /// + /// Returns [`ProtoError::UnknownFrameType`] for a discriminant this + /// protocol version does not define. pub const fn from_u8(value: u8) -> Result { match value { 1 => Ok(Self::Ethernet), @@ -39,6 +45,13 @@ pub struct OverlayHeader { } impl OverlayHeader { + /// Builds a header for a payload of `payload_len` bytes. + /// + /// # Errors + /// + /// Returns [`ProtoError::UnsupportedFlags`] if `flags` sets any bit that is + /// still reserved, or [`ProtoError::PayloadTooLarge`] if `payload_len` does + /// not fit into the 16-bit wire field. pub fn new( frame_type: FrameType, room_id: u64, @@ -99,35 +112,49 @@ impl OverlayHeader { bytes } + /// Decodes the fixed-size header prefix of a received datagram. + /// + /// # Errors + /// + /// Returns [`ProtoError::DatagramTooShort`] if `bytes` is shorter than + /// [`OVERLAY_HEADER_LEN`], [`ProtoError::BadMagic`] or + /// [`ProtoError::UnsupportedVersion`] if the datagram is not a datagram of + /// this protocol version, [`ProtoError::UnsupportedFlags`] if a reserved + /// flag bit is set, or [`ProtoError::UnknownFrameType`] for an unknown + /// frame type. pub fn decode(bytes: &[u8]) -> Result { - if bytes.len() < OVERLAY_HEADER_LEN { + // Taking the header as a fixed-size array up front makes every field + // read below a statically bounds-checked array index, so this function + // has no panicking path. + let Some(header) = bytes.first_chunk::() else { return Err(ProtoError::DatagramTooShort { actual: bytes.len(), minimum: OVERLAY_HEADER_LEN, }); - } + }; - let magic = u32::from_be_bytes(bytes[0..4].try_into().expect("header magic slice length")); + let magic = u32::from_be_bytes([header[0], header[1], header[2], header[3]]); if magic != OVERLAY_MAGIC { return Err(ProtoError::BadMagic { actual: magic }); } - let version = bytes[4]; + let version = header[4]; if version != OVERLAY_VERSION { return Err(ProtoError::UnsupportedVersion { actual: version }); } - let flags = u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length")); + let flags = u16::from_be_bytes([header[18], header[19]]); validate_overlay_flags(flags)?; Ok(Self { - frame_type: FrameType::from_u8(bytes[5])?, - room_id: u64::from_be_bytes(bytes[6..14].try_into().expect("room id slice length")), - peer_id: u32::from_be_bytes(bytes[14..18].try_into().expect("peer id slice length")), + frame_type: FrameType::from_u8(header[5])?, + room_id: u64::from_be_bytes([ + header[6], header[7], header[8], header[9], header[10], header[11], header[12], + header[13], + ]), + peer_id: u32::from_be_bytes([header[14], header[15], header[16], header[17]]), flags, - payload_len: u16::from_be_bytes( - bytes[20..22].try_into().expect("payload len slice length"), - ), + payload_len: u16::from_be_bytes([header[20], header[21]]), }) } } @@ -139,6 +166,12 @@ pub struct OverlayPacket<'a> { } impl<'a> OverlayPacket<'a> { + /// Pairs a decoded header with its payload. + /// + /// # Errors + /// + /// Returns [`ProtoError::PayloadLengthMismatch`] if `payload` is not + /// exactly as long as the header declares. pub fn new(header: OverlayHeader, payload: &'a [u8]) -> Result { let declared = usize::from(header.payload_len); @@ -193,6 +226,11 @@ fn validate_overlay_flags(flags: u16) -> Result<(), ProtoError> { Ok(()) } +/// Encodes a complete overlay datagram: header followed by `payload`. +/// +/// # Errors +/// +/// Propagates every error of [`OverlayHeader::new`]. pub fn encode_datagram( frame_type: FrameType, room_id: u64, @@ -207,6 +245,13 @@ pub fn encode_datagram( Ok(datagram) } +/// Checks an encoded datagram against the QUIC datagram size the peers +/// negotiated. +/// +/// # Errors +/// +/// Returns [`ProtoError::DatagramExceedsBudget`] if `datagram_len` exceeds +/// `max_datagram_size`. pub fn validate_datagram_budget( datagram_len: usize, max_datagram_size: usize, @@ -221,6 +266,12 @@ pub fn validate_datagram_budget( Ok(()) } +/// Decodes a received overlay datagram, borrowing its payload from `bytes`. +/// +/// # Errors +/// +/// Propagates every error of [`OverlayHeader::decode`] and +/// [`OverlayPacket::new`]. pub fn decode_datagram(bytes: &[u8]) -> Result, ProtoError> { let header = OverlayHeader::decode(bytes)?; let payload = &bytes[OVERLAY_HEADER_LEN..]; diff --git a/crates/lanparty-proto/src/safety.rs b/crates/lanparty-proto/src/safety.rs index 1b401fa..303fffb 100644 --- a/crates/lanparty-proto/src/safety.rs +++ b/crates/lanparty-proto/src/safety.rs @@ -14,7 +14,6 @@ const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0; const IPV6_NEXT_HEADER_ROUTING: u8 = 43; const IPV6_NEXT_HEADER_FRAGMENT: u8 = 44; const IPV6_NEXT_HEADER_AH: u8 = 51; -const IPV6_NEXT_HEADER_NO_NEXT: u8 = 59; const IPV6_NEXT_HEADER_DESTINATION_OPTIONS: u8 = 60; const IPV6_NEXT_HEADER_ICMPV6: u8 = 58; const DHCPV4_SERVER_PORT: u16 = 67; @@ -34,6 +33,9 @@ pub enum EthernetSafetyDrop { Ipv6Fragment, } +/// Returns why a frame coming from the physical LAN must not be forwarded into +/// the tunnel, or `None` if it may be forwarded. +#[must_use] pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option { if !frame.source().is_valid_unicast() { return Some(EthernetSafetyDrop::InvalidSourceMac); @@ -42,6 +44,12 @@ pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option) -> Option { if let Some(drop_reason) = common_safety_drop_reason(frame) { return Some(drop_reason); @@ -93,9 +101,9 @@ fn is_vlan_tagged_frame(frame: EthernetFrame<'_>) -> bool { } fn is_link_local_control_destination(mac: MacAddr) -> bool { - let [a, b, c, d, e, f] = mac.octets(); + let [prefix @ .., last] = mac.octets(); - [a, b, c, d, e] == [0x01, 0x80, 0xc2, 0x00, 0x00] && f <= 0x0f + prefix == [0x01, 0x80, 0xc2, 0x00, 0x00] && last <= 0x0f } fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool { @@ -190,7 +198,6 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt loop { match next_header { next_header if next_header == expected_next_header => return Some(offset), - IPV6_NEXT_HEADER_NO_NEXT => return None, IPV6_NEXT_HEADER_HOP_BY_HOP | IPV6_NEXT_HEADER_ROUTING | IPV6_NEXT_HEADER_DESTINATION_OPTIONS => { @@ -216,6 +223,9 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt return None; } } + // Any other next-header value is either an upper-layer protocol we + // were not asked about or the "no next header" terminator (59), so + // the header we are looking for is not in this packet. _ => return None, } } diff --git a/crates/lanparty-relay/Cargo.toml b/crates/lanparty-relay/Cargo.toml index 5624896..8cdcd8b 100644 --- a/crates/lanparty-relay/Cargo.toml +++ b/crates/lanparty-relay/Cargo.toml @@ -20,3 +20,11 @@ tokio.workspace = true [dev-dependencies] lanparty-client-core = { path = "../lanparty-client-core" } lanparty-gateway = { path = "../lanparty-gateway" } + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" + +[lints.rust] +unsafe_code = "forbid" diff --git a/crates/lanparty-relay/src/config.rs b/crates/lanparty-relay/src/config.rs index db92ee5..1d822a4 100644 --- a/crates/lanparty-relay/src/config.rs +++ b/crates/lanparty-relay/src/config.rs @@ -29,6 +29,12 @@ pub struct RelayArgs { } impl RelayArgs { + /// Turns the parsed arguments into a validated [`RelayConfig`]. + /// + /// # Errors + /// + /// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if the per-room client + /// limit is zero. pub fn into_config(self) -> Result { RelayConfig::with_dev_cert_der_out( self.listen, @@ -46,10 +52,24 @@ pub struct RelayConfig { } impl RelayConfig { + /// Builds a relay configuration that does not export its development + /// certificate. + /// + /// # Errors + /// + /// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if + /// `max_clients_per_room` is zero. pub fn new(listen: ListenEndpoint, max_clients_per_room: usize) -> Result { Self::with_dev_cert_der_out(listen, max_clients_per_room, None) } + /// Builds a relay configuration that also writes its generated development + /// certificate to `dev_cert_der_out`, so clients can trust it. + /// + /// # Errors + /// + /// Returns [`ConfigError::ZeroMaxClientsPerRoom`] if + /// `max_clients_per_room` is zero. pub fn with_dev_cert_der_out( listen: ListenEndpoint, max_clients_per_room: usize, diff --git a/crates/lanparty-relay/src/lib.rs b/crates/lanparty-relay/src/lib.rs index 511fced..45fd02d 100644 --- a/crates/lanparty-relay/src/lib.rs +++ b/crates/lanparty-relay/src/lib.rs @@ -3,6 +3,7 @@ //! The QUIC server loop admits peers through this room registry, while the //! registry itself stays socket-free so the relay invariants remain directly //! testable. +#![cfg_attr(test, allow(clippy::unwrap_used))] mod config; mod server; @@ -233,6 +234,13 @@ impl Default for RoomRegistry { } impl RoomRegistry { + /// Creates an empty registry that admits at most `max_clients_per_room` + /// clients per room. + /// + /// # Panics + /// + /// Panics if `max_clients_per_room` is zero, which would make every join + /// impossible. #[must_use] pub fn new(max_clients_per_room: usize) -> Self { assert!( @@ -247,12 +255,24 @@ impl RoomRegistry { } } - pub fn join(&mut self, hello: EndpointHello) -> Result { - hello.validate().map_err(reject_control_error)?; + /// Admits an endpoint to the room named in its hello, creating the room if + /// this is its first member. + /// + /// # Errors + /// + /// Returns a [`Reject`] if the hello is invalid, its datagram budget cannot + /// carry a useful TAP MTU, the room is full, the announced MAC is already + /// taken, the room already has a gateway, or the room-id space is + /// exhausted. + pub fn join(&mut self, hello: &EndpointHello) -> Result { + hello + .validate() + .map_err(|error| reject_control_error(&error))?; + // recommended_tap_mtu is bounded by DEFAULT_TAP_MTU, so it always fits. let supported_tap_mtu = recommended_tap_mtu(usize::from(hello.max_datagram_size())) - .map_err(|error| Reject::new(RejectReason::MtuTooSmall, error.to_string()))? - as u16; + .map(|mtu| u16::try_from(mtu).unwrap_or(u16::MAX)) + .map_err(|error| Reject::new(RejectReason::MtuTooSmall, error.to_string()))?; let room_code = hello.room().clone(); if !self.rooms.contains_key(&room_code) { @@ -263,10 +283,14 @@ impl RoomRegistry { ); } - self.rooms - .get_mut(&room_code) - .expect("room was inserted before lookup") - .join(hello, supported_tap_mtu) + let Some(room) = self.rooms.get_mut(&room_code) else { + return Err(Reject::new( + RejectReason::InternalError, + "room disappeared between creation and lookup", + )); + }; + + room.join(hello, supported_tap_mtu) } #[must_use] @@ -279,6 +303,12 @@ impl RoomRegistry { self.rooms.get(room).map(Room::snapshot) } + /// Removes a peer from a room, deleting the room once it is empty. + /// + /// # Errors + /// + /// Returns [`ForwardingError::UnknownRoom`] or + /// [`ForwardingError::UnknownPeer`] if the room or peer is not present. pub fn leave(&mut self, room: &RoomCode, peer_id: u32) -> Result { let room_state = self .rooms @@ -294,6 +324,14 @@ impl RoomRegistry { Ok(LeaveResult::new(peer, room_removed)) } + /// Decides where one Ethernet frame from `ingress_peer_id` should go, and + /// accounts it against that peer's rate limits. + /// + /// # Errors + /// + /// Returns [`ForwardingError::UnknownRoom`] or + /// [`ForwardingError::UnknownPeer`] if the room or the ingress peer is not + /// present. pub fn forward_ethernet( &mut self, room: &RoomCode, @@ -329,7 +367,7 @@ impl RoomRegistry { #[derive(Debug, Clone)] struct Room { - room_id: u64, + id: u64, next_peer_id: u32, max_clients: usize, effective_tap_mtu: Option, @@ -354,9 +392,9 @@ impl PeerEntry { } impl Room { - fn new(room_id: u64, max_clients: usize) -> Self { + fn new(id: u64, max_clients: usize) -> Self { Self { - room_id, + id, next_peer_id: 1, max_clients, effective_tap_mtu: None, @@ -371,10 +409,10 @@ impl Room { fn join( &mut self, - hello: EndpointHello, + hello: &EndpointHello, supported_tap_mtu: u16, ) -> Result { - self.validate_role_capacity(&hello)?; + self.validate_role_capacity(hello)?; let effective_tap_mtu = self.accept_effective_mtu(supported_tap_mtu)?; let peer_id = self.allocate_peer_id()?; @@ -389,7 +427,7 @@ impl Room { Role::Gateway => Some(peer.peer_id()), Role::Client => self.gateway.as_ref().map(|gateway| gateway.info.peer_id()), }; - let welcome = ServerWelcome::new(self.room_id, peer_id, effective_tap_mtu) + let welcome = ServerWelcome::new(self.id, peer_id, effective_tap_mtu) .map(|welcome| welcome.with_gateway_peer_id(gateway_peer_id)) .map_err(|error| { Reject::new( @@ -492,7 +530,7 @@ impl Room { ); RoomSnapshot { - room_id: self.room_id, + room_id: self.id, effective_tap_mtu: self.effective_tap_mtu.unwrap_or_default(), gateway: self.gateway.as_ref().map(|gateway| gateway.info.clone()), clients, @@ -545,9 +583,8 @@ impl Room { })?; let ingress_role = ingress.role(); let ingress_mac = ingress.mac(); - let frame = match EthernetFrame::parse(frame_bytes) { - Ok(frame) => frame, - Err(_) => return Ok(ForwardingDecision::dropped(DropReason::Malformed)), + let Ok(frame) = EthernetFrame::parse(frame_bytes) else { + return Ok(ForwardingDecision::dropped(DropReason::Malformed)); }; if !frame.source().is_valid_unicast() { @@ -758,7 +795,7 @@ fn client_total_bandwidth_limit() -> TokenBucket { ) } -fn reject_control_error(error: ControlError) -> Reject { +fn reject_control_error(error: &ControlError) -> Reject { let reason = match error { ControlError::UnsupportedVersion { .. } => RejectReason::UnsupportedVersion, ControlError::InvalidClientMac { .. } => RejectReason::InvalidMac, @@ -918,8 +955,8 @@ mod tests { fn accepts_gateway_and_client_into_room() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let snapshot = registry.snapshot(&room()).unwrap(); assert_eq!(registry.room_count(), 1); @@ -942,7 +979,7 @@ mod tests { fn reports_missing_gateway_to_client_joining_first() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); assert!(!client.welcome().gateway_connected()); assert_eq!(client.welcome().gateway_peer_id(), None); @@ -951,9 +988,9 @@ mod tests { #[test] fn rejects_second_gateway() { let mut registry = RoomRegistry::default(); - registry.join(gateway_hello()).unwrap(); + registry.join(&gateway_hello()).unwrap(); - let reject = registry.join(gateway_hello()).unwrap_err(); + let reject = registry.join(&gateway_hello()).unwrap_err(); assert_eq!(reject.reason(), &RejectReason::GatewayAlreadyConnected); } @@ -961,9 +998,9 @@ mod tests { #[test] fn rejects_duplicate_client_mac() { let mut registry = RoomRegistry::default(); - registry.join(client_hello(1)).unwrap(); + registry.join(&client_hello(1)).unwrap(); - let reject = registry.join(client_hello(1)).unwrap_err(); + let reject = registry.join(&client_hello(1)).unwrap_err(); assert_eq!(reject.reason(), &RejectReason::DuplicateMac); } @@ -971,9 +1008,9 @@ mod tests { #[test] fn enforces_client_limit() { let mut registry = RoomRegistry::new(1); - registry.join(client_hello(1)).unwrap(); + registry.join(&client_hello(1)).unwrap(); - let reject = registry.join(client_hello(2)).unwrap_err(); + let reject = registry.join(&client_hello(2)).unwrap_err(); assert_eq!(reject.reason(), &RejectReason::RoomFull); } @@ -982,10 +1019,10 @@ mod tests { fn keeps_room_mtu_stable_after_first_peer() { let mut registry = RoomRegistry::default(); let first = EndpointHello::client(room(), mac(1), 1024).unwrap(); - registry.join(first).unwrap(); + registry.join(&first).unwrap(); let reject = registry - .join(EndpointHello::gateway(room(), 900).unwrap()) + .join(&EndpointHello::gateway(room(), 900).unwrap()) .unwrap_err(); assert_eq!(reject.reason(), &RejectReason::MtuTooSmall); @@ -996,9 +1033,9 @@ mod tests { fn second_peer_uses_existing_lower_room_mtu() { let mut registry = RoomRegistry::default(); let first = EndpointHello::client(room(), mac(1), 1024).unwrap(); - registry.join(first).unwrap(); + registry.join(&first).unwrap(); - let gateway = registry.join(gateway_hello()).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); assert_eq!(gateway.welcome().effective_tap_mtu(), 972); } @@ -1006,8 +1043,8 @@ mod tests { #[test] fn removes_client_from_room_indexes() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); - registry.join(client_hello(2)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); + registry.join(&client_hello(2)).unwrap(); let result = registry.leave(&room(), client.peer().peer_id()).unwrap(); let snapshot = registry.snapshot(&room()).unwrap(); @@ -1016,13 +1053,13 @@ mod tests { assert!(!result.room_removed()); assert_eq!(snapshot.clients().len(), 1); assert_eq!(snapshot.last_seen(client.peer().peer_id()), None); - assert!(registry.join(client_hello(1)).is_ok()); + assert!(registry.join(&client_hello(1)).is_ok()); } #[test] fn removes_empty_room_after_last_peer_leaves() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let result = registry.leave(&room(), client.peer().peer_id()).unwrap(); @@ -1035,8 +1072,8 @@ mod tests { #[test] fn removes_gateway_without_removing_room_when_clients_remain() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + registry.join(&client_hello(1)).unwrap(); let result = registry.leave(&room(), gateway.peer().peer_id()).unwrap(); let snapshot = registry.snapshot(&room()).unwrap(); @@ -1046,13 +1083,13 @@ mod tests { assert!(snapshot.gateway().is_none()); assert_eq!(snapshot.last_seen(gateway.peer().peer_id()), None); assert_eq!(snapshot.clients().len(), 1); - assert!(registry.join(gateway_hello()).is_ok()); + assert!(registry.join(&gateway_hello()).is_ok()); } #[test] fn reports_unknown_peer_on_leave() { let mut registry = RoomRegistry::default(); - registry.join(client_hello(1)).unwrap(); + registry.join(&client_hello(1)).unwrap(); let error = registry.leave(&room(), 99).unwrap_err(); @@ -1068,9 +1105,9 @@ mod tests { #[test] fn forwards_unknown_client_unicast_to_gateway() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let frame = ethernet(MacAddr::new([0x00, 1, 2, 3, 4, 5]), mac(1)); let decision = registry @@ -1086,9 +1123,9 @@ mod tests { #[test] fn drops_gateway_unicast_to_unknown_remote_mac() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - registry.join(client_hello(1)).unwrap(); - registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + registry.join(&client_hello(1)).unwrap(); + registry.join(&client_hello(2)).unwrap(); let frame = ethernet(physical_mac(), MacAddr::new([0x00, 1, 2, 3, 4, 5])); let decision = registry @@ -1101,9 +1138,9 @@ mod tests { #[test] fn forwards_gateway_unicast_to_matching_client() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let frame = ethernet(mac(2), MacAddr::new([0x00, 1, 2, 3, 4, 5])); let decision = registry @@ -1118,9 +1155,9 @@ mod tests { #[test] fn floods_broadcast_without_reflecting_ingress() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let frame = ethernet(MacAddr::BROADCAST, mac(1)); let decision = registry @@ -1137,8 +1174,8 @@ mod tests { #[test] fn refreshes_peer_last_seen_after_valid_frames() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let client_seen_at = Instant::now() + Duration::from_secs(5); let gateway_seen_at = client_seen_at + Duration::from_secs(1); let client_frame = ethernet(MacAddr::BROADCAST, mac(1)); @@ -1175,7 +1212,7 @@ mod tests { #[test] fn keeps_last_seen_unchanged_for_unauthorized_client_source() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let before = registry .snapshot(&room()) .unwrap() @@ -1205,9 +1242,9 @@ mod tests { #[test] fn rate_limits_client_broadcast_after_burst() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let frame = ethernet(MacAddr::BROADCAST, mac(1)); let now = Instant::now(); @@ -1241,9 +1278,9 @@ mod tests { #[test] fn rate_limits_client_unknown_unicast_after_burst() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let unknown_unicast = ethernet(physical_mac(), mac(1)); let known_unicast = ethernet(mac(2), mac(1)); let now = Instant::now(); @@ -1281,8 +1318,8 @@ mod tests { #[test] fn rate_limits_client_total_bandwidth_after_burst() { let mut registry = RoomRegistry::default(); - let client_one = registry.join(client_hello(1)).unwrap(); - let client_two = registry.join(client_hello(2)).unwrap(); + let client_one = registry.join(&client_hello(1)).unwrap(); + let client_two = registry.join(&client_hello(2)).unwrap(); let payload = vec![0; usize::from(client_one.welcome().effective_tap_mtu())]; let frame = ethernet_with_payload(mac(2), mac(1), ETHERTYPE_IPV4, &payload); let frame_len = frame.len() as u64; @@ -1329,7 +1366,7 @@ mod tests { #[test] fn filters_client_frames_with_forged_source_mac() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let frame = ethernet(MacAddr::BROADCAST, mac(2)); let decision = registry @@ -1342,8 +1379,8 @@ mod tests { #[test] fn filters_invalid_source_macs_from_clients_and_gateway() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let client_frame = ethernet(MacAddr::BROADCAST, MacAddr::BROADCAST); let gateway_frame = ethernet(mac(1), MacAddr::ZERO); @@ -1361,8 +1398,8 @@ mod tests { #[test] fn filters_jumbo_frames() { let mut registry = RoomRegistry::default(); - registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let mut frame = ethernet(MacAddr::BROADCAST, mac(1)); frame.resize(MAX_STANDARD_ETHERNET_FRAME_LEN + 1, 0); @@ -1376,8 +1413,8 @@ mod tests { #[test] fn drops_frames_above_effective_tap_mtu() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let oversized_payload = vec![0; usize::from(client.welcome().effective_tap_mtu()) + 1]; let client_frame = ethernet_with_payload( MacAddr::BROADCAST, @@ -1402,8 +1439,8 @@ mod tests { #[test] fn filters_l2_control_plane_frames_from_clients_and_gateway() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let stp_destination = MacAddr::new([0x01, 0x80, 0xc2, 0, 0, 0]); let client_frame = ethernet_with_payload(stp_destination, mac(1), 0x0026, &[]); let gateway_frame = @@ -1423,8 +1460,8 @@ mod tests { #[test] fn filters_remote_vlan_tagged_frames_but_allows_lan_tags() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let payload = [0, 42, 0x08, 0x00, 1, 2, 3, 4]; let client_frame = ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_8021Q, &payload); @@ -1450,8 +1487,8 @@ mod tests { #[test] fn filters_remote_dhcp_server_replies_but_allows_lan_replies() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let payload = ipv4_udp_payload(DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT); let client_frame = ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_IPV4, &payload); @@ -1473,8 +1510,8 @@ mod tests { #[test] fn filters_remote_dhcpv6_server_replies_but_allows_lan_replies() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 1, 0, 2]); let payload = ipv6_udp_after_destination_options_payload(DHCPV6_SERVER_PORT, DHCPV6_CLIENT_PORT); @@ -1497,8 +1534,8 @@ mod tests { #[test] fn allows_remote_dhcpv4_client_requests() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let payload = ipv4_udp_payload(DHCPV4_CLIENT_PORT, DHCPV4_SERVER_PORT); let frame = ethernet_with_payload(MacAddr::BROADCAST, mac(1), ETHERTYPE_IPV4, &payload); @@ -1513,8 +1550,8 @@ mod tests { #[test] fn allows_remote_dhcpv6_client_requests() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 1, 0, 2]); let payload = ipv6_udp_payload(DHCPV6_CLIENT_PORT, DHCPV6_SERVER_PORT); let frame = ethernet_with_payload(destination, mac(1), ETHERTYPE_IPV6, &payload); @@ -1530,8 +1567,8 @@ mod tests { #[test] fn filters_remote_ipv6_fragments_but_allows_lan_fragments() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); let payload = ipv6_payload( IPV6_NEXT_HEADER_FRAGMENT, @@ -1556,8 +1593,8 @@ mod tests { #[test] fn filters_remote_ipv6_router_advertisements() { let mut registry = RoomRegistry::default(); - registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); let frame = ethernet_with_payload( destination, @@ -1576,8 +1613,8 @@ mod tests { #[test] fn filters_remote_ipv6_router_advertisements_after_extension_headers() { let mut registry = RoomRegistry::default(); - registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); let frame = ethernet_with_payload( destination, @@ -1596,8 +1633,8 @@ mod tests { #[test] fn allows_remote_icmpv6_that_is_not_router_advertisement() { let mut registry = RoomRegistry::default(); - let gateway = registry.join(gateway_hello()).unwrap(); - let client = registry.join(client_hello(1)).unwrap(); + let gateway = registry.join(&gateway_hello()).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let destination = MacAddr::new([0x33, 0x33, 0, 0, 0, 1]); let frame = ethernet_with_payload( destination, @@ -1617,7 +1654,7 @@ mod tests { #[test] fn drops_malformed_frames() { let mut registry = RoomRegistry::default(); - let client = registry.join(client_hello(1)).unwrap(); + let client = registry.join(&client_hello(1)).unwrap(); let decision = registry .forward_ethernet(&room(), client.peer().peer_id(), &[0; 4]) @@ -1630,7 +1667,7 @@ mod tests { #[test] fn reports_unknown_ingress_peer() { let mut registry = RoomRegistry::default(); - registry.join(client_hello(1)).unwrap(); + registry.join(&client_hello(1)).unwrap(); let frame = ethernet(MacAddr::BROADCAST, mac(1)); let error = registry.forward_ethernet(&room(), 99, &frame).unwrap_err(); diff --git a/crates/lanparty-relay/src/main.rs b/crates/lanparty-relay/src/main.rs index f568e6d..74c2f0f 100644 --- a/crates/lanparty-relay/src/main.rs +++ b/crates/lanparty-relay/src/main.rs @@ -1,3 +1,4 @@ +#![cfg_attr(test, allow(clippy::unwrap_used))] use clap::Parser; use lanparty_relay::{RelayArgs, RelayConfig, RelayServer}; diff --git a/crates/lanparty-relay/src/server.rs b/crates/lanparty-relay/src/server.rs index 95ce78a..6ec82b4 100644 --- a/crates/lanparty-relay/src/server.rs +++ b/crates/lanparty-relay/src/server.rs @@ -166,6 +166,13 @@ impl MalformedDatagramTracker { } impl RelayServer { + /// Binds the relay's QUIC endpoint and generates its development + /// certificate. + /// + /// # Errors + /// + /// Returns an error if the certificate cannot be generated or written, or + /// the listen address cannot be bound. pub fn bind(config: &RelayConfig) -> Result { let (server_config, certificate) = development_server_config_with_certificate()?; if let Some(path) = config.dev_cert_der_out() { @@ -185,12 +192,24 @@ impl RelayServer { } } + /// Returns the address the relay is actually listening on, which resolves + /// port 0 to the port the OS picked. + /// + /// # Errors + /// + /// Returns an error if the endpoint address cannot be read. pub fn local_addr(&self) -> Result { self.endpoint .local_addr() .context("failed to read relay local address") } + /// Accepts connections and serves rooms until the process is asked to shut + /// down. + /// + /// # Errors + /// + /// Returns an error if waiting for the shutdown signal fails. pub async fn run_until_shutdown(self) -> Result<()> { let endpoint = self.endpoint.clone(); let rooms = Arc::clone(&self.rooms); @@ -651,20 +670,16 @@ fn relay_frame_log_line( }; let source_mac = log .source_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let destination_mac = log .destination_mac() - .map(|mac| mac.to_string()) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |mac| mac.to_string()); let ethertype_or_len = log .ethertype_or_len() - .map(|value| format!("0x{value:04x}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}")); let drop_reason = log .drop_reason() - .map(|reason| format!("{reason:?}")) - .unwrap_or_else(|| "-".to_owned()); + .map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}")); format!( "relay frame room={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={} targets={}", @@ -722,8 +737,7 @@ fn egress_budget_skip_log_line( max_datagram_size: usize, ) -> String { format!( - "relay egress skipped room={} peer_id={} target_peer_id={} len={} max_datagram_size={} reason=datagram_budget", - room, ingress_peer_id, target_peer_id, datagram_len, max_datagram_size + "relay egress skipped room={room} peer_id={ingress_peer_id} target_peer_id={target_peer_id} len={datagram_len} max_datagram_size={max_datagram_size} reason=datagram_budget" ) } @@ -885,12 +899,12 @@ async fn build_handshake_response( }; let room = hello.room().clone(); - let hello = match limit_hello_to_connection(hello, connection_max_datagram_size) { + let hello = match limit_hello_to_connection(&hello, connection_max_datagram_size) { Ok(hello) => hello, Err(reject) => return (None, ControlMessage::Reject(reject)), }; let peer_max_datagram_size = usize::from(hello.max_datagram_size()); - let join = rooms.lock().await.join(hello); + let join = rooms.lock().await.join(&hello); match join { Ok(join) => { @@ -912,12 +926,12 @@ async fn build_handshake_response( } fn limit_hello_to_connection( - hello: EndpointHello, + hello: &EndpointHello, connection_max_datagram_size: usize, ) -> Result { - let max_datagram_size = usize::from(hello.max_datagram_size()) - .min(connection_max_datagram_size) - .min(usize::from(u16::MAX)) as u16; + let max_datagram_size = u16::try_from(connection_max_datagram_size) + .unwrap_or(u16::MAX) + .min(hello.max_datagram_size()); match hello.role() { Role::Client => EndpointHello::client( @@ -929,7 +943,7 @@ fn limit_hello_to_connection( ), Role::Gateway => EndpointHello::gateway(hello.room().clone(), max_datagram_size), } - .map_err(crate::reject_control_error) + .map_err(|error| crate::reject_control_error(&error)) } fn reject(reject: Reject) -> (Option, ControlMessage) { @@ -938,7 +952,7 @@ fn reject(reject: Reject) -> (Option, ControlMessage) { fn reject_codec_error(error: ControlCodecError) -> Reject { match error { - ControlCodecError::InvalidMessage(error) => crate::reject_control_error(error), + ControlCodecError::InvalidMessage(error) => crate::reject_control_error(&error), ControlCodecError::FrameTooShort { .. } | ControlCodecError::MessageTooLarge { .. } | ControlCodecError::IncompletePayload { .. } @@ -1026,6 +1040,10 @@ fn development_server_config_with_certificate() -> Result<(ServerConfig, Certifi #[cfg(test)] mod tests { + // The end-to-end tests script a whole client/gateway session pair; splitting + // them into helpers would scatter one readable scenario across the module. + #![allow(clippy::too_many_lines)] + use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -1882,8 +1900,8 @@ mod tests { let discover = udp_ipv4_frame( MacAddr::BROADCAST, client_mac, - Ipv4Addr::new(0, 0, 0, 0), - Ipv4Addr::new(255, 255, 255, 255), + Ipv4Addr::UNSPECIFIED, + Ipv4Addr::BROADCAST, DHCPV4_CLIENT_PORT, DHCPV4_SERVER_PORT, &discover_payload, @@ -1913,7 +1931,7 @@ mod tests { MacAddr::BROADCAST, dhcp_server_mac, dhcp_server_ip, - Ipv4Addr::new(255, 255, 255, 255), + Ipv4Addr::BROADCAST, DHCPV4_SERVER_PORT, DHCPV4_CLIENT_PORT, &offer_payload, @@ -2588,7 +2606,7 @@ mod tests { mac: MacAddr, ) -> AcceptedPeer { let hello = EndpointHello::client(RoomCode::new("TESTROOM").unwrap(), mac, 1400).unwrap(); - let join = rooms.lock().await.join(hello).unwrap(); + let join = rooms.lock().await.join(&hello).unwrap(); AcceptedPeer { room: RoomCode::new("TESTROOM").unwrap(), @@ -2601,7 +2619,7 @@ mod tests { async fn accepted_gateway_for_forwarding(rooms: &Arc>) -> AcceptedPeer { let hello = EndpointHello::gateway(RoomCode::new("TESTROOM").unwrap(), 1400).unwrap(); - let join = rooms.lock().await.join(hello).unwrap(); + let join = rooms.lock().await.join(&hello).unwrap(); AcceptedPeer { room: RoomCode::new("TESTROOM").unwrap(), @@ -2766,7 +2784,7 @@ mod tests { sum = (sum & 0xffff) + (sum >> 16); } - !(sum as u16) + !u16::try_from(sum).unwrap() } fn ethernet_frame_with_payload(