feat(client): validate TAP Ethernet frame I/O

The future client pump should exchange Ethernet frames with TAP through a narrow
API, not raw byte reads and writes. Add TAP frame validation at the adapter
boundary so malformed or jumbo frames are rejected before they enter the relay
path.

Expose `TAP_FRAME_BUFFER_LEN` and `validate_tap_ethernet_frame`, then add
Windows helpers that read a TAP frame and validate it, or validate and fully
write an Ethernet frame. Raw read/write methods remain available for lower-level
adapter work.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- Windows-target cargo clippy for lanparty-client-tap with -D warnings
- git diff --check

Refs: PLAN.md one TAP Ethernet frame per datagram
This commit is contained in:
2026-05-21 18:55:08 +02:00
parent c5fc13d892
commit 70fb23b538
2 changed files with 46 additions and 3 deletions
+27 -1
View File
@@ -4,13 +4,15 @@
//! how to find and open an installed TAP-Windows6 Ethernet adapter; the Windows //! 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. //! client binary owns when to connect it to QUIC and how to protect routes.
use anyhow::{Result, bail}; use anyhow::{Context, Result, bail};
use lanparty_proto::{EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN};
pub const TAP_COMPONENT_ID: &str = "tap0901"; pub const TAP_COMPONENT_ID: &str = "tap0901";
pub const TAP_ADAPTER_KEY: &str = pub const TAP_ADAPTER_KEY: &str =
r"SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}"; r"SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}";
pub const TAP_DEVICE_PREFIX: &str = r"\\.\Global\"; pub const TAP_DEVICE_PREFIX: &str = r"\\.\Global\";
pub const TAP_DEVICE_SUFFIX: &str = ".tap"; pub const TAP_DEVICE_SUFFIX: &str = ".tap";
pub const TAP_FRAME_BUFFER_LEN: usize = MAX_STANDARD_ETHERNET_FRAME_LEN;
const FILE_DEVICE_UNKNOWN: u32 = 0x0000_0022; const FILE_DEVICE_UNKNOWN: u32 = 0x0000_0022;
const METHOD_BUFFERED: u32 = 0; const METHOD_BUFFERED: u32 = 0;
const FILE_ANY_ACCESS: u32 = 0; const FILE_ANY_ACCESS: u32 = 0;
@@ -70,6 +72,19 @@ pub fn tap_device_path(instance_id: &str) -> String {
format!("{TAP_DEVICE_PREFIX}{instance_id}{TAP_DEVICE_SUFFIX}") format!("{TAP_DEVICE_PREFIX}{instance_id}{TAP_DEVICE_SUFFIX}")
} }
pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> {
let frame = EthernetFrame::parse(frame).context("TAP Ethernet frame is malformed")?;
if frame.is_jumbo() {
bail!(
"TAP Ethernet frame length {} exceeds maximum {}",
frame.len(),
MAX_STANDARD_ETHERNET_FRAME_LEN
);
}
Ok(())
}
#[must_use] #[must_use]
pub const fn tap_control_code(request: u32) -> u32 { pub const fn tap_control_code(request: u32) -> u32 {
(FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (request << 2) | METHOD_BUFFERED (FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (request << 2) | METHOD_BUFFERED
@@ -129,6 +144,17 @@ mod tests {
assert_eq!(tap_ioctl_set_media_status(), 0x0022_0018); assert_eq!(tap_ioctl_set_media_status(), 0x0022_0018);
} }
#[test]
fn validates_tap_ethernet_frames() {
let mut valid = vec![0; 14];
valid[12..14].copy_from_slice(&0x0800_u16.to_be_bytes());
assert!(validate_tap_ethernet_frame(&valid).is_ok());
assert!(validate_tap_ethernet_frame(&valid[..13]).is_err());
valid.resize(TAP_FRAME_BUFFER_LEN + 1, 0);
assert!(validate_tap_ethernet_frame(&valid).is_err());
}
#[test] #[test]
fn validates_adapter_info() { fn validates_adapter_info() {
let info = let info =
+19 -2
View File
@@ -4,7 +4,7 @@ use std::{
ptr::{null, null_mut}, ptr::{null, null_mut},
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result, bail};
use lanparty_proto::MacAddr; use lanparty_proto::MacAddr;
use windows_sys::Win32::{ use windows_sys::Win32::{
Foundation::{ Foundation::{
@@ -26,7 +26,7 @@ use windows_sys::Win32::{
use crate::{ use crate::{
TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu, TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu,
tap_ioctl_set_media_status, tap_ioctl_set_media_status, validate_tap_ethernet_frame,
}; };
#[derive(Debug)] #[derive(Debug)]
@@ -128,6 +128,13 @@ impl TapAdapter {
Ok(bytes_read as usize) Ok(bytes_read as usize)
} }
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)
}
pub fn write_frame(&self, frame: &[u8]) -> Result<usize> { pub fn write_frame(&self, frame: &[u8]) -> Result<usize> {
let mut bytes_written = 0_u32; let mut bytes_written = 0_u32;
let ok = unsafe { let ok = unsafe {
@@ -148,6 +155,16 @@ impl TapAdapter {
Ok(bytes_written as usize) Ok(bytes_written as usize)
} }
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( fn device_io_control(
&self, &self,
code: u32, code: u32,