feat(client): add TAP-Windows adapter crate
The Windows client still needs a real Ethernet TAP boundary before it can pump
frames between the adapter and the relay session. Keep that OS-specific surface
separate from the QUIC client state by adding a `lanparty-client-tap` crate.
The new crate discovers TAP-Windows6 adapters through the Windows network
adapter registry class, validates `tap0901` component ids, constructs the
`\\.\Global\{NetCfgInstanceId}.tap` device path, opens the TAP device handle,
and exposes blocking Ethernet frame read/write helpers. It also wraps the
TAP-Windows IOCTLs for media status, driver MAC, and driver MTU.
This does not wire the TAP crate into `lanparty-client-win` yet and does not
attempt route protection. The value of this slice is the target-checkable OS
boundary that the next client pump can depend on.
Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- Windows-target cargo check for lanparty-client-tap with clang-cl/lld-link
- Windows-target cargo clippy for lanparty-client-tap with -D warnings
- git diff --check
Refs: PLAN.md Windows TAP client
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! 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::{Result, bail};
|
||||
|
||||
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";
|
||||
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}")
|
||||
}
|
||||
|
||||
#[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_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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user