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:
2026-08-16 19:00:54 +02:00
parent 2bed62e9ec
commit e62f584377
35 changed files with 1024 additions and 227 deletions
+8
View File
@@ -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"
+20
View File
@@ -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,
+129 -92
View File
@@ -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
View File
@@ -1,3 +1,4 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
use clap::Parser;
use lanparty_relay::{RelayArgs, RelayConfig, RelayServer};
+42 -24
View File
@@ -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(