From 18f8e41fa187f15c741e81893cf09438dfe61ace Mon Sep 17 00:00:00 2001 From: Tangweirui2021 <85839291+Tangweirui2021@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:18:31 +0800 Subject: [PATCH] fix(windows): handle missing registry values during TUN name allocation (#2777) * fix(windows): reuse TUN names with incomplete registry records A leftover Connection key must not force a new adapter identity after EasyTier has reserved the original name. Skip that identity before reading registry values and let rust-tun reopen or recreate the adapter. Keep conflict checks for other GUIDs: Wintun can otherwise rename an unrelated adapter that already uses the candidate name. Share the GUID calculation with VirtualNic so allocation and creation stay consistent. Verified on Windows 11 with concurrent processes, normal and forced exit, missing Name/PnpInstanceID values, and a different-GUID collision. The original reservation tests, Linux workspace Clippy, formatting, and Windows core checks pass. Co-authored-by: Daniel <2116528714@qq.com> --------- Co-authored-by: KKRainbow <5665404+KKRainbow@users.noreply.github.com> --- easytier/src/arch/windows.rs | 2 +- easytier/src/arch/windows/tun_name.rs | 35 +++++++++++++++++++++++---- easytier/src/instance/virtual_nic.rs | 5 +--- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/easytier/src/arch/windows.rs b/easytier/src/arch/windows.rs index b638fcdf..2fb4e1db 100644 --- a/easytier/src/arch/windows.rs +++ b/easytier/src/arch/windows.rs @@ -24,7 +24,7 @@ use windows::{ }; mod tun_name; -pub(crate) use tun_name::reserve_tun_name; +pub(crate) use tun_name::{reserve_tun_name, tun_device_guid}; pub fn disable_connection_reset(socket: &S) -> io::Result<()> { let handle = SOCKET(socket.as_raw_socket() as usize); diff --git a/easytier/src/arch/windows/tun_name.rs b/easytier/src/arch/windows/tun_name.rs index 9ba4d40e..29d2cb26 100644 --- a/easytier/src/arch/windows/tun_name.rs +++ b/easytier/src/arch/windows/tun_name.rs @@ -52,24 +52,49 @@ fn reserve_name(name: &str) -> anyhow::Result> { Ok((!existed).then_some(handle)) } -fn interface_exists(name: &str) -> anyhow::Result { +pub(crate) fn tun_device_guid(name: &str) -> uuid::Uuid { + let identity = format!("https://easytier.rs/wintun/{}", name.to_lowercase()); + uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, identity.as_bytes()) +} + +fn interface_name_conflicts(name: &str) -> anyhow::Result { // IP interface tables can retain removed adapters after process exit. // Check the device tree: disabled devices still occupy their names, but // nonpresent devices left by a terminated process do not. let network = RegKey::predef(HKEY_LOCAL_MACHINE).open_subkey( r"SYSTEM\CurrentControlSet\Control\Network\{4D36E972-E325-11CE-BFC1-08002BE10318}", )?; + let expected_guid = format!("{{{}}}", tun_device_guid(name)); for guid in network.enum_keys() { - let connection = match network.open_subkey(format!(r"{}\Connection", guid?)) { + let guid = guid?; + // The caller holds the name reservation, so another EasyTier instance + // cannot use this identity. Let rust-tun reopen or recreate it even if + // Windows retained an incomplete Connection key. + if guid.eq_ignore_ascii_case(&expected_guid) { + continue; + } + let connection = match network.open_subkey(format!(r"{}\Connection", guid)) { Ok(key) => key, Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue, Err(err) => return Err(err.into()), }; - let existing_name: String = connection.get_value("Name")?; + let existing_name: String = match connection.get_value("Name") { + Ok(name) => name, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue, + Err(err) => return Err(err.into()), + }; if !existing_name.eq_ignore_ascii_case(name) { continue; } - let instance_id: String = connection.get_value("PnpInstanceID")?; + let instance_id: String = match connection.get_value("PnpInstanceID") { + Ok(id) => id, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + // The name exists, but device presence cannot be verified. + // Keep it occupied rather than risk a name collision. + return Ok(true); + } + Err(err) => return Err(err.into()), + }; let mut dev_inst = 0; let result = unsafe { CM_Locate_DevNodeW( @@ -99,7 +124,7 @@ pub(crate) fn reserve_tun_name(requested: &str) -> anyhow::Result<(String, Owned let Some(handle) = reserve_name(&name)? else { continue; }; - if !interface_exists(&name)? { + if !interface_name_conflicts(&name)? { return Ok((name, handle)); } } diff --git a/easytier/src/instance/virtual_nic.rs b/easytier/src/instance/virtual_nic.rs index f37266f7..1fee2ac4 100644 --- a/easytier/src/instance/virtual_nic.rs +++ b/easytier/src/instance/virtual_nic.rs @@ -581,10 +581,7 @@ impl VirtualNic { config.platform_config(|config| { // Wintun uses the GUID for Windows network profile identity. // Include the name so explicitly shared NICs use the same GUID. - let identity = format!("https://easytier.rs/wintun/{}", dev_name.to_lowercase()); - config.device_guid( - uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, identity.as_bytes()).as_u128(), - ); + config.device_guid(crate::arch::windows::tun_device_guid(&dev_name).as_u128()); config.skip_config(true); config.ring_cap(Some(std::cmp::min( config.min_ring_cap() * 32,