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
//! 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_ADAPTER_KEY: &str =
r"SYSTEM\CurrentControlSet\Control\Class\{4D36E972-E325-11CE-BFC1-08002BE10318}";
pub const TAP_DEVICE_PREFIX: &str = r"\\.\Global\";
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 METHOD_BUFFERED: 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}")
}
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]
pub const fn tap_control_code(request: u32) -> u32 {
(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);
}
#[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]
fn validates_adapter_info() {
let info =