2d30f4ed68
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
443 lines
13 KiB
Rust
443 lines
13 KiB
Rust
use std::{
|
|
ffi::c_void,
|
|
io::{self, ErrorKind},
|
|
ptr::{null, null_mut},
|
|
};
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use lanparty_proto::MacAddr;
|
|
use windows_sys::Win32::{
|
|
Foundation::{
|
|
CloseHandle, ERROR_FILE_NOT_FOUND, ERROR_MORE_DATA, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS,
|
|
GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
|
|
},
|
|
Storage::FileSystem::{
|
|
CreateFileW, FILE_ATTRIBUTE_SYSTEM, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
|
|
ReadFile, WriteFile,
|
|
},
|
|
System::{
|
|
IO::DeviceIoControl,
|
|
Registry::{
|
|
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_SET_VALUE, REG_SZ, RegCloseKey, RegEnumKeyExW,
|
|
RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
|
|
},
|
|
},
|
|
};
|
|
|
|
use crate::{
|
|
TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu,
|
|
tap_ioctl_set_media_status, tap_network_address_value, validate_tap_ethernet_frame,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
pub struct TapAdapter {
|
|
info: TapAdapterInfo,
|
|
handle: OwnedHandle,
|
|
}
|
|
|
|
impl TapAdapter {
|
|
pub fn open(info: TapAdapterInfo) -> Result<Self> {
|
|
let path = info.device_path();
|
|
let wide_path = wide_null(&path);
|
|
let handle = unsafe {
|
|
// SAFETY: wide_path is NUL-terminated and lives for the duration of the call.
|
|
// The security attributes and template handle are intentionally null.
|
|
CreateFileW(
|
|
wide_path.as_ptr(),
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
|
null(),
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_SYSTEM,
|
|
null_mut(),
|
|
)
|
|
};
|
|
let handle = OwnedHandle::new(handle)
|
|
.with_context(|| format!("failed to open TAP adapter device {path}"))?;
|
|
|
|
Ok(Self { info, handle })
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn info(&self) -> &TapAdapterInfo {
|
|
&self.info
|
|
}
|
|
|
|
pub fn set_media_connected(&self, connected: bool) -> Result<()> {
|
|
let mut status = u32::from(connected);
|
|
self.device_io_control(
|
|
tap_ioctl_set_media_status(),
|
|
(&mut status as *mut u32).cast::<c_void>(),
|
|
std::mem::size_of::<u32>() as u32,
|
|
null_mut(),
|
|
0,
|
|
)
|
|
.context("failed to set TAP media status")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn driver_mac(&self) -> Result<MacAddr> {
|
|
let mut bytes = [0_u8; 6];
|
|
self.device_io_control(
|
|
tap_ioctl_get_mac(),
|
|
null_mut(),
|
|
0,
|
|
bytes.as_mut_ptr().cast::<c_void>(),
|
|
bytes.len() as u32,
|
|
)
|
|
.context("failed to read TAP driver MAC address")?;
|
|
|
|
Ok(MacAddr::new(bytes))
|
|
}
|
|
|
|
pub fn driver_mtu(&self) -> Result<u32> {
|
|
let mut mtu = 0_u32;
|
|
self.device_io_control(
|
|
tap_ioctl_get_mtu(),
|
|
null_mut(),
|
|
0,
|
|
(&mut mtu as *mut u32).cast::<c_void>(),
|
|
std::mem::size_of::<u32>() as u32,
|
|
)
|
|
.context("failed to read TAP driver MTU")?;
|
|
|
|
Ok(mtu)
|
|
}
|
|
|
|
pub fn read_frame(&self, buffer: &mut [u8]) -> Result<usize> {
|
|
let mut bytes_read = 0_u32;
|
|
let ok = unsafe {
|
|
// SAFETY: buffer is valid for writes of buffer.len() bytes and the handle is owned by
|
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
|
ReadFile(
|
|
self.handle.raw(),
|
|
buffer.as_mut_ptr(),
|
|
buffer
|
|
.len()
|
|
.try_into()
|
|
.context("TAP read buffer is too large")?,
|
|
&mut bytes_read,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error()).context("failed to read TAP frame");
|
|
}
|
|
|
|
Ok(bytes_read as usize)
|
|
}
|
|
|
|
pub fn read_ethernet_frame(&self, buffer: &mut [u8]) -> Result<usize> {
|
|
let len = self.read_frame(buffer)?;
|
|
validate_tap_ethernet_frame(&buffer[..len])?;
|
|
|
|
Ok(len)
|
|
}
|
|
|
|
pub fn write_frame(&self, frame: &[u8]) -> Result<usize> {
|
|
let mut bytes_written = 0_u32;
|
|
let ok = unsafe {
|
|
// SAFETY: frame is valid for reads of frame.len() bytes and the handle is owned by
|
|
// this adapter. The synchronous handle uses a null OVERLAPPED pointer.
|
|
WriteFile(
|
|
self.handle.raw(),
|
|
frame.as_ptr(),
|
|
frame.len().try_into().context("TAP frame is too large")?,
|
|
&mut bytes_written,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error()).context("failed to write TAP frame");
|
|
}
|
|
|
|
Ok(bytes_written as usize)
|
|
}
|
|
|
|
pub fn write_ethernet_frame(&self, frame: &[u8]) -> Result<()> {
|
|
validate_tap_ethernet_frame(frame)?;
|
|
let written = self.write_frame(frame)?;
|
|
if written != frame.len() {
|
|
bail!("partial TAP frame write: {written}/{}", frame.len());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn device_io_control(
|
|
&self,
|
|
code: u32,
|
|
input: *mut c_void,
|
|
input_len: u32,
|
|
output: *mut c_void,
|
|
output_len: u32,
|
|
) -> io::Result<u32> {
|
|
let mut bytes_returned = 0_u32;
|
|
let ok = unsafe {
|
|
// SAFETY: input/output pointers and lengths are supplied by the typed public methods
|
|
// above. The synchronous handle uses a null OVERLAPPED pointer.
|
|
DeviceIoControl(
|
|
self.handle.raw(),
|
|
code,
|
|
input.cast_const(),
|
|
input_len,
|
|
output,
|
|
output_len,
|
|
&mut bytes_returned,
|
|
null_mut(),
|
|
)
|
|
};
|
|
if ok == 0 {
|
|
return Err(io::Error::last_os_error());
|
|
}
|
|
|
|
Ok(bytes_returned)
|
|
}
|
|
}
|
|
|
|
pub fn open_first_adapter() -> Result<TapAdapter> {
|
|
let mut adapters = available_adapters()?;
|
|
let info = adapters
|
|
.drain(..)
|
|
.next()
|
|
.context("no TAP-Windows6 adapters found")?;
|
|
|
|
TapAdapter::open(info)
|
|
}
|
|
|
|
pub fn configure_adapter_mac(info: &TapAdapterInfo, mac: MacAddr) -> Result<()> {
|
|
let driver_key_name = info
|
|
.driver_key_name()
|
|
.context("TAP adapter was not discovered from the Windows registry")?;
|
|
let registry_path = format!("{TAP_ADAPTER_KEY}\\{driver_key_name}");
|
|
let key = RegKey::open_with_access(HKEY_LOCAL_MACHINE, ®istry_path, KEY_SET_VALUE)
|
|
.with_context(|| format!("failed to open TAP adapter registry key {registry_path}"))?;
|
|
key.set_string("NetworkAddress", &tap_network_address_value(mac)?)
|
|
.context("failed to configure TAP adapter NetworkAddress")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
|
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
|
.context("failed to open TAP adapter registry key")?;
|
|
let mut adapters = Vec::new();
|
|
|
|
for subkey_name in adapters_key.subkey_names()? {
|
|
let subkey = adapters_key
|
|
.open_subkey(&subkey_name)
|
|
.with_context(|| format!("failed to open TAP adapter registry subkey {subkey_name}"))?;
|
|
let Some(component_id) = subkey.query_string("ComponentId")? else {
|
|
continue;
|
|
};
|
|
if !is_tap_component_id(&component_id) {
|
|
continue;
|
|
}
|
|
let Some(instance_id) = subkey.query_string("NetCfgInstanceId")? else {
|
|
continue;
|
|
};
|
|
|
|
adapters.push(TapAdapterInfo::from_registry(
|
|
subkey_name,
|
|
instance_id,
|
|
component_id,
|
|
)?);
|
|
}
|
|
|
|
Ok(adapters)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct OwnedHandle(HANDLE);
|
|
|
|
// SAFETY: Windows file handles are process-wide kernel object references that may be used from
|
|
// multiple threads. `OwnedHandle` only closes the handle in `Drop`; callers must still uphold any
|
|
// higher-level synchronization required by the device protocol.
|
|
unsafe impl Send for OwnedHandle {}
|
|
|
|
// SAFETY: Sharing references to `OwnedHandle` only exposes the raw handle to synchronous Windows
|
|
// APIs. The handle value itself is immutable, and Windows permits issuing I/O on a file handle from
|
|
// more than one thread.
|
|
unsafe impl Sync for OwnedHandle {}
|
|
|
|
impl OwnedHandle {
|
|
fn new(handle: HANDLE) -> io::Result<Self> {
|
|
if handle == INVALID_HANDLE_VALUE {
|
|
Err(io::Error::last_os_error())
|
|
} else {
|
|
Ok(Self(handle))
|
|
}
|
|
}
|
|
|
|
const fn raw(&self) -> HANDLE {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Drop for OwnedHandle {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
// SAFETY: self.0 is a valid owned HANDLE created by CreateFileW.
|
|
CloseHandle(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct RegKey(HKEY);
|
|
|
|
impl RegKey {
|
|
fn open(root: HKEY, path: &str) -> io::Result<Self> {
|
|
Self::open_with_access(root, path, KEY_READ)
|
|
}
|
|
|
|
fn open_with_access(root: HKEY, path: &str, access: u32) -> io::Result<Self> {
|
|
let path = wide_null(path);
|
|
let mut key = null_mut();
|
|
let status = unsafe {
|
|
// SAFETY: path is NUL-terminated and phkresult points to valid storage.
|
|
RegOpenKeyExW(root, path.as_ptr(), 0, access, &mut key)
|
|
};
|
|
windows_status(status)?;
|
|
|
|
Ok(Self(key))
|
|
}
|
|
|
|
fn open_subkey(&self, path: &str) -> io::Result<Self> {
|
|
Self::open(self.0, path)
|
|
}
|
|
|
|
fn subkey_names(&self) -> io::Result<Vec<String>> {
|
|
let mut names = Vec::new();
|
|
let mut index = 0_u32;
|
|
|
|
loop {
|
|
let mut buffer = vec![0_u16; 256];
|
|
let mut len = buffer.len() as u32;
|
|
let status = unsafe {
|
|
// SAFETY: buffer is valid for len UTF-16 code units and len points to storage
|
|
// that RegEnumKeyExW updates with the returned name length.
|
|
RegEnumKeyExW(
|
|
self.0,
|
|
index,
|
|
buffer.as_mut_ptr(),
|
|
&mut len,
|
|
null(),
|
|
null_mut(),
|
|
null_mut(),
|
|
null_mut(),
|
|
)
|
|
};
|
|
match status {
|
|
ERROR_SUCCESS => {
|
|
buffer.truncate(len as usize);
|
|
names.push(String::from_utf16_lossy(&buffer));
|
|
index += 1;
|
|
}
|
|
ERROR_NO_MORE_ITEMS => return Ok(names),
|
|
ERROR_MORE_DATA => {
|
|
return Err(io::Error::new(
|
|
ErrorKind::InvalidData,
|
|
"registry subkey name exceeded internal buffer",
|
|
));
|
|
}
|
|
status => return Err(windows_error(status)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn query_string(&self, name: &str) -> io::Result<Option<String>> {
|
|
let name = wide_null(name);
|
|
let mut value_type = 0_u32;
|
|
let mut byte_len = 0_u32;
|
|
let status = unsafe {
|
|
// SAFETY: name is NUL-terminated. A null data buffer asks Windows for the byte size.
|
|
RegQueryValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
null(),
|
|
&mut value_type,
|
|
null_mut(),
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
match status {
|
|
ERROR_SUCCESS => {}
|
|
ERROR_FILE_NOT_FOUND => return Ok(None),
|
|
status => return Err(windows_error(status)),
|
|
}
|
|
|
|
if value_type != REG_SZ {
|
|
return Ok(None);
|
|
}
|
|
if byte_len == 0 {
|
|
return Ok(Some(String::new()));
|
|
}
|
|
|
|
let mut buffer = vec![0_u16; byte_len.div_ceil(2) as usize];
|
|
let status = unsafe {
|
|
// SAFETY: buffer is valid for byte_len bytes and name remains NUL-terminated.
|
|
RegQueryValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
null(),
|
|
&mut value_type,
|
|
buffer.as_mut_ptr().cast::<u8>(),
|
|
&mut byte_len,
|
|
)
|
|
};
|
|
windows_status(status)?;
|
|
let nul = buffer
|
|
.iter()
|
|
.position(|value| *value == 0)
|
|
.unwrap_or(buffer.len());
|
|
buffer.truncate(nul);
|
|
|
|
Ok(Some(String::from_utf16_lossy(&buffer)))
|
|
}
|
|
|
|
fn set_string(&self, name: &str, value: &str) -> io::Result<()> {
|
|
let name = wide_null(name);
|
|
let value = wide_null(value);
|
|
let status = unsafe {
|
|
// SAFETY: name and value are NUL-terminated UTF-16 buffers. REG_SZ data length is
|
|
// measured in bytes and includes the trailing NUL.
|
|
RegSetValueExW(
|
|
self.0,
|
|
name.as_ptr(),
|
|
0,
|
|
REG_SZ,
|
|
value.as_ptr().cast::<u8>(),
|
|
(value.len() * std::mem::size_of::<u16>()) as u32,
|
|
)
|
|
};
|
|
windows_status(status)
|
|
}
|
|
}
|
|
|
|
impl Drop for RegKey {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
// SAFETY: self.0 is a valid open registry key owned by this value.
|
|
RegCloseKey(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn windows_status(status: u32) -> io::Result<()> {
|
|
if status == ERROR_SUCCESS {
|
|
Ok(())
|
|
} else {
|
|
Err(windows_error(status))
|
|
}
|
|
}
|
|
|
|
fn windows_error(status: u32) -> io::Error {
|
|
io::Error::from_raw_os_error(status as i32)
|
|
}
|
|
|
|
fn wide_null(value: &str) -> Vec<u16> {
|
|
value.encode_utf16().chain(std::iter::once(0)).collect()
|
|
}
|