Files
softlan-vpn/crates/lanparty-client-tap/src/lib.rs
T
ddidderr 2d30f4ed68 feat(client): persist TAP MAC identity
The Windows client already generates and announces a stable locally administered
MAC, but it only rejected the TAP adapter when the driver reported a different
address. Persist the tunnel MAC to tap-windows6's NetworkAddress registry value
before opening the adapter so the driver can load the intended current address.

The TAP crate now keeps the driver registry key name from discovery, formats the
NetworkAddress value as the 12-digit hex string expected by NDIS, and rejects
invalid multicast, broadcast, or globally administered MACs before writing.

Runtime validation stays in place. tap-windows6 reads NetworkAddress during
adapter initialization, so an adapter that Windows already initialized with an
old value may still need disable/enable or reinstall on the real Windows test
machine before the GET_MAC ioctl reports the new identity.

Test Plan:
- cargo fmt --check
- cargo test -p lanparty-client-tap
- cargo test -p lanparty-client-win
- cargo clippy -p lanparty-client-tap --all-targets -- -D warnings
- cargo check -p lanparty-client-tap --target x86_64-pc-windows-gnu
- cargo check -p lanparty-client-tap --target x86_64-pc-windows-msvc
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings
- git diff --check

Refs: PLAN.md
2026-05-21 21:21:47 +02:00

221 lines
6.7 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, MacAddr};
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,
driver_key_name: Option<String>,
}
impl TapAdapterInfo {
pub fn new(instance_id: impl Into<String>, component_id: impl Into<String>) -> Result<Self> {
Self::from_parts(instance_id, component_id, None)
}
#[cfg_attr(not(windows), allow(dead_code))]
fn from_registry(
driver_key_name: impl Into<String>,
instance_id: impl Into<String>,
component_id: impl Into<String>,
) -> Result<Self> {
let driver_key_name = driver_key_name.into();
if driver_key_name.trim().is_empty() {
bail!("TAP adapter registry key name cannot be empty");
}
Self::from_parts(instance_id, component_id, Some(driver_key_name))
}
fn from_parts(
instance_id: impl Into<String>,
component_id: impl Into<String>,
driver_key_name: Option<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,
driver_key_name,
})
}
#[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 driver_key_name(&self) -> Option<&str> {
self.driver_key_name.as_deref()
}
#[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(())
}
pub fn tap_network_address_value(mac: MacAddr) -> Result<String> {
if !mac.is_valid_client_identity() {
bail!("TAP MAC {mac} is not a locally administered unicast address");
}
let [a, b, c, d, e, f] = mac.octets();
Ok(format!("{a:02X}{b:02X}{c:02X}{d:02X}{e:02X}{f:02X}"))
}
#[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, configure_adapter_mac, 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.driver_key_name(), None);
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());
}
#[test]
fn formats_tap_network_address_registry_value() {
assert_eq!(
tap_network_address_value(MacAddr::new([0x02, 0xaa, 0xbb, 0xcc, 0xdd, 0xee])).unwrap(),
"02AABBCCDDEE"
);
assert!(tap_network_address_value(MacAddr::BROADCAST).is_err());
assert!(tap_network_address_value(MacAddr::new([0, 1, 2, 3, 4, 5])).is_err());
}
}