Files
softlan-vpn/crates/lanparty-client-tap/src/windows.rs
T
ddidderr e62f584377 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
2026-08-16 19:00:54 +02:00

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, &registry_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()
}