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
This commit is contained in:
@@ -74,6 +74,7 @@ Windows route-table boundary:
|
|||||||
Windows TAP adapter boundary:
|
Windows TAP adapter boundary:
|
||||||
|
|
||||||
- TAP-Windows6 adapter discovery from the Windows network adapter registry
|
- 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
|
- `\\.\Global\{NetCfgInstanceId}.tap` device path construction
|
||||||
- blocking Ethernet frame reads/writes through the TAP device handle
|
- blocking Ethernet frame reads/writes through the TAP device handle
|
||||||
- TAP driver IOCTL helpers for media status, adapter MAC, and MTU
|
- 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,
|
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
|
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
|
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
|
The startup status reports whether the relay already has a LAN gateway for the
|
||||||
room.
|
room.
|
||||||
`--virtual-mac` can still override the stored identity for manual testing. On
|
`--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
|
along with the TAP interface index/LUID. The client applies a scoped TAP
|
||||||
interface metric and disables TAP default routes while it runs, periodically
|
interface metric and disables TAP default routes while it runs, periodically
|
||||||
rechecks that the relay route remains pinned, then restores the previous route
|
rechecks that the relay route remains pinned, then restores the previous route
|
||||||
policy on exit. Until automatic TAP MAC configuration is wired, startup fails
|
policy on exit. Startup still fails before bridging if the driver-reported MAC
|
||||||
before bridging if the driver-reported MAC does not match the tunnel identity.
|
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,
|
It prints and reports client diagnostics snapshots with relay reachability,
|
||||||
route-pinning, QUIC datagram budget, TAP status/IP, frame/datagram counters,
|
route-pinning, QUIC datagram budget, TAP status/IP, frame/datagram counters,
|
||||||
and drops.
|
and drops.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! client binary owns when to connect it to QUIC and how to protect routes.
|
//! client binary owns when to connect it to QUIC and how to protect routes.
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
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_COMPONENT_ID: &str = "tap0901";
|
||||||
pub const TAP_ADAPTER_KEY: &str =
|
pub const TAP_ADAPTER_KEY: &str =
|
||||||
@@ -24,10 +24,33 @@ const TAP_IOCTL_SET_MEDIA_STATUS_REQUEST: u32 = 6;
|
|||||||
pub struct TapAdapterInfo {
|
pub struct TapAdapterInfo {
|
||||||
instance_id: String,
|
instance_id: String,
|
||||||
component_id: String,
|
component_id: String,
|
||||||
|
driver_key_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TapAdapterInfo {
|
impl TapAdapterInfo {
|
||||||
pub fn new(instance_id: impl Into<String>, component_id: impl Into<String>) -> Result<Self> {
|
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();
|
let instance_id = instance_id.into();
|
||||||
if instance_id.trim().is_empty() {
|
if instance_id.trim().is_empty() {
|
||||||
bail!("TAP adapter instance id cannot be empty");
|
bail!("TAP adapter instance id cannot be empty");
|
||||||
@@ -41,6 +64,7 @@ impl TapAdapterInfo {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
instance_id,
|
instance_id,
|
||||||
component_id,
|
component_id,
|
||||||
|
driver_key_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +78,11 @@ impl TapAdapterInfo {
|
|||||||
&self.component_id
|
&self.component_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn driver_key_name(&self) -> Option<&str> {
|
||||||
|
self.driver_key_name.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn device_path(&self) -> String {
|
pub fn device_path(&self) -> String {
|
||||||
tap_device_path(&self.instance_id)
|
tap_device_path(&self.instance_id)
|
||||||
@@ -85,6 +114,15 @@ pub fn validate_tap_ethernet_frame(frame: &[u8]) -> Result<()> {
|
|||||||
Ok(())
|
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]
|
#[must_use]
|
||||||
pub const fn tap_control_code(request: u32) -> u32 {
|
pub const fn tap_control_code(request: u32) -> u32 {
|
||||||
(FILE_DEVICE_UNKNOWN << 16) | (FILE_ANY_ACCESS << 14) | (request << 2) | METHOD_BUFFERED
|
(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;
|
mod windows;
|
||||||
|
|
||||||
#[cfg(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))]
|
#[cfg(not(windows))]
|
||||||
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||||
@@ -161,6 +199,7 @@ mod tests {
|
|||||||
TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "tap0901").unwrap();
|
TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "tap0901").unwrap();
|
||||||
|
|
||||||
assert_eq!(info.component_id(), "tap0901");
|
assert_eq!(info.component_id(), "tap0901");
|
||||||
|
assert_eq!(info.driver_key_name(), None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
info.device_path(),
|
info.device_path(),
|
||||||
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
|
r"\\.\Global\{01234567-89AB-CDEF-0123-456789ABCDEF}.tap"
|
||||||
@@ -168,4 +207,14 @@ mod tests {
|
|||||||
assert!(TapAdapterInfo::new("", "tap0901").is_err());
|
assert!(TapAdapterInfo::new("", "tap0901").is_err());
|
||||||
assert!(TapAdapterInfo::new("{01234567-89AB-CDEF-0123-456789ABCDEF}", "wintun").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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ use windows_sys::Win32::{
|
|||||||
System::{
|
System::{
|
||||||
IO::DeviceIoControl,
|
IO::DeviceIoControl,
|
||||||
Registry::{
|
Registry::{
|
||||||
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, REG_SZ, RegCloseKey, RegEnumKeyExW, RegOpenKeyExW,
|
HKEY, HKEY_LOCAL_MACHINE, KEY_READ, KEY_SET_VALUE, REG_SZ, RegCloseKey, RegEnumKeyExW,
|
||||||
RegQueryValueExW,
|
RegOpenKeyExW, RegQueryValueExW, RegSetValueExW,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
TAP_ADAPTER_KEY, TapAdapterInfo, is_tap_component_id, tap_ioctl_get_mac, tap_ioctl_get_mtu,
|
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)]
|
#[derive(Debug)]
|
||||||
@@ -206,15 +206,28 @@ pub fn open_first_adapter() -> Result<TapAdapter> {
|
|||||||
TapAdapter::open(info)
|
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>> {
|
pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
||||||
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
let adapters_key = RegKey::open(HKEY_LOCAL_MACHINE, TAP_ADAPTER_KEY)
|
||||||
.context("failed to open TAP adapter registry key")?;
|
.context("failed to open TAP adapter registry key")?;
|
||||||
let mut adapters = Vec::new();
|
let mut adapters = Vec::new();
|
||||||
|
|
||||||
for subkey in adapters_key.subkey_names()? {
|
for subkey_name in adapters_key.subkey_names()? {
|
||||||
let subkey = adapters_key
|
let subkey = adapters_key
|
||||||
.open_subkey(&subkey)
|
.open_subkey(&subkey_name)
|
||||||
.with_context(|| format!("failed to open TAP adapter registry subkey {subkey}"))?;
|
.with_context(|| format!("failed to open TAP adapter registry subkey {subkey_name}"))?;
|
||||||
let Some(component_id) = subkey.query_string("ComponentId")? else {
|
let Some(component_id) = subkey.query_string("ComponentId")? else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -225,7 +238,11 @@ pub fn available_adapters() -> Result<Vec<TapAdapterInfo>> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
adapters.push(TapAdapterInfo::new(instance_id, component_id)?);
|
adapters.push(TapAdapterInfo::from_registry(
|
||||||
|
subkey_name,
|
||||||
|
instance_id,
|
||||||
|
component_id,
|
||||||
|
)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(adapters)
|
Ok(adapters)
|
||||||
@@ -272,11 +289,15 @@ struct RegKey(HKEY);
|
|||||||
|
|
||||||
impl RegKey {
|
impl RegKey {
|
||||||
fn open(root: HKEY, path: &str) -> io::Result<Self> {
|
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 path = wide_null(path);
|
||||||
let mut key = null_mut();
|
let mut key = null_mut();
|
||||||
let status = unsafe {
|
let status = unsafe {
|
||||||
// SAFETY: path is NUL-terminated and phkresult points to valid storage.
|
// 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)?;
|
windows_status(status)?;
|
||||||
|
|
||||||
@@ -375,6 +396,24 @@ impl RegKey {
|
|||||||
|
|
||||||
Ok(Some(String::from_utf16_lossy(&buffer)))
|
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 {
|
impl Drop for RegKey {
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ struct TapRouteProtectionGuard {
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
|
fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
|
||||||
let tap = lanparty_client_tap::open_first_adapter()?;
|
let tap = open_configured_tap_adapter(session.config().virtual_mac())?;
|
||||||
let tap_interface =
|
let tap_interface =
|
||||||
lanparty_client_route::interface_identity_from_guid(tap.info().instance_id())
|
lanparty_client_route::interface_identity_from_guid(tap.info().instance_id())
|
||||||
.context("failed to resolve TAP interface identity")?;
|
.context("failed to resolve TAP interface identity")?;
|
||||||
@@ -368,6 +368,23 @@ fn open_tap_adapter(session: &ClientSession) -> Result<OpenedTapAdapter> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn open_configured_tap_adapter(virtual_mac: MacAddr) -> Result<TapAdapter> {
|
||||||
|
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(
|
fn client_diagnostics_snapshot(
|
||||||
session: &ClientSession,
|
session: &ClientSession,
|
||||||
route_pinned: bool,
|
route_pinned: bool,
|
||||||
@@ -533,7 +550,7 @@ fn preferred_tap_ip(
|
|||||||
fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> {
|
fn validate_tap_driver_mac(expected_mac: MacAddr, driver_mac: MacAddr) -> Result<()> {
|
||||||
if driver_mac != expected_mac {
|
if driver_mac != expected_mac {
|
||||||
bail!(
|
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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user