chore(lints): enforce per-crate lint policy and add build profiles
Give every member crate its own `[lints.clippy]` (`pedantic`, `todo`, `unwrap_used`) and `[lints.rust]` block, add explicit `release` and `production` profiles to the workspace manifest, and fix the code so that `just clippy` -- `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passes with zero warnings. Declaring the lints is the easy half and on its own it was actively harmful: `unsafe_code = "forbid"` cannot be overridden from source, so it broke `lanparty-gateway`, `lanparty-client-tap`, and `lanparty-client-route`, which all need OS FFI, and the workspace no longer compiled on Linux at all. The rest of the workspace produced roughly 120 denied lints. A lint policy nobody can satisfy is worse than no policy, so this commit makes the tree actually clean. Unsafe policy: the three FFI crates use `unsafe_code = "deny"`, and the one module in each that holds the FFI (`gateway::packet`, `client_tap::windows`, `client_route::windows`) opts back in with a documented `#![allow(unsafe_code)]`. Every other crate keeps `"forbid"`. `deny` was chosen over dropping the lint so that `unsafe` outside those modules is still a hard error, and over per-block allows because the FFI is dense enough that per-block attributes would drown the code. Build profiles: `release` stays optimized but debuggable (debug info, debug assertions, overflow checks, no LTO, incremental) so that a bug reproduced at close-to-real speed still panics loudly and gives a usable backtrace. `production` inherits from it and turns all of that off, adding fat LTO and a single codegen unit, and is what ships. Note the consequence: `cargo build --release` binaries are now slower than before this commit and must not be shipped; use `just build-production`. Code changes made to satisfy the lints, grouped by kind: - `# Errors` sections on every public fallible function, and `# Panics` on `RoomRegistry::new`. The Windows-only implementations and their non-Windows `bail!` stubs are documented in parallel so the crates stay clean when built for Windows too, which CI on Linux cannot check. - Panicking paths removed rather than documented where the panic was only an unreachable invariant: `OverlayHeader::decode` and `declared_payload_len` take a fixed-size prefix via `first_chunk` instead of `try_into().expect(...)`, and `RoomRegistry::join` returns an `InternalError` reject instead of `expect`ing the room it just inserted. - Tests keep using `unwrap`/`unwrap_err`: each crate root carries `#![cfg_attr(test, allow(clippy::unwrap_used))]`. The lint is about production code; an `expect` message per assertion buys nothing. - Lossy casts replaced with `try_from` where a real conversion was happening (datagram-size negotiation, control-frame length prefix, MTU clamping). In `gateway::packet` the cast lints are allowed module-wide instead: those casts move values between libc's C types and ours after the value is already known to fit, so fallible conversions would only add unreachable branches. - Signature changes, all on private or crate-internal items except one: `RoomRegistry::join` now takes `&EndpointHello` (public, it never consumed the hello), `Room::join` and `reject_control_error` likewise. `Room`'s `room_id` field is now `id`. - `#[allow(clippy::too_many_lines)]` with a reason on three end-to-end test scenarios and on `bridge_until_shutdown`, whose `select!` loop mutates state shared by every arm; splitting it would hide that. - Mechanical fixes from `cargo clippy --fix`: `map_or_else`, `let...else`, inline format args, backticks in doc comments, `Duration::from_mins`. No runtime behavior changes: the only observable differences are the error text when a relay room lookup fails immediately after insertion (previously a panic) and the profile rename described above. Also documents the lint policy and the profile table in README.md, whose build section now points at the `just` recipes it should have used all along. Test Plan: - `just clippy` -- passes, zero warnings - `just test` -- 186 tests pass, 0 failed - `cargo check --profile production --workspace --all-features` -- passes - `just fmt` -- clean - Not verified: the Windows-only code paths and their doc comments, which need a Windows target to compile. Refs: https://doc.rust-lang.org/cargo/reference/profiles.html Refs: https://rust-lang.github.io/rust-clippy/master/index.html#missing_errors_doc Refs: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-check-attributes
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<String>, component_id: impl Into<String>) -> Result<Self> {
|
||||
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<String> {
|
||||
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<Vec<TapAdapterInfo>> {
|
||||
bail!("TAP-Windows6 adapter discovery is only available on Windows");
|
||||
|
||||
@@ -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<Self> {
|
||||
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<MacAddr> {
|
||||
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<u32> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<usize> {
|
||||
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<TapAdapter> {
|
||||
let mut adapters = available_adapters()?;
|
||||
let info = adapters
|
||||
@@ -232,6 +290,13 @@ pub fn open_first_adapter() -> Result<TapAdapter> {
|
||||
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<Vec<TapAdapterInfo>> {
|
||||
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
||||
.context("failed to open TAP adapter registry key")?;
|
||||
|
||||
Reference in New Issue
Block a user