70fb23b538
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
172 lines
5.1 KiB
Rust
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());
|
|
}
|
|
}
|