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
540 lines
16 KiB
Rust
540 lines
16 KiB
Rust
//! 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},
|
|
ptr::{null, null_mut},
|
|
};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use lanparty_proto::MacAddr;
|
|
use windows_sys::Win32::{
|
|
Foundation::{
|
|
CloseHandle,
|
|
ERROR_FILE_NOT_FOUND,
|
|
ERROR_MORE_DATA,
|
|
ERROR_NO_MORE_ITEMS,
|
|
ERROR_SUCCESS,
|
|
GENERIC_READ,
|
|
GENERIC_WRITE,
|
|
HANDLE,
|
|
INVALID_HANDLE_VALUE,
|
|
},
|
|
Storage::FileSystem::{
|
|
CreateFileW,
|
|
FILE_ATTRIBUTE_SYSTEM,
|
|
FILE_SHARE_READ,
|
|
FILE_SHARE_WRITE,
|
|
OPEN_EXISTING,
|
|
ReadFile,
|
|
WriteFile,
|
|
},
|
|
System::{
|
|
IO::DeviceIoControl,
|
|
Registry::{
|
|
HKEY,
|
|
HKEY_LOCAL_MACHINE,
|
|
KEY_READ,
|
|
KEY_SET_VALUE,
|
|
REG_SZ,
|
|
RegCloseKey,
|
|
RegEnumKeyExW,
|
|
RegOpenKeyExW,
|
|
RegQueryValueExW,
|
|
RegSetValueExW,
|
|
},
|
|
},
|
|
};
|
|
|
|
use crate::{
|
|
TAP_ADAPTER_KEY,
|
|
TapAdapterInfo,
|
|
is_tap_component_id,
|
|
tap_ioctl_get_mac,
|
|
tap_ioctl_get_mtu,
|
|
tap_ioctl_set_media_status,
|
|
tap_network_address_value,
|
|
validate_tap_ethernet_frame,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct TapAdapter {
|
|
info: TapAdapterInfo,
|
|
handle: OwnedHandle,
|
|
}
|
|
|
|
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);
|
|
let handle = unsafe {
|
|
// SAFETY: wide_path is NUL-terminated and lives for the duration of the call.
|
|
// The security attributes and template handle are intentionally null.
|
|
CreateFileW(
|
|
wide_path.as_ptr(),
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
null(),
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_SYSTEM,
|
|
null_mut(),
|
|
)
|
|
};
|
|
let handle = OwnedHandle::new(handle)
|
|
.with_context(|| format!("failed to open TAP adapter device {path}"))?;
|
|
|
|
Ok(Self { info, handle })
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn info(&self) -> &TapAdapterInfo {
|
|
&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(
|
|
tap_ioctl_set_media_status(),
|
|
(&mut status as *mut u32).cast::<c_void>(),
|
|
std::mem::size_of::<u32>() as u32,
|
|
null_mut(),
|
|
0,
|
|
)
|
|
.context("failed to set TAP media status")?;
|
|
|
|
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(
|
|
tap_ioctl_get_mac(),
|
|
null_mut(),
|
|
0,
|
|
bytes.as_mut_ptr().cast::<c_void>(),
|
|
bytes.len() as u32,
|
|
)
|
|
.context("failed to read TAP driver MAC address")?;
|
|
|
|
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(
|
|
tap_ioctl_get_mtu(),
|
|
null_mut(),
|
|
0,
|
|
(&mut mtu as *mut u32).cast::<c_void>(),
|
|
std::mem::size_of::<u32>() as u32,
|
|
)
|
|
.context("failed to read TAP driver MTU")?;
|
|
|
|
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 {
|
|
// SAFETY: buffer is valid for writes of buffer.len() bytes and the handle is owned by
|
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
|
ReadFile(
|
|
self.handle.raw(),
|
|
buffer.as_mut_ptr(),
|
|
buffer
|
|
.len()
|
|
.try_into()
|
|
.context("TAP read buffer is too large")?,
|
|
&mut bytes_read,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error()).context("failed to read TAP frame");
|
|
}
|
|
|
|
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])?;
|
|
|
|
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 {
|
|
// SAFETY: frame is valid for reads of frame.len() bytes and the handle is owned by
|
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
|
WriteFile(
|
|
self.handle.raw(),
|
|
frame.as_ptr(),
|
|
frame.len().try_into().context("TAP frame is too large")?,
|
|
&mut bytes_written,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error()).context("failed to write TAP frame");
|
|
}
|
|
|
|
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)?;
|
|
if written != frame.len() {
|
|
bail!("partial TAP frame write: {written}/{}", frame.len());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn device_io_control(
|
|
&self,
|
|
code: u32,
|
|
input: *mut c_void,
|
|
input_len: u32,
|
|
output: *mut c_void,
|
|
output_len: u32,
|
|
) -> io::Result<u32> {
|
|
let mut bytes_returned = 0_u32;
|
|
let ok = unsafe {
|
|
// SAFETY: input/output pointers and lengths are supplied by the typed public methods
|
|
// above. The synchronous handle uses a null OVERLAPPED pointer.
|
|
DeviceIoControl(
|
|
self.handle.raw(),
|
|
code,
|
|
input.cast_const(),
|
|
input_len,
|
|
output,
|
|
output_len,
|
|
&mut bytes_returned,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
|
|
Ok(bytes_returned)
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
.drain(..)
|
|
.next()
|
|
.context("no TAP-Windows6 adapters found")?;
|
|
|
|
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()
|
|
.context("TAP adapter was not discovered from the Windows registry")?;
|
|
let registry_path = format!("{TAP_ADAPTER_KEY}\\{driver_key_name}");
|
|
let key = RegKey::open_with_access(HKEY_LOCAL_MACHINE, ®istry_path, KEY_SET_VALUE)
|
|
.with_context(|| format!("failed to open TAP adapter registry key {registry_path}"))?;
|
|
key.set_string("NetworkAddress", &tap_network_address_value(mac)?)
|
|
.context("failed to configure TAP adapter NetworkAddress")?;
|
|
|
|
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")?;
|
|
let mut adapters = Vec::new();
|
|
|
|
for subkey_name in adapters_key.subkey_names()? {
|
|
let subkey = adapters_key
|
|
.open_subkey(&subkey_name)
|
|
.with_context(|| format!("failed to open TAP adapter registry subkey {subkey_name}"))?;
|
|
let Some(component_id) = subkey.query_string("ComponentId")? else {
|
|
continue;
|
|
};
|
|
if !is_tap_component_id(&component_id) {
|
|
continue;
|
|
}
|
|
let Some(instance_id) = subkey.query_string("NetCfgInstanceId")? else {
|
|
continue;
|
|
};
|
|
|
|
adapters.push(TapAdapterInfo::from_registry(
|
|
subkey_name,
|
|
instance_id,
|
|
component_id,
|
|
)?);
|
|
}
|
|
|
|
Ok(adapters)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct OwnedHandle(HANDLE);
|
|
|
|
// SAFETY: Windows file handles are process-wide kernel object references that may be used from
|
|
// multiple threads. `OwnedHandle` only closes the handle in `Drop`; callers must still uphold any
|
|
// higher-level synchronization required by the device protocol.
|
|
unsafe impl Send for OwnedHandle {}
|
|
|
|
// SAFETY: Sharing references to `OwnedHandle` only exposes the raw handle to synchronous Windows
|
|
// APIs. The handle value itself is immutable, and Windows permits issuing I/O on a file handle from
|
|
// more than one thread.
|
|
unsafe impl Sync for OwnedHandle {}
|
|
|
|
impl OwnedHandle {
|
|
fn new(handle: HANDLE) -> io::Result<Self> {
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
Err(io::Error::last_os_error())
|
|
} else {
|
|
Ok(Self(handle))
|
|
}
|
|
}
|
|
|
|
const fn raw(&self) -> HANDLE {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for OwnedHandle {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
// SAFETY: self.0 is a valid owned HANDLE created by CreateFileW.
|
|
CloseHandle(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct RegKey(HKEY);
|
|
|
|
impl RegKey {
|
|
fn open(root: HKEY, path: &str) -> io::Result<Self> {
|
|
Self::open_with_access(root, path, KEY_READ)
|
|
}
|
|
|
|
fn open_with_access(root: HKEY, path: &str, access: u32) -> io::Result<Self> {
|
|
let path = wide_null(path);
|
|
let mut key = null_mut();
|
|
let status = unsafe {
|
|
// SAFETY: path is NUL-terminated and phkresult points to valid storage.
|
|
RegOpenKeyExW(root, path.as_ptr(), 0, access, &mut key)
|
|
};
|
|
windows_status(status)?;
|
|
|
|
Ok(Self(key))
|
|
}
|
|
|
|
fn open_subkey(&self, path: &str) -> io::Result<Self> {
|
|
Self::open(self.0, path)
|
|
}
|
|
|
|
fn subkey_names(&self) -> io::Result<Vec<String>> {
|
|
let mut names = Vec::new();
|
|
let mut index = 0_u32;
|
|
|
|
loop {
|
|
let mut buffer = vec![0_u16; 256];
|
|
let mut len = buffer.len() as u32;
|
|
let status = unsafe {
|
|
// SAFETY: buffer is valid for len UTF-16 code units and len points to storage
|
|
// that RegEnumKeyExW updates with the returned name length.
|
|
RegEnumKeyExW(
|
|
self.0,
|
|
index,
|
|
buffer.as_mut_ptr(),
|
|
&mut len,
|
|
null(),
|
|
null_mut(),
|
|
null_mut(),
|
|
null_mut(),
|
|
)
|
|
};
|
|
match status {
|
|
ERROR_SUCCESS => {
|
|
buffer.truncate(len as usize);
|
|
names.push(String::from_utf16_lossy(&buffer));
|
|
index += 1;
|
|
}
|
|
ERROR_NO_MORE_ITEMS => return Ok(names),
|
|
ERROR_MORE_DATA => {
|
|
return Err(io::Error::new(
|
|
ErrorKind::InvalidData,
|
|
"registry subkey name exceeded internal buffer",
|
|
));
|
|
}
|
|
status => return Err(windows_error(status)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn query_string(&self, name: &str) -> io::Result<Option<String>> {
|
|
let name = wide_null(name);
|
|
let mut value_type = 0_u32;
|
|
let mut byte_len = 0_u32;
|
|
let status = unsafe {
|
|
// SAFETY: name is NUL-terminated. A null data buffer asks Windows for the byte size.
|
|
RegQueryValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
null(),
|
|
&mut value_type,
|
|
null_mut(),
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
match status {
|
|
ERROR_SUCCESS => {}
|
|
ERROR_FILE_NOT_FOUND => return Ok(None),
|
|
status => return Err(windows_error(status)),
|
|
}
|
|
|
|
if value_type != REG_SZ {
|
|
return Ok(None);
|
|
}
|
|
if byte_len == 0 {
|
|
return Ok(Some(String::new()));
|
|
}
|
|
|
|
let mut buffer = vec![0_u16; byte_len.div_ceil(2) as usize];
|
|
let status = unsafe {
|
|
// SAFETY: buffer is valid for byte_len bytes and name remains NUL-terminated.
|
|
RegQueryValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
null(),
|
|
&mut value_type,
|
|
buffer.as_mut_ptr().cast::<u8>(),
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
windows_status(status)?;
|
|
let nul = buffer
|
|
.iter()
|
|
.position(|value| *value == 0)
|
|
.unwrap_or(buffer.len());
|
|
buffer.truncate(nul);
|
|
|
|
Ok(Some(String::from_utf16_lossy(&buffer)))
|
|
}
|
|
|
|
fn set_string(&self, name: &str, value: &str) -> io::Result<()> {
|
|
let name = wide_null(name);
|
|
let value = wide_null(value);
|
|
let status = unsafe {
|
|
// SAFETY: name and value are NUL-terminated UTF-16 buffers. REG_SZ data length is
|
|
// measured in bytes and includes the trailing NUL.
|
|
RegSetValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
0,
|
|
REG_SZ,
|
|
value.as_ptr().cast::<u8>(),
|
|
(value.len() * std::mem::size_of::<u16>()) as u32,
|
|
)
|
|
};
|
|
windows_status(status)
|
|
}
|
|
}
|
|
|
|
impl Drop for RegKey {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
// SAFETY: self.0 is a valid open registry key owned by this value.
|
|
RegCloseKey(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn windows_status(status: u32) -> io::Result<()> {
|
|
if status == ERROR_SUCCESS {
|
|
Ok(())
|
|
} else {
|
|
Err(windows_error(status))
|
|
}
|
|
}
|
|
|
|
fn windows_error(status: u32) -> io::Error {
|
|
io::Error::from_raw_os_error(status as i32)
|
|
}
|
|
|
|
fn wide_null(value: &str) -> Vec<u16> {
|
|
value.encode_utf16().chain(std::iter::once(0)).collect()
|
|
}
|