diff --git a/README.md b/README.md index ecba2fd..3986cf9 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Windows route-table boundary: Windows TAP adapter boundary: - TAP-Windows6 adapter discovery from the Windows network adapter registry +- TAP `NetworkAddress` registry configuration for the tunnel MAC identity - `\\.\Global\{NetCfgInstanceId}.tap` device path construction - blocking Ethernet frame reads/writes through the TAP device handle - TAP driver IOCTL helpers for media status, adapter MAC, and MTU @@ -162,7 +163,8 @@ with a generated locally administered virtual MAC persisted in handshake, pins a host route for the relay IP on the current pre-TAP interface, verifies that the relay route still uses that pinned host route after TAP activation, and then bridges Ethernet frames between the relay and the first -TAP-Windows6 adapter until shutdown. +TAP-Windows6 adapter until shutdown. Before opening the adapter, it writes the +generated tunnel MAC to the TAP driver's `NetworkAddress` registry setting. The startup status reports whether the relay already has a LAN gateway for the room. `--virtual-mac` can still override the stored identity for manual testing. On @@ -171,8 +173,10 @@ TAP media connected, and reports the driver MAC/MTU before forwarding frames, along with the TAP interface index/LUID. The client applies a scoped TAP interface metric and disables TAP default routes while it runs, periodically rechecks that the relay route remains pinned, then restores the previous route -policy on exit. Until automatic TAP MAC configuration is wired, startup fails -before bridging if the driver-reported MAC does not match the tunnel identity. +policy on exit. Startup still fails before bridging if the driver-reported MAC +does not match the tunnel identity, because an already-initialized Windows TAP +adapter may need to be disabled/enabled or reinstalled before it reloads the +configured `NetworkAddress`. It prints and reports client diagnostics snapshots with relay reachability, route-pinning, QUIC datagram budget, TAP status/IP, frame/datagram counters, and drops. diff --git a/crates/lanparty-client-tap/src/lib.rs b/crates/lanparty-client-tap/src/lib.rs index a617dc9..60db060 100644 --- a/crates/lanparty-client-tap/src/lib.rs +++ b/crates/lanparty-client-tap/src/lib.rs @@ -5,7 +5,7 @@ //! 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}; +use lanparty_proto::{EthernetFrame, MAX_STANDARD_ETHERNET_FRAME_LEN, MacAddr}; pub const TAP_COMPONENT_ID: &str = "tap0901"; pub const TAP_ADAPTER_KEY: &str = @@ -24,10 +24,33 @@ const TAP_IOCTL_SET_MEDIA_STATUS_REQUEST: u32 = 6; pub struct TapAdapterInfo { instance_id: String, component_id: String, + driver_key_name: Option, } impl TapAdapterInfo { pub fn new(instance_id: impl Into, component_id: impl Into) -> Result { + Self::from_parts(instance_id, component_id, None) + } + + #[cfg_attr(not(windows), allow(dead_code))] + fn from_registry( + driver_key_name: impl Into, + instance_id: impl Into, + component_id: impl Into, + ) -> Result { + 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, + component_id: impl Into, + driver_key_name: Option, + ) -> Result { let instance_id = instance_id.into(); if instance_id.trim().is_empty() { bail!("TAP adapter instance id cannot be empty"); @@ -41,6 +64,7 @@ impl TapAdapterInfo { Ok(Self { instance_id, component_id, + driver_key_name, }) } @@ -54,6 +78,11 @@ impl TapAdapterInfo { &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) @@ -85,6 +114,15 @@ pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> { Ok(()) } +pub fn tap_network_address_value(mac: MacAddr) -> Result { + 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 @@ -109,7 +147,7 @@ pub const fn tap_ioctl_set_media_status() -> u32 { mod windows; #[cfg(windows)] -pub use windows::{TapAdapter, available_adapters, open_first_adapter}; +pub use windows::{TapAdapter, available_adapters, configure_adapter_mac, open_first_adapter}; #[cfg(not(windows))] pub fn available_adapters() -> Result> { @@ -161,6 +199,7 @@ mod tests { 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" @@ -168,4 +207,14 @@ mod tests { 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()); + } } diff --git a/crates/lanparty-client-tap/src/windows.rs b/crates/lanparty-client-tap/src/windows.rs index 8569cf7..e75c045 100644 --- a/crates/lanparty-client-tap/src/windows.rs +++ b/crates/lanparty-client-tap/src/windows.rs @@ -18,15 +18,15 @@ use windows_sys::Win32::{ System::{ IO::DeviceIoControl, Registry::{ - HKEY, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, RegCloseKey, RegEnumKeyExW, RegOpenKeyExW, - RegQueryValueExW, + 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, validate_tap_ethernet_frame, + tap_ioctl_set_media_status, tap_network_address_value, validate_tap_ethernet_frame, }; #[derive(Debug)] @@ -206,15 +206,28 @@ pub fn open_first_adapter() -> Result { 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> { 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 in adapters_key.subkey_names()? { + for subkey_name in adapters_key.subkey_names()? { let subkey = adapters_key - .open_subkey(&subkey) - .with_context(|| format!("failed to open TAP adapter registry subkey {subkey}"))?; + .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; }; @@ -225,7 +238,11 @@ pub fn available_adapters() -> Result> { continue; }; - adapters.push(TapAdapterInfo::new(instance_id, component_id)?); + adapters.push(TapAdapterInfo::from_registry( + subkey_name, + instance_id, + component_id, + )?); } Ok(adapters) @@ -272,11 +289,15 @@ struct RegKey(HKEY); impl RegKey { fn open(root: HKEY, path: &str) -> io::Result { + Self::open_with_access(root, path, KEY_READ) + } + + fn open_with_access(root: HKEY, path: &str, access: u32) -> io::Result { 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, KEY_READ, &mut key) + RegOpenKeyExW(root, path.as_ptr(), 0, access, &mut key) }; windows_status(status)?; @@ -375,6 +396,24 @@ impl RegKey { 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::(), + (value.len() * std::mem::size_of::()) as u32, + ) + }; + windows_status(status) + } } impl Drop for RegKey { diff --git a/crates/lanparty-client-win/src/main.rs b/crates/lanparty-client-win/src/main.rs index 4421b08..09c8e70 100644 --- a/crates/lanparty-client-win/src/main.rs +++ b/crates/lanparty-client-win/src/main.rs @@ -328,7 +328,7 @@ struct TapRouteProtectionGuard { #[cfg(windows)] fn open_tap_adapter(session: &ClientSession) -> Result { - let tap = lanparty_client_tap::open_first_adapter()?; + let tap = open_configured_tap_adapter(session.config().virtual_mac())?; let tap_interface = lanparty_client_route::interface_identity_from_guid(tap.info().instance_id()) .context("failed to resolve TAP interface identity")?; @@ -368,6 +368,23 @@ fn open_tap_adapter(session: &ClientSession) -> Result { }) } +#[cfg(windows)] +fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result { + let mut adapters = lanparty_client_tap::available_adapters()?; + let info = adapters + .drain(..) + .next() + .context("no TAP-Windows6 adapters found")?; + lanparty_client_tap::configure_adapter_mac(&info, virtual_mac).with_context(|| { + format!( + "failed to persist TAP MAC {virtual_mac} for adapter {}", + info.instance_id() + ) + })?; + + TapAdapter::open(info) +} + fn client_diagnostics_snapshot( session: &ClientSession, route_pinned: bool, @@ -533,7 +550,7 @@ fn preferred_tap_ip( fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> { if driver_mac != expected_mac { bail!( - "TAP driver MAC {driver_mac} does not match tunnel identity {expected_mac}; automatic MAC configuration is not wired yet" + "TAP driver MAC {driver_mac} does not match tunnel identity {expected_mac}; the NetworkAddress registry value was written before opening the adapter, but Windows may need the TAP adapter disabled/enabled or reinstalled before the driver reloads it" ); }