Files
softlan-vpn/crates/lanparty-client-tap/src/lib.rs
T
ddidderr 70fb23b538 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
2026-05-21 18:55:08 +02:00

172 lines
5.1 KiB
Rust

//! TAP-Windows6 adapter discovery and raw Ethernet frame I/O.
//!
//! 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.
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;
const TAP_IOCTL_GET_MAC_REQUEST: u32 = 1;
const TAP_IOCTL_GET_MTU_REQUEST: u32 = 3;
const TAP_IOCTL_SET_MEDIA_STATUS_REQUEST: u32 = 6;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TapAdapterInfo {
instance_id: String,
component_id: String,
}
impl TapAdapterInfo {
pub fn new(instance_id: impl Into<String>, component_id: impl Into<String>) -> Result<Self> {
let instance_id = instance_id.into();
if instance_id.trim().is_empty() {
bail!("TAP adapter instance id cannot be empty");
}
let component_id = component_id.into();
if !is_tap_component_id(&component_id) {
bail!("unsupported TAP adapter component id {component_id:?}");
}
Ok(Self {
instance_id,
component_id,
})
}
#[must_use]
pub fn instance_id(&self) -> &str {
&self.instance_id
}
#[must_use]
pub fn component_id(&self) -> &str {
&self.component_id
}
#[must_use]
pub fn device_path(&self) -> String {
tap_device_path(&self.instance_id)
}
}
#[must_use]
pub fn is_tap_component_id(component_id: &str) -> bool {
let id = component_id.trim();
id.eq_ignore_ascii_case(TAP_COMPONENT_ID)
|| id.eq_ignore_ascii_case(concat!("root\\", "tap0901"))
}
#[must_use]
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
}
#[must_use]
pub const fn tap_ioctl_get_mac() -> u32 {
tap_control_code(TAP_IOCTL_GET_MAC_REQUEST)
}
#[must_use]
pub const fn tap_ioctl_get_mtu() -> u32 {
tap_control_code(TAP_IOCTL_GET_MTU_REQUEST)
}
#[must_use]
pub const fn tap_ioctl_set_media_status() -> u32 {
tap_control_code(TAP_IOCTL_SET_MEDIA_STATUS_REQUEST)
}
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use windows::{TapAdapter, available_adapters, open_first_adapter};
#[cfg(not(windows))]
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
bail!("TAP-Windows6 adapter discovery is only available on Windows");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifies_tap_windows_component_ids() {
assert!(is_tap_component_id("tap0901"));
assert!(is_tap_component_id("TAP0901"));
assert!(is_tap_component_id("root\\tap0901"));
assert!(!is_tap_component_id("wintun"));
assert!(!is_tap_component_id("tap0801"));
}
#[test]
fn builds_tap_device_path() {
assert_eq!(
tap_device_path("{01234567-89AB-CDEF-0123-456789ABCDEF}"),
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
);
}
#[test]
fn computes_tap_ioctl_codes() {
assert_eq!(tap_ioctl_get_mac(), 0x0022_0004);
assert_eq!(tap_ioctl_get_mtu(), 0x0022_000c);
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 =
TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "tap0901").unwrap();
assert_eq!(info.component_id(), "tap0901");
assert_eq!(
info.device_path(),
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
);
assert!(TapAdapterInfo::new("", "tap0901").is_err());
assert!(TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "wintun").is_err());
}
}