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:
@@ -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"
|
||||
|
||||
@@ -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, ConfigError> {
|
||||
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, ConfigError> {
|
||||
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,
|
||||
|
||||
@@ -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<JoinAccepted, Reject> {
|
||||
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<JoinAccepted, Reject> {
|
||||
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<LeaveResult, ForwardingError> {
|
||||
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<u16>,
|
||||
@@ -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<JoinAccepted, Reject> {
|
||||
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();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![cfg_attr(test, allow(clippy::unwrap_used))]
|
||||
use clap::Parser;
|
||||
use lanparty_relay::{RelayArgs, RelayConfig, RelayServer};
|
||||
|
||||
|
||||
@@ -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<Self> {
|
||||
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<SocketAddr> {
|
||||
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<EndpointHello, Reject> {
|
||||
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<AcceptedPeer>, ControlMessage) {
|
||||
@@ -938,7 +952,7 @@ fn reject(reject: Reject) -> (Option<AcceptedPeer>, 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<Mutex<RoomRegistry>>) -> 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(
|
||||
|
||||
Reference in New Issue
Block a user