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:
35 files changed
+1024
-227
No files matched your search
@@ -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"
|
||||
@@ -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<Self> {
|
||||
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<Self> {
|
||||
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<PathBuf>) -> Result<Self> {
|
||||
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<ClientIdentity> {
|
||||
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<String>,
|
||||
@@ -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<ClientSendOutcome> {
|
||||
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<ReceivedEthernetFrame> {
|
||||
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<ClientReceiveOutcome> {
|
||||
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<ControlMessage> {
|
||||
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<ClientSendOutcome> {
|
||||
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<ReceivedEthernetFrame> {
|
||||
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<ClientReceiveOutcome> {
|
||||
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<ClientSession> {
|
||||
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<ClientSession
|
||||
|
||||
#[must_use]
|
||||
fn negotiated_quic_datagram_size(configured: u16, peer: usize) -> 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<ClientConfig> {
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user