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
+19
View File
@@ -32,3 +32,22 @@ thiserror = "2"
tokio = { version = "1.53.1", features = ["macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
tracing = "0.1"
windows-sys = "0.61.2"
[profile.release]
debug = true
strip = false
debug-assertions = true
overflow-checks = true
lto = false
panic = "unwind"
incremental = true
[profile.production]
inherits = "release"
debug = false
strip = true
debug-assertions = false
overflow-checks = false
lto = true
incremental = false
codegen-units = 1
+23 -4
View File
@@ -123,17 +123,36 @@ Public relay binary and relay-owned room state:
## Build And Local Checks
```bash
cargo fmt --check
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo build --release -p lanparty-relay -p lanparty-gateway
just fmt
just test
just clippy
just build-release
git diff --check
```
`just clippy` must stay completely clean: every crate turns on
`clippy::pedantic`, `clippy::todo`, and `clippy::unwrap_used`, and the recipe
runs with `-D warnings` over all targets and features. Crates that need OS FFI
(`lanparty-gateway`, `lanparty-client-tap`, `lanparty-client-route`) set
`unsafe_code = "deny"` instead of `"forbid"`, and the single module holding
their FFI opts back in with `#![allow(unsafe_code)]`; every other crate forbids
`unsafe` outright.
These checks cover the local Rust code and the real client/relay/gateway session
paths that can run without Windows TAP or LAN hardware. For the Windows client
build and the manual MVP end-to-end proof, see [TESTING.md](TESTING.md).
### Build Profiles
| Profile | Command | Purpose |
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
| `dev` | `just build` | Everyday development. |
| `release` | `just build-release` | Optimized but debuggable: debug info, debug assertions and overflow checks on, no LTO. |
| `production` | `just build-production` | Shipping builds: no debug info, stripped, no debug assertions, fat LTO, one codegen unit. |
Use `release` when reproducing a bug at close-to-real speed, and `production`
for anything handed to someone else.
## Relay
```bash
+8
View File
@@ -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"
+115 -17
View File
@@ -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();
+10
View File
@@ -16,3 +16,13 @@ windows-sys = {
"Win32_Networking_WinSock",
]
}
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
# `deny` rather than `forbid`: this crate needs OS FFI, so single
# modules opt back in with `#![allow(unsafe_code)]`.
unsafe_code = "deny"
+61
View File
@@ -3,6 +3,7 @@
//! The client binary uses this crate to keep Win32 route/metric calls out of
//! the relay session code. The crate can snapshot the current relay route and
//! install scoped route/interface overrides that are restored when dropped.
#![cfg_attr(test, allow(clippy::unwrap_used))]
use std::net::IpAddr;
@@ -297,6 +298,13 @@ pub use windows::{
set_scoped_interface_mtu,
};
/// Returns the route Windows would currently use to reach
/// `destination`, so the relay path can be pinned to it.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn best_route_to(_destination: IpAddr) -> Result<RouteSnapshot> {
bail!("Windows route inspection is only available on Windows");
@@ -308,11 +316,26 @@ pub struct PinnedRelayRoute {
_private: (),
}
/// Installs a host route that keeps relay traffic on `route`'s
/// interface, restoring the previous state when the returned guard is
/// dropped.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn pin_relay_route(_route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
bail!("Windows route pinning is only available on Windows");
}
/// Resolves a Windows interface GUID string to the LUID and index the
/// IP Helper API expects.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn interface_identity_from_guid(_interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
bail!("Windows interface identity lookup is only available on Windows");
@@ -324,6 +347,12 @@ pub struct ScopedInterfaceMetric {
_private: (),
}
/// Reads the current routing metric of an interface.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn interface_metric(
_identity: NetworkInterfaceIdentity,
@@ -332,6 +361,13 @@ pub fn interface_metric(
bail!("Windows interface metric lookup is only available on Windows");
}
/// Overrides an interface's routing metric until the returned guard is
/// dropped.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn set_scoped_interface_metric(
_identity: NetworkInterfaceIdentity,
@@ -347,6 +383,13 @@ pub struct ScopedDefaultRoutes {
_private: (),
}
/// Enables or disables an interface's default routes until the returned
/// guard is dropped.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn set_scoped_default_routes_disabled(
_identity: NetworkInterfaceIdentity,
@@ -356,6 +399,12 @@ pub fn set_scoped_default_routes_disabled(
bail!("Windows interface default-route updates are only available on Windows");
}
/// Reads the current MTU of an interface.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn interface_mtu(
_identity: NetworkInterfaceIdentity,
@@ -364,6 +413,12 @@ pub fn interface_mtu(
bail!("Windows interface MTU lookup is only available on Windows");
}
/// Lists the unicast IP addresses currently assigned to an interface.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn interface_unicast_addresses(
_identity: NetworkInterfaceIdentity,
@@ -377,6 +432,12 @@ pub struct ScopedInterfaceMtu {
_private: (),
}
/// Overrides an interface's MTU until the returned guard is dropped.
///
/// # Errors
///
/// Always fails on non-Windows targets; this crate only implements the
/// Windows IP Helper backend.
#[cfg(not(windows))]
pub fn set_scoped_interface_mtu(
_identity: NetworkInterfaceIdentity,
@@ -1,3 +1,9 @@
//! Windows IP Helper (`iphlpapi`) route and interface manipulation.
//!
//! All Win32 FFI is confined to this module, which is why it opts back into
//! `unsafe`.
#![allow(unsafe_code)]
use std::{
fmt,
io,
@@ -58,6 +64,13 @@ use crate::{
RouteSnapshot,
};
/// Resolves a Windows interface GUID string to the LUID and index the
/// IP Helper API expects.
///
/// # Errors
///
/// Returns an error if `interface_guid` is not a well-formed GUID or
/// does not name an existing interface.
pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInterfaceIdentity> {
let guid = parse_interface_guid(interface_guid)?;
let mut luid = NET_LUID_LH::default();
@@ -83,6 +96,11 @@ pub fn interface_identity_from_guid(interface_guid: &str) -> Result<NetworkInter
Ok(NetworkInterfaceIdentity::new(index, luid_value(luid)))
}
/// Reads the current routing metric of an interface.
///
/// # Errors
///
/// Returns an error if the interface row cannot be read.
pub fn interface_metric(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -92,6 +110,11 @@ pub fn interface_metric(
Ok(metric_snapshot(identity, family, row))
}
/// Reads the current MTU of an interface.
///
/// # Errors
///
/// Returns an error if the interface row cannot be read.
pub fn interface_mtu(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -101,6 +124,11 @@ pub fn interface_mtu(
Ok(mtu_snapshot(identity, family, row))
}
/// Lists the unicast IP addresses currently assigned to an interface.
///
/// # Errors
///
/// Returns an error if the unicast address table cannot be read.
pub fn interface_unicast_addresses(
identity: NetworkInterfaceIdentity,
) -> Result<Vec<InterfaceUnicastAddress>> {
@@ -128,6 +156,13 @@ pub fn interface_unicast_addresses(
.collect())
}
/// Overrides an interface's routing metric until the returned guard is
/// dropped.
///
/// # Errors
///
/// Returns an error if the previous metric cannot be read or the new
/// one cannot be applied.
pub fn set_scoped_interface_metric(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -146,6 +181,12 @@ pub fn set_scoped_interface_metric(
})
}
/// Enables or disables an interface's default routes until the returned
/// guard is dropped.
///
/// # Errors
///
/// Returns an error if the interface row cannot be read or updated.
pub fn set_scoped_default_routes_disabled(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -164,6 +205,12 @@ pub fn set_scoped_default_routes_disabled(
})
}
/// Overrides an interface's MTU until the returned guard is dropped.
///
/// # Errors
///
/// Returns an error if `mtu` is zero, or if the interface row cannot be
/// read or updated.
pub fn set_scoped_interface_mtu(
identity: NetworkInterfaceIdentity,
family: IpInterfaceFamily,
@@ -299,6 +346,12 @@ impl Drop for ScopedInterfaceMtu {
}
}
/// Returns the route Windows would currently use to reach
/// `destination`, so the relay path can be pinned to it.
///
/// # Errors
///
/// Returns an error if the Windows best-route lookup fails.
pub fn best_route_to(destination: IpAddr) -> Result<RouteSnapshot> {
let destination_sockaddr = sockaddr_from_ip(destination);
let mut route = MIB_IPFORWARD_ROW2::default();
@@ -481,6 +534,13 @@ const fn address_family(family: IpInterfaceFamily) -> u16 {
}
}
/// Installs a host route that keeps relay traffic on `route`'s
/// interface, restoring the previous state when the returned guard is
/// dropped.
///
/// # Errors
///
/// Returns an error if the route cannot be installed.
pub fn pin_relay_route(route: &RouteSnapshot) -> Result<PinnedRelayRoute> {
let mut pinned = pinned_route_row(route);
let status = unsafe {
+10
View File
@@ -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"
+28 -2
View File
@@ -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");
+71
View File
@@ -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")?;
+8
View File
@@ -16,3 +16,11 @@ tokio.workspace = true
[target."cfg(windows)".dependencies]
lanparty-client-route = { path = "../lanparty-client-route" }
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
+10 -14
View File
@@ -1,3 +1,4 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
#[cfg(any(windows, test))]
use std::collections::BTreeMap;
#[cfg(any(windows, test))]
@@ -384,6 +385,8 @@ async fn run_client(
}
#[cfg(not(windows))]
// Mirrors the Windows signature so the call site needs no `cfg`.
#[allow(clippy::unused_async)]
async fn run_client(_session: &ClientSession) -> Result<()> {
unreachable!("ensure_supported_platform rejects non-Windows before tunnel setup")
}
@@ -756,24 +759,19 @@ fn client_frame_log_line(
};
let source_mac = log
.source_mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
let destination_mac = log
.destination_mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
let ethertype_or_len = log
.ethertype_or_len()
.map(|value| format!("0x{value:04x}"))
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}"));
let peer_id = log
.peer_id()
.map(|peer_id| peer_id.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string());
let drop_reason = log
.drop_reason()
.map(|reason| format!("{reason:?}"))
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}"));
format!(
"client frame direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}",
@@ -934,8 +932,7 @@ impl ControlEventFormatter {
Role::Client => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
format!(
"relay event: client peer {} with MAC {} left ({reason:?})",
peer.peer_id(),
@@ -961,8 +958,7 @@ fn format_peer_joined(peer: &PeerInfo) -> String {
Role::Client => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
format!(
"relay event: client peer {} joined with MAC {}",
peer.peer_id(),
+8
View File
@@ -9,3 +9,11 @@ lanparty-proto = { path = "../lanparty-proto" }
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
+43 -14
View File
@@ -23,24 +23,49 @@ pub enum ControlCodecError {
InvalidMessage(#[from] ControlError),
}
/// Encodes a control message as a length-prefixed JSON frame.
///
/// # Errors
///
/// Returns [`ControlCodecError::InvalidMessage`] if `message` does not pass its
/// own validation, [`ControlCodecError::Json`] if serialization fails, or
/// [`ControlCodecError::MessageTooLarge`] if the payload exceeds
/// [`MAX_CONTROL_MESSAGE_LEN`].
pub fn encode_control_message(message: &ControlMessage) -> Result<Vec<u8>, ControlCodecError> {
message.validate()?;
let payload = serde_json::to_vec(message)?;
let payload_len = payload.len();
if payload_len > MAX_CONTROL_MESSAGE_LEN {
// The wire prefix is a u32, and MAX_CONTROL_MESSAGE_LEN keeps it in range,
// so both conditions collapse into the same "too large" error.
let Some(prefix) = u32::try_from(payload_len)
.ok()
.filter(|_| payload_len <= MAX_CONTROL_MESSAGE_LEN)
else {
return Err(ControlCodecError::MessageTooLarge {
len: payload_len,
max: MAX_CONTROL_MESSAGE_LEN,
});
}
};
let mut frame = Vec::with_capacity(CONTROL_LENGTH_PREFIX_LEN + payload_len);
frame.extend_from_slice(&(payload_len as u32).to_be_bytes());
frame.extend_from_slice(&prefix.to_be_bytes());
frame.extend_from_slice(&payload);
Ok(frame)
}
/// Decodes exactly one control frame, which must contain one whole message and
/// nothing else.
///
/// # Errors
///
/// Returns [`ControlCodecError::FrameTooShort`] or
/// [`ControlCodecError::IncompletePayload`] if `frame` does not hold a complete
/// message, [`ControlCodecError::TrailingBytes`] if it holds more than one,
/// [`ControlCodecError::MessageTooLarge`] if the declared length is over the
/// limit, [`ControlCodecError::Json`] if the payload is not valid JSON, or
/// [`ControlCodecError::InvalidMessage`] if the decoded message fails
/// validation.
pub fn decode_control_frame(frame: &[u8]) -> Result<ControlMessage, ControlCodecError> {
let Some(total_len) = complete_control_frame_len(frame)? else {
return Err(incomplete_frame_error(frame));
@@ -59,6 +84,14 @@ pub fn decode_control_frame(frame: &[u8]) -> Result<ControlMessage, ControlCodec
Ok(message)
}
/// Returns the total length of the first complete frame in `buffer`, or `None`
/// if more bytes are still needed. Stream readers use this to decide when a
/// frame can be handed to [`decode_control_frame`].
///
/// # Errors
///
/// Returns [`ControlCodecError::MessageTooLarge`] if the declared payload
/// length exceeds [`MAX_CONTROL_MESSAGE_LEN`].
pub fn complete_control_frame_len(buffer: &[u8]) -> Result<Option<usize>, ControlCodecError> {
if buffer.len() < CONTROL_LENGTH_PREFIX_LEN {
return Ok(None);
@@ -81,18 +114,14 @@ pub fn complete_control_frame_len(buffer: &[u8]) -> Result<Option<usize>, Contro
}
fn declared_payload_len(buffer: &[u8]) -> Result<usize, ControlCodecError> {
if buffer.len() < CONTROL_LENGTH_PREFIX_LEN {
let Some(prefix) = buffer.first_chunk::<CONTROL_LENGTH_PREFIX_LEN>() else {
return Err(ControlCodecError::FrameTooShort {
actual: buffer.len(),
minimum: CONTROL_LENGTH_PREFIX_LEN,
});
}
};
Ok(u32::from_be_bytes(
buffer[0..CONTROL_LENGTH_PREFIX_LEN]
.try_into()
.expect("length prefix slice has exact size"),
) as usize)
Ok(u32::from_be_bytes(*prefix) as usize)
}
fn incomplete_frame_error(frame: &[u8]) -> ControlCodecError {
@@ -174,7 +203,8 @@ mod tests {
#[test]
fn rejects_oversized_declared_length() {
let mut frame = [0; CONTROL_LENGTH_PREFIX_LEN];
frame.copy_from_slice(&((MAX_CONTROL_MESSAGE_LEN as u32) + 1).to_be_bytes());
let oversized = u32::try_from(MAX_CONTROL_MESSAGE_LEN).unwrap() + 1;
frame.copy_from_slice(&oversized.to_be_bytes());
assert!(matches!(
complete_control_frame_len(&frame).unwrap_err(),
@@ -185,11 +215,10 @@ mod tests {
#[test]
fn validates_decoded_messages() {
let json = format!(
r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{}}}}}"#,
MIN_USEFUL_TAP_MTU
r#"{{"type":"welcome","payload":{{"protocol_version":1,"room_id":1,"peer_id":0,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}}}"#
);
let mut frame = Vec::new();
frame.extend_from_slice(&(json.len() as u32).to_be_bytes());
frame.extend_from_slice(&u32::try_from(json.len()).unwrap().to_be_bytes());
frame.extend_from_slice(json.as_bytes());
assert!(matches!(
+70 -9
View File
@@ -3,6 +3,7 @@
//! QUIC streams carry these messages as length-prefixed JSON frames. The crate
//! defines the typed handshake/status model and the small framing layer needed
//! by client, relay, and gateway stream handlers.
#![cfg_attr(test, allow(clippy::unwrap_used))]
use std::{fmt, str::FromStr};
@@ -54,6 +55,12 @@ pub enum ControlError {
pub struct RoomCode(String);
impl RoomCode {
/// Validates and wraps a room code.
///
/// # Errors
///
/// Returns a [`ControlError`] variant describing which room-code rule the
/// value breaks (length or allowed characters).
pub fn new(value: impl Into<String>) -> Result<Self, ControlError> {
let value = value.into();
validate_room_code(&value)?;
@@ -135,6 +142,13 @@ pub struct EndpointHello {
}
impl EndpointHello {
/// Builds the hello a tunnel client sends.
///
/// # Errors
///
/// Returns an error if `announced_mac` is not a locally administered
/// unicast address or `max_datagram_size` is too small to carry a useful
/// TAP MTU.
pub fn client(
room: RoomCode,
announced_mac: MacAddr,
@@ -152,6 +166,13 @@ impl EndpointHello {
Ok(hello)
}
/// Builds the hello the LAN gateway sends. A gateway announces no MAC
/// because it bridges the whole LAN rather than one identity.
///
/// # Errors
///
/// Returns an error if `max_datagram_size` is too small to carry a useful
/// TAP MTU.
pub fn gateway(room: RoomCode, max_datagram_size: u16) -> Result<Self, ControlError> {
let hello = Self {
protocol_version: CONTROL_PROTOCOL_VERSION,
@@ -165,6 +186,14 @@ impl EndpointHello {
Ok(hello)
}
/// Re-checks a decoded [`EndpointHello`], which serde may have built
/// without going through the constructors.
///
/// # Errors
///
/// Returns an error if the protocol version is not supported, the datagram
/// size cannot carry a useful TAP MTU, or the role and announced MAC do not
/// match.
pub fn validate(&self) -> Result<(), ControlError> {
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
return Err(ControlError::UnsupportedVersion {
@@ -225,6 +254,13 @@ pub struct ServerWelcome {
}
impl ServerWelcome {
/// Builds the welcome the relay sends once a peer is admitted to a room.
///
/// # Errors
///
/// Returns [`ControlError::InvalidPeerId`] if `peer_id` is 0, or
/// [`ControlError::EffectiveMtuTooSmall`] if `effective_tap_mtu` is below
/// [`MIN_USEFUL_TAP_MTU`].
pub fn new(room_id: u64, peer_id: u32, effective_tap_mtu: u16) -> Result<Self, ControlError> {
if peer_id == 0 {
return Err(ControlError::InvalidPeerId);
@@ -272,6 +308,13 @@ impl ServerWelcome {
self
}
/// Re-checks a decoded [`ServerWelcome`], which serde may have built
/// without going through [`ServerWelcome::new`].
///
/// # Errors
///
/// Returns an error if the protocol version is not supported, a peer id is
/// 0, or the effective TAP MTU is below [`MIN_USEFUL_TAP_MTU`].
pub fn validate(&self) -> Result<(), ControlError> {
if self.protocol_version != CONTROL_PROTOCOL_VERSION {
return Err(ControlError::UnsupportedVersion {
@@ -342,6 +385,12 @@ pub struct PeerInfo {
}
impl PeerInfo {
/// Describes one peer of a room.
///
/// # Errors
///
/// Returns an error if `peer_id` is 0, a client has no or an unusable MAC,
/// or a gateway announces one.
pub fn new(peer_id: u32, role: Role, mac: Option<MacAddr>) -> Result<Self, ControlError> {
if peer_id == 0 {
return Err(ControlError::InvalidPeerId);
@@ -364,6 +413,12 @@ impl PeerInfo {
}
}
/// Re-checks a decoded [`PeerInfo`], which serde may have built without
/// going through [`PeerInfo::new`].
///
/// # Errors
///
/// Returns an error if the peer id is 0 or the role and MAC do not match.
pub fn validate(&self) -> Result<(), ControlError> {
if self.peer_id == 0 {
return Err(ControlError::InvalidPeerId);
@@ -463,14 +518,21 @@ pub enum ControlMessage {
}
impl ControlMessage {
/// Validates a decoded control message by delegating to the payload type.
///
/// # Errors
///
/// Returns the [`ControlError`] reported by the contained payload, or
/// [`ControlError::InvalidPeerId`] for a peer-left notice about peer 0.
pub fn validate(&self) -> Result<(), ControlError> {
match self {
Self::Hello(hello) => hello.validate(),
Self::Welcome(welcome) => welcome.validate(),
Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } => Ok(()),
Self::PeerJoined(peer) => peer.validate(),
Self::PeerLeft { peer_id, .. } if *peer_id == 0 => Err(ControlError::InvalidPeerId),
Self::PeerLeft { .. } => Ok(()),
Self::Reject(_) | Self::Stats(_) | Self::Disconnect { .. } | Self::PeerLeft { .. } => {
Ok(())
}
}
}
}
@@ -559,15 +621,15 @@ mod tests {
#[test]
fn server_welcome_rejects_reserved_peer_id_and_tiny_mtu() {
assert_eq!(
ServerWelcome::new(1, 0, MIN_USEFUL_TAP_MTU as u16).unwrap_err(),
ServerWelcome::new(1, 0, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap_err(),
ControlError::InvalidPeerId
);
assert!(matches!(
ServerWelcome::new(1, 2, (MIN_USEFUL_TAP_MTU - 1) as u16).unwrap_err(),
ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU - 1).unwrap()).unwrap_err(),
ControlError::EffectiveMtuTooSmall { .. }
));
assert_eq!(
ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16)
ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap())
.unwrap()
.with_gateway_peer_id(Some(0))
.validate()
@@ -578,7 +640,7 @@ mod tests {
#[test]
fn server_welcome_reports_gateway_presence() {
let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16).unwrap();
let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap()).unwrap();
assert_eq!(welcome.mode(), ConnectionMode::Relay);
assert!(!welcome.gateway_connected());
@@ -595,7 +657,7 @@ mod tests {
#[test]
fn server_welcome_reports_connection_mode() {
let welcome = ServerWelcome::new(1, 2, MIN_USEFUL_TAP_MTU as u16)
let welcome = ServerWelcome::new(1, 2, u16::try_from(MIN_USEFUL_TAP_MTU).unwrap())
.unwrap()
.with_mode(ConnectionMode::DirectFailedRelayFallback);
@@ -619,8 +681,7 @@ mod tests {
#[test]
fn server_welcome_defaults_missing_mode_to_relay() {
let json = format!(
r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{}}}"#,
MIN_USEFUL_TAP_MTU
r#"{{"protocol_version":1,"room_id":1,"peer_id":2,"effective_tap_mtu":{MIN_USEFUL_TAP_MTU}}}"#
);
let welcome: ServerWelcome = serde_json::from_str(&json).unwrap();
+10
View File
@@ -18,3 +18,13 @@ tokio.workspace = true
[dev-dependencies]
rcgen.workspace = true
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
# `deny` rather than `forbid`: this crate needs OS FFI, so single
# modules opt back in with `#![allow(unsafe_code)]`.
unsafe_code = "deny"
+79 -30
View File
@@ -1,7 +1,8 @@
//! Linux LAN gateway control-plane connection.
//!
//! This crate owns the gateway binary's relay connection and Linux AF_PACKET
//! This crate owns the gateway binary's relay connection and Linux `AF_PACKET`
//! bridge loop that moves Ethernet frames between the relay and wired LAN.
#![cfg_attr(test, allow(clippy::unwrap_used))]
#[cfg(target_os = "linux")]
mod packet;
@@ -62,7 +63,7 @@ use tokio::io::unix::AsyncFd;
const MAX_CONTROL_FRAME_LEN: usize = CONTROL_LENGTH_PREFIX_LEN + MAX_CONTROL_MESSAGE_LEN;
const DISCONNECT_DRAIN_TIMEOUT: Duration = Duration::from_millis(250);
#[cfg(target_os = "linux")]
const CAM_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
const CAM_REFRESH_INTERVAL: Duration = Duration::from_mins(1);
#[cfg(target_os = "linux")]
const GATEWAY_STATS_INTERVAL: Duration = Duration::from_secs(10);
#[cfg(target_os = "linux")]
@@ -102,7 +103,7 @@ pub struct GatewayArgs {
#[arg(long)]
room: RoomCode,
/// Wired LAN interface that will later be opened with AF_PACKET.
/// Wired LAN interface that will later be opened with `AF_PACKET`.
#[arg(long, visible_alias = "iface")]
interface: String,
@@ -112,6 +113,13 @@ pub struct GatewayArgs {
}
impl GatewayArgs {
/// Resolves the parsed arguments into a validated [`GatewayConfig`],
/// reading the relay CA certificate from disk.
///
/// # Errors
///
/// Returns an error if the CA certificate cannot be read, the relay
/// endpoint cannot be resolved, or the resulting configuration is invalid.
pub fn into_config(self) -> Result<GatewayConfig> {
let relay_ca_cert = fs::read(&self.relay_ca_cert).with_context(|| {
format!(
@@ -147,6 +155,12 @@ pub struct GatewayConfig {
}
impl GatewayConfig {
/// Assembles everything the gateway needs to bridge one LAN into one room.
///
/// # Errors
///
/// Returns an error if the server name, CA certificate, or interface name is
/// empty, or if `max_datagram_size` cannot carry a useful TAP MTU.
pub fn new(
relay_addr: SocketAddr,
server_name: impl Into<String>,
@@ -270,6 +284,13 @@ impl GatewayConnection {
self.quic_max_datagram_size
}
/// Forwards one LAN Ethernet frame to the relay.
///
/// # Errors
///
/// Returns an error if the frame is malformed, filtered by the LAN safety
/// rules, exceeds the negotiated datagram budget, or the datagram send
/// fails.
pub fn send_ethernet(&self, frame: &[u8]) -> Result<()> {
match send_gateway_ethernet(
&self.connection,
@@ -288,10 +309,21 @@ impl GatewayConnection {
}
}
/// Waits for the next Ethernet frame a remote client sent into the room.
///
/// # Errors
///
/// Returns an error if the QUIC connection fails while reading.
pub async fn recv_ethernet(&self) -> Result<ReceivedEthernetFrame> {
recv_gateway_ethernet(&self.connection, &self.welcome, &self.stats).await
}
/// Waits for the next control-plane message from the relay.
///
/// # Errors
///
/// Returns an error if the control stream fails or carries a frame that
/// cannot be decoded.
pub async fn recv_control_event(&self) -> Result<ControlMessage> {
recv_gateway_control_event(&self.connection).await
}
@@ -301,11 +333,26 @@ impl GatewayConnection {
self.stats.snapshot()
}
/// Reports the current tunnel counters to the relay.
///
/// # Errors
///
/// Returns an error if the control stream write fails.
pub async fn send_stats_snapshot(&self) -> Result<()> {
send_gateway_stats(&self.connection, self.stats.snapshot()).await
}
/// Runs the LAN-to-relay bridge until the connection closes or the process
/// is asked to shut down.
///
/// # Errors
///
/// Returns an error if the LAN socket or the relay connection fails in a
/// way the bridge cannot recover from.
#[cfg(target_os = "linux")]
// The bridge is one `select!` loop over every event source; splitting arms
// into helpers would hide the shared state they all mutate.
#[allow(clippy::too_many_lines)]
pub async fn bridge_until_shutdown(self, packet_socket: PacketSocket) -> Result<()> {
let mut remote_clients = RemoteClientTable::new(packet_socket.interface_mac());
let mut cam_refresh_tick = tokio::time::interval_at(
@@ -596,12 +643,9 @@ async fn recv_gateway_ethernet_outcome(
stats.record_dropped_frame();
continue;
}
let ethernet_frame = match EthernetFrame::parse(packet.payload()) {
Ok(frame) => frame,
Err(_) => {
stats.record_malformed_frame();
continue;
}
let Ok(ethernet_frame) = EthernetFrame::parse(packet.payload()) else {
stats.record_malformed_frame();
continue;
};
stats.record_ethernet_rx(ethernet_frame);
@@ -757,8 +801,7 @@ fn format_gateway_control_event(event: &ControlMessage) -> String {
ControlMessage::PeerJoined(peer) if peer.role() == Role::Client => {
let mac = peer
.mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "unknown".to_string());
.map_or_else(|| "unknown".to_string(), |mac| mac.to_string());
format!(
"gateway control event: client peer {} joined with MAC {}",
peer.peer_id(),
@@ -796,24 +839,19 @@ fn gateway_frame_log_line(
};
let source_mac = log
.source_mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
let destination_mac = log
.destination_mac()
.map(|mac| mac.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |mac| mac.to_string());
let ethertype_or_len = log
.ethertype_or_len()
.map(|value| format!("0x{value:04x}"))
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |value| format!("0x{value:04x}"));
let peer_id = log
.peer_id()
.map(|peer_id| peer_id.to_string())
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |peer_id| peer_id.to_string());
let drop_reason = log
.drop_reason()
.map(|reason| format!("{reason:?}"))
.unwrap_or_else(|| "-".to_owned());
.map_or_else(|| "-".to_owned(), |reason| format!("{reason:?}"));
format!(
"gateway frame interface={} direction={:?} peer_id={} src={} dst={} ethertype_or_len={} len={} action={:?} drop_reason={}",
@@ -853,7 +891,7 @@ async fn read_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>) -> Result<Byte
return Ok(Bytes::from(buffer));
}
Ok(Err(error)) => return Err(error).context("failed to read LAN Ethernet frame"),
Err(_would_block) => continue,
Err(_would_block) => {}
}
}
}
@@ -870,7 +908,7 @@ async fn write_lan_ethernet(packet_socket: &AsyncFd<PacketSocket>, frame: &[u8])
Ok(Ok(sent)) if sent == frame.len() => return Ok(()),
Ok(Ok(sent)) => bail!("partial LAN Ethernet frame write: {sent}/{}", frame.len()),
Ok(Err(error)) => return Err(error).context("failed to write LAN Ethernet frame"),
Err(_would_block) => continue,
Err(_would_block) => {}
}
}
}
@@ -996,6 +1034,14 @@ fn cam_refresh_frame(source: MacAddr, destination: MacAddr) -> Vec<u8> {
frame
}
/// Connects to the relay, announces the gateway hello, and returns the
/// connection once the relay answers with a welcome.
///
/// # Errors
///
/// Returns an error if the QUIC endpoint cannot be created or connected, the
/// relay does not negotiate DATAGRAM support, the control handshake fails, or
/// the relay rejects the hello.
pub async fn connect_gateway(config: GatewayConfig) -> Result<GatewayConnection> {
let client_config = relay_client_config(config.relay_ca_cert_der())?;
let mut endpoint = Endpoint::client(client_bind_addr(config.relay_addr()))
@@ -1009,9 +1055,9 @@ pub async fn connect_gateway(config: GatewayConfig) -> Result<GatewayConnection>
let peer_datagram_size = connection
.max_datagram_size()
.context("relay did not negotiate QUIC DATAGRAM support")?;
let hello_datagram_size = usize::from(config.max_datagram_size())
.min(peer_datagram_size)
.min(usize::from(u16::MAX)) as u16;
let hello_datagram_size = u16::try_from(peer_datagram_size)
.unwrap_or(u16::MAX)
.min(config.max_datagram_size());
let hello = EndpointHello::gateway(config.room().clone(), hello_datagram_size)
.context("failed to build gateway hello")?;
let response = request_control_message(&connection, ControlMessage::Hello(hello)).await?;
@@ -1167,6 +1213,9 @@ mod tests {
}
#[tokio::test]
// One end-to-end handshake: a scripted relay plus the gateway side of the
// exchange only make sense read together.
#[allow(clippy::too_many_lines)]
async fn connects_to_relay_control_stream_as_gateway() {
let (server_config, certificate) = test_server_config();
let endpoint = Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap()).unwrap();
@@ -1339,10 +1388,10 @@ mod tests {
#[test]
fn snapshots_gateway_broadcast_stats() {
let stats = GatewayTunnelStats::default();
let broadcast_tx_bytes = broadcast_ethernet_frame(b"broadcast tx");
let broadcast_rx_bytes = broadcast_ethernet_frame(b"broadcast rx");
let broadcast_tx = EthernetFrame::parse(&broadcast_tx_bytes).unwrap();
let broadcast_rx = EthernetFrame::parse(&broadcast_rx_bytes).unwrap();
let sent_frame_bytes = broadcast_ethernet_frame(b"broadcast tx");
let received_frame_bytes = broadcast_ethernet_frame(b"broadcast rx");
let broadcast_tx = EthernetFrame::parse(&sent_frame_bytes).unwrap();
let broadcast_rx = EthernetFrame::parse(&received_frame_bytes).unwrap();
stats.record_ethernet_tx(broadcast_tx);
stats.record_datagram_rx();
+1
View File
@@ -1,3 +1,4 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
use clap::Parser;
use lanparty_gateway::GatewayArgs;
#[cfg(target_os = "linux")]
+49 -4
View File
@@ -1,3 +1,20 @@
//! Raw `AF_PACKET` socket plumbing for the Linux gateway.
//!
//! Everything that talks to the kernel through libc lives here, which is why
//! this is the one gateway module that opts back into `unsafe`.
#![allow(unsafe_code)]
// The libc bindings type kernel ABI fields as whatever C type the header uses
// (`c_char`, `c_int`, `c_ushort`, ...). Moving values between those and the Rust
// types used here is reinterpretation of a value that is already known to fit -
// interface indices, `ETH_P_ALL`, MAC octets, and the byte counts returned by
// send/recv after their error check - so a fallible conversion would only add
// unreachable branches.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
use std::{
ffi::CString,
fs,
@@ -22,6 +39,14 @@ pub struct PacketSocket {
}
impl PacketSocket {
/// Opens a promiscuous `AF_PACKET` socket bound to `interface`.
///
/// # Errors
///
/// Returns an error if the interface name is unusable or unknown, the
/// interface is wireless or has no carrier, the interface is not Ethernet,
/// or any of the socket, bind, promiscuous-membership, or MAC-lookup calls
/// fails - typically for lack of `CAP_NET_RAW`.
pub fn open(interface: &str) -> io::Result<Self> {
let interface_index = interface_index(interface)?;
reject_wireless_interface(interface)?;
@@ -58,7 +83,7 @@ impl PacketSocket {
// matches that struct. fd remains owned by this function across the call.
libc::bind(
fd.as_raw_fd(),
(&address as *const libc::sockaddr_ll).cast::<libc::sockaddr>(),
(&raw const address).cast::<libc::sockaddr>(),
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
@@ -91,6 +116,13 @@ impl PacketSocket {
self.interface_mac
}
/// Sends one Ethernet frame on the LAN, returning the number of bytes the
/// kernel accepted.
///
/// # Errors
///
/// Returns the OS error if the send fails, including `WouldBlock` on this
/// non-blocking socket.
pub fn send_frame(&self, frame: &[u8]) -> io::Result<usize> {
let sent = unsafe {
// SAFETY: frame.as_ptr() is valid for frame.len() bytes for the duration of send,
@@ -109,6 +141,13 @@ impl PacketSocket {
Ok(sent as usize)
}
/// Receives one inbound Ethernet frame into `buffer`, skipping the copies
/// of our own outgoing frames that `AF_PACKET` also delivers.
///
/// # Errors
///
/// Returns the OS error if the receive fails, including `WouldBlock` on this
/// non-blocking socket.
pub fn recv_frame(&self, buffer: &mut [u8]) -> io::Result<usize> {
loop {
let mut address = unsafe {
@@ -126,8 +165,8 @@ impl PacketSocket {
buffer.as_mut_ptr().cast::<libc::c_void>(),
buffer.len(),
0,
(&mut address as *mut libc::sockaddr_ll).cast::<libc::sockaddr>(),
&mut address_len,
(&raw mut address).cast::<libc::sockaddr>(),
&raw mut address_len,
)
};
if received < 0 {
@@ -146,6 +185,12 @@ impl AsRawFd for PacketSocket {
}
}
/// Resolves a network interface name to its kernel index.
///
/// # Errors
///
/// Returns an error if the name is empty, contains a path separator or NUL, is
/// longer than `IFNAMSIZ`, or does not name an existing interface.
pub fn interface_index(interface: &str) -> io::Result<u32> {
let name = interface_name(interface)?;
@@ -170,7 +215,7 @@ fn enable_promiscuous_membership(fd: RawFd, interface_index: u32) -> io::Result<
fd,
libc::SOL_PACKET,
libc::PACKET_ADD_MEMBERSHIP,
(&membership as *const libc::packet_mreq).cast::<libc::c_void>(),
(&raw const membership).cast::<libc::c_void>(),
std::mem::size_of::<libc::packet_mreq>() as libc::socklen_t,
)
};
+8
View File
@@ -5,3 +5,11 @@ edition.workspace = true
[dependencies]
thiserror.workspace = true
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
+15
View File
@@ -1,4 +1,5 @@
//! Shared network endpoint parsing for LAN party tunnel binaries.
#![cfg_attr(test, allow(clippy::unwrap_used))]
use std::{
fmt,
@@ -17,6 +18,12 @@ pub struct RelayEndpoint {
}
impl RelayEndpoint {
/// Builds an endpoint from an already split host and port.
///
/// # Errors
///
/// Returns [`RelayEndpointError::EmptyHost`] if `host` is empty or only
/// whitespace, or [`RelayEndpointError::InvalidPort`] if `port` is zero.
pub fn new(host: impl Into<String>, port: u16) -> Result<Self, RelayEndpointError> {
let host = host.into();
let host = host.trim();
@@ -45,6 +52,14 @@ impl RelayEndpoint {
self.port
}
/// Resolves the endpoint to its first socket address, performing a DNS
/// lookup when the host is not an IP literal.
///
/// # Errors
///
/// Returns [`RelayEndpointError::ResolveFailed`] if resolution itself
/// fails, or [`RelayEndpointError::NoResolvedAddress`] if it succeeds but
/// yields no address.
pub fn resolve(&self) -> Result<SocketAddr, RelayEndpointError> {
let mut addrs = (self.host.as_str(), self.port)
.to_socket_addrs()
+8
View File
@@ -9,3 +9,11 @@ serde.workspace = true
[dev-dependencies]
serde_json.workspace = true
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
+1
View File
@@ -2,6 +2,7 @@
//!
//! Runtime crates can convert these values into `tracing` fields, JSON logs, or
//! user-facing status lines without each component inventing its own vocabulary.
#![cfg_attr(test, allow(clippy::unwrap_used))]
use std::net::IpAddr;
+8
View File
@@ -6,3 +6,11 @@ edition.workspace = true
[dependencies]
serde.workspace = true
thiserror.workspace = true
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[lints.rust]
unsafe_code = "forbid"
+7
View File
@@ -12,6 +12,13 @@ pub struct EthernetFrame<'a> {
}
impl<'a> EthernetFrame<'a> {
/// Borrows `bytes` as an Ethernet frame after checking that it is long
/// enough to contain a complete Ethernet header.
///
/// # Errors
///
/// Returns [`ProtoError::EthernetFrameTooShort`] if `bytes` is shorter than
/// [`MIN_ETHERNET_FRAME_LEN`].
pub fn parse(bytes: &'a [u8]) -> Result<Self, ProtoError> {
if bytes.len() < MIN_ETHERNET_FRAME_LEN {
return Err(ProtoError::EthernetFrameTooShort {
+1
View File
@@ -3,6 +3,7 @@
//! This crate intentionally contains no socket, TAP, QUIC, or OS-specific
//! behavior. It is the small contract that the Windows client, Linux gateway,
//! and relay must all agree on.
#![cfg_attr(test, allow(clippy::unwrap_used))]
mod ethernet;
mod mac;
+2 -2
View File
@@ -91,8 +91,8 @@ impl From<MacAddr> for [u8; 6] {
impl fmt::Display for MacAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let [a, b, c, d, e, g] = self.0;
write!(f, "{a:02x}:{b:02x}:{c:02x}:{d:02x}:{e:02x}:{g:02x}")
let [o0, o1, o2, o3, o4, o5] = self.0;
write!(f, "{o0:02x}:{o1:02x}:{o2:02x}:{o3:02x}:{o4:02x}:{o5:02x}")
}
}
+15
View File
@@ -17,6 +17,14 @@ pub enum MtuError {
},
}
/// Returns the largest TAP MTU that still fits into the negotiated QUIC
/// datagram budget, after subtracting overlay and Ethernet framing overhead
/// plus `safety_margin`.
///
/// # Errors
///
/// Returns [`MtuError::DatagramTooSmall`] if the budget cannot even carry
/// [`MIN_USEFUL_TAP_MTU`] bytes of payload on top of that overhead.
pub fn max_tap_mtu_for_datagram(
quic_max_datagram_size: usize,
safety_margin: usize,
@@ -34,6 +42,13 @@ pub fn max_tap_mtu_for_datagram(
Ok(quic_max_datagram_size - overhead)
}
/// Returns the TAP MTU to configure: [`DEFAULT_TAP_MTU`], clamped down when the
/// negotiated QUIC datagram budget cannot carry it.
///
/// # Errors
///
/// Returns [`MtuError::DatagramTooSmall`] if the budget is too small to be
/// useful at all; see [`max_tap_mtu_for_datagram`].
pub fn recommended_tap_mtu(quic_max_datagram_size: usize) -> Result<usize, MtuError> {
let max = max_tap_mtu_for_datagram(quic_max_datagram_size, DEFAULT_DATAGRAM_SAFETY_MARGIN)?;
+62 -11
View File
@@ -19,6 +19,12 @@ impl FrameType {
self as u8
}
/// Decodes the on-wire frame type discriminant.
///
/// # Errors
///
/// Returns [`ProtoError::UnknownFrameType`] for a discriminant this
/// protocol version does not define.
pub const fn from_u8(value: u8) -> Result<Self, ProtoError> {
match value {
1 => Ok(Self::Ethernet),
@@ -39,6 +45,13 @@ pub struct OverlayHeader {
}
impl OverlayHeader {
/// Builds a header for a payload of `payload_len` bytes.
///
/// # Errors
///
/// Returns [`ProtoError::UnsupportedFlags`] if `flags` sets any bit that is
/// still reserved, or [`ProtoError::PayloadTooLarge`] if `payload_len` does
/// not fit into the 16-bit wire field.
pub fn new(
frame_type: FrameType,
room_id: u64,
@@ -99,35 +112,49 @@ impl OverlayHeader {
bytes
}
/// Decodes the fixed-size header prefix of a received datagram.
///
/// # Errors
///
/// Returns [`ProtoError::DatagramTooShort`] if `bytes` is shorter than
/// [`OVERLAY_HEADER_LEN`], [`ProtoError::BadMagic`] or
/// [`ProtoError::UnsupportedVersion`] if the datagram is not a datagram of
/// this protocol version, [`ProtoError::UnsupportedFlags`] if a reserved
/// flag bit is set, or [`ProtoError::UnknownFrameType`] for an unknown
/// frame type.
pub fn decode(bytes: &[u8]) -> Result<Self, ProtoError> {
if bytes.len() < OVERLAY_HEADER_LEN {
// Taking the header as a fixed-size array up front makes every field
// read below a statically bounds-checked array index, so this function
// has no panicking path.
let Some(header) = bytes.first_chunk::<OVERLAY_HEADER_LEN>() else {
return Err(ProtoError::DatagramTooShort {
actual: bytes.len(),
minimum: OVERLAY_HEADER_LEN,
});
}
};
let magic = u32::from_be_bytes(bytes[0..4].try_into().expect("header magic slice length"));
let magic = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
if magic != OVERLAY_MAGIC {
return Err(ProtoError::BadMagic { actual: magic });
}
let version = bytes[4];
let version = header[4];
if version != OVERLAY_VERSION {
return Err(ProtoError::UnsupportedVersion { actual: version });
}
let flags = u16::from_be_bytes(bytes[18..20].try_into().expect("flags slice length"));
let flags = u16::from_be_bytes([header[18], header[19]]);
validate_overlay_flags(flags)?;
Ok(Self {
frame_type: FrameType::from_u8(bytes[5])?,
room_id: u64::from_be_bytes(bytes[6..14].try_into().expect("room id slice length")),
peer_id: u32::from_be_bytes(bytes[14..18].try_into().expect("peer id slice length")),
frame_type: FrameType::from_u8(header[5])?,
room_id: u64::from_be_bytes([
header[6], header[7], header[8], header[9], header[10], header[11], header[12],
header[13],
]),
peer_id: u32::from_be_bytes([header[14], header[15], header[16], header[17]]),
flags,
payload_len: u16::from_be_bytes(
bytes[20..22].try_into().expect("payload len slice length"),
),
payload_len: u16::from_be_bytes([header[20], header[21]]),
})
}
}
@@ -139,6 +166,12 @@ pub struct OverlayPacket<'a> {
}
impl<'a> OverlayPacket<'a> {
/// Pairs a decoded header with its payload.
///
/// # Errors
///
/// Returns [`ProtoError::PayloadLengthMismatch`] if `payload` is not
/// exactly as long as the header declares.
pub fn new(header: OverlayHeader, payload: &'a [u8]) -> Result<Self, ProtoError> {
let declared = usize::from(header.payload_len);
@@ -193,6 +226,11 @@ fn validate_overlay_flags(flags: u16) -> Result<(), ProtoError> {
Ok(())
}
/// Encodes a complete overlay datagram: header followed by `payload`.
///
/// # Errors
///
/// Propagates every error of [`OverlayHeader::new`].
pub fn encode_datagram(
frame_type: FrameType,
room_id: u64,
@@ -207,6 +245,13 @@ pub fn encode_datagram(
Ok(datagram)
}
/// Checks an encoded datagram against the QUIC datagram size the peers
/// negotiated.
///
/// # Errors
///
/// Returns [`ProtoError::DatagramExceedsBudget`] if `datagram_len` exceeds
/// `max_datagram_size`.
pub fn validate_datagram_budget(
datagram_len: usize,
max_datagram_size: usize,
@@ -221,6 +266,12 @@ pub fn validate_datagram_budget(
Ok(())
}
/// Decodes a received overlay datagram, borrowing its payload from `bytes`.
///
/// # Errors
///
/// Propagates every error of [`OverlayHeader::decode`] and
/// [`OverlayPacket::new`].
pub fn decode_datagram(bytes: &[u8]) -> Result<OverlayPacket<'_>, ProtoError> {
let header = OverlayHeader::decode(bytes)?;
let payload = &bytes[OVERLAY_HEADER_LEN..];
+14 -4
View File
@@ -14,7 +14,6 @@ const IPV6_NEXT_HEADER_HOP_BY_HOP: u8 = 0;
const IPV6_NEXT_HEADER_ROUTING: u8 = 43;
const IPV6_NEXT_HEADER_FRAGMENT: u8 = 44;
const IPV6_NEXT_HEADER_AH: u8 = 51;
const IPV6_NEXT_HEADER_NO_NEXT: u8 = 59;
const IPV6_NEXT_HEADER_DESTINATION_OPTIONS: u8 = 60;
const IPV6_NEXT_HEADER_ICMPV6: u8 = 58;
const DHCPV4_SERVER_PORT: u16 = 67;
@@ -34,6 +33,9 @@ pub enum EthernetSafetyDrop {
Ipv6Fragment,
}
/// Returns why a frame coming from the physical LAN must not be forwarded into
/// the tunnel, or `None` if it may be forwarded.
#[must_use]
pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
if !frame.source().is_valid_unicast() {
return Some(EthernetSafetyDrop::InvalidSourceMac);
@@ -42,6 +44,12 @@ pub fn gateway_lan_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<Ethern
common_safety_drop_reason(frame)
}
/// Returns why a frame received from a remote tunnel client must not be
/// injected into the physical LAN, or `None` if it may be injected.
///
/// This is stricter than [`gateway_lan_safety_drop_reason`]: remote peers must
/// not be able to act as a VLAN trunk, DHCP server, or IPv6 router on the LAN.
#[must_use]
pub fn remote_client_safety_drop_reason(frame: EthernetFrame<'_>) -> Option<EthernetSafetyDrop> {
if let Some(drop_reason) = common_safety_drop_reason(frame) {
return Some(drop_reason);
@@ -93,9 +101,9 @@ fn is_vlan_tagged_frame(frame: EthernetFrame<'_>) -> bool {
}
fn is_link_local_control_destination(mac: MacAddr) -> bool {
let [a, b, c, d, e, f] = mac.octets();
let [prefix @ .., last] = mac.octets();
[a, b, c, d, e] == [0x01, 0x80, 0xc2, 0x00, 0x00] && f <= 0x0f
prefix == [0x01, 0x80, 0xc2, 0x00, 0x00] && last <= 0x0f
}
fn is_dhcp_server_reply(frame: EthernetFrame<'_>) -> bool {
@@ -190,7 +198,6 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt
loop {
match next_header {
next_header if next_header == expected_next_header => return Some(offset),
IPV6_NEXT_HEADER_NO_NEXT => return None,
IPV6_NEXT_HEADER_HOP_BY_HOP
| IPV6_NEXT_HEADER_ROUTING
| IPV6_NEXT_HEADER_DESTINATION_OPTIONS => {
@@ -216,6 +223,9 @@ fn ipv6_upper_layer_payload_offset(ipv6: &[u8], expected_next_header: u8) -> Opt
return None;
}
}
// Any other next-header value is either an upper-layer protocol we
// were not asked about or the "no next header" terminator (59), so
// the header we are looking for is not in this packet.
_ => return None,
}
}
+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(