mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-14 08:26:14 -08:00
fix(windows): reuse numbered TUN names after complete shutdown (#2694)
* fix(windows): reuse stable TUN identities after complete shutdown Wait for NIC forwarding and packet-drain tasks to release their handles before stopping or replacing the interface. Stop the old static-IP NIC before entering its recreation path. Store typed NIC state so shutdown can await the owned tasks instead of only cancelling them on drop. Use the instance UUID for the default Windows interface name and derive a stable Wintun GUID from the device name. Keep explicit names and their existing sharing behavior, while generated names remain dedicated. Keep resolved names out of the input flags so recreation preserves that choice. Pin rust-tun to the fix that joins its reader during session drop. Remove the global NetworkList cleanup that deleted profiles based only on their names. Stable identities avoid creating a new profile for each restart without guessing which existing records are safe to delete. Add regression tests for receiver release and Windows instance restarts with another dedicated or shared-NIC member still running. Validation: formatting and workspace Clippy pass; 66 NIC, shared-NIC and TUN runtime tests pass in the privileged rust container. Fixes #2682 Related: #110 * fix(windows): allocate the first unused numbered TUN name Use et_1, et_2 and subsequent vacant slots for default Windows NICs. Reserve each name across threads and processes with a named kernel object, retaining it through firewall cleanup and releasing it on exit. Check the current device tree to skip disabled adapters without treating removed adapters in the IP interface tables as occupied slots. Keep GUIDs derived from names and preserve explicit NIC sharing. Cover concurrent allocation, case-insensitive reservations and slot reuse. * fix(windows): release idle TUN names across cached and service owners Release a shared NIC name after its dispatcher has stopped and its firewall rules have been removed, even when the registry caches the NIC. Clear the old interface name so a later cache drop cannot remove rules belonging to its replacement. Await dispatcher teardown on control failure and timeout as well as on successful shutdown. Grant only SYNCHRONIZE access to name reservations across accounts so SYSTEM services and desktop processes can skip occupied slots. Cover idle registry reuse, firewall ownership and failed-control cleanup with regression tests. * test(tun): use the renamed dispatcher task handle on Unix Update the two Unix-only teardown tests to match the task field used by explicit dispatcher shutdown. * fix(windows): report firewall cleanup failures and verify all rules Inspect all eight interface rules, including the ALL protocol pair. Treat only a missing rule as absent and propagate other lookup errors. Attempt every rule deletion and report failures with rule names instead of silently returning success after a permission or service error. Validate rule protocols, directions, profiles, interface binding and allow settings on Windows. Cover repeated add/remove, unprivileged failures, full cleanup and shared-NIC rule ownership across restarts. Pin tun-easytier to merged v2 commit 5ba6be073e5b486b68c12f80fac5cfb50601f024 and update Cargo.lock. Its tree matches the previously tested PR commit.
This commit is contained in:
1 parent
15a3d43039
commit
d25ccd4c17
11 files changed
+613
-190
No files matched your search
Generated
+8
-1
@@ -8756,6 +8756,12 @@ dependencies = [
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -10640,7 +10646,7 @@ checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
[[package]]
|
||||
name = "tun-easytier"
|
||||
version = "1.1.1"
|
||||
source = "git+https://github.com/EasyTier/rust-tun#12378839e7985283df0e4fb536b7137230356db5"
|
||||
source = "git+https://github.com/EasyTier/rust-tun?rev=5ba6be073e5b486b68c12f80fac5cfb50601f024#5ba6be073e5b486b68c12f80fac5cfb50601f024"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"cfg-if",
|
||||
@@ -10881,6 +10887,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"rand 0.10.2",
|
||||
"serde_core",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
||||
+6
-1
@@ -113,7 +113,7 @@ rcgen = { version = "0.14.10", optional = true }
|
||||
tokio-rustls = { workspace = true, optional = true }
|
||||
|
||||
# for tap device
|
||||
tun = { package = "tun-easytier", git = "https://github.com/EasyTier/rust-tun", features = [
|
||||
tun = { package = "tun-easytier", git = "https://github.com/EasyTier/rust-tun", rev = "5ba6be073e5b486b68c12f80fac5cfb50601f024", features = [
|
||||
"async",
|
||||
], optional = true }
|
||||
# for net ns
|
||||
@@ -127,6 +127,7 @@ nix = { version = "0.31.3", features = [
|
||||
|
||||
uuid = { workspace = true, features = [
|
||||
"v4",
|
||||
"v5",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"serde",
|
||||
@@ -257,6 +258,10 @@ windows = { workspace = true, features = [
|
||||
"Win32_System_Variant",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Authorization",
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
] }
|
||||
encoding = "0.2"
|
||||
winreg = "0.56"
|
||||
|
||||
+120
-36
@@ -4,7 +4,7 @@ use anyhow::Context;
|
||||
use network_interface::NetworkInterfaceConfig;
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::FALSE,
|
||||
Foundation::{ERROR_FILE_NOT_FOUND, FALSE},
|
||||
NetworkManagement::WindowsFirewall::{
|
||||
INetFwPolicy2, INetFwRule, NET_FW_ACTION_ALLOW, NET_FW_PROFILE2_DOMAIN,
|
||||
NET_FW_PROFILE2_PRIVATE, NET_FW_PROFILE2_PUBLIC, NET_FW_RULE_DIR_IN,
|
||||
@@ -23,6 +23,9 @@ use windows::{
|
||||
core::{BOOL, BSTR},
|
||||
};
|
||||
|
||||
mod tun_name;
|
||||
pub(crate) use tun_name::reserve_tun_name;
|
||||
|
||||
pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
|
||||
let handle = SOCKET(socket.as_raw_socket() as usize);
|
||||
|
||||
@@ -61,16 +64,6 @@ pub fn disable_connection_reset<S: AsRawSocket>(socket: &S) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn interface_count() -> io::Result<usize> {
|
||||
let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("Failed to get interfaces. error: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(ifaces.len())
|
||||
}
|
||||
|
||||
pub fn find_interface_index(iface_name: &str) -> io::Result<u32> {
|
||||
let ifaces = network_interface::NetworkInterface::show().map_err(|e| {
|
||||
io::Error::new(
|
||||
@@ -382,6 +375,7 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
|
||||
}?;
|
||||
|
||||
let rules = unsafe { policy.Rules()? };
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for protocol_name in ["TCP", "UDP", "ICMP", "ALL"] {
|
||||
for direction in ["Inbound", "Outbound"] {
|
||||
@@ -390,12 +384,19 @@ pub fn remove_interface_firewall_rules(interface_name: &str) -> anyhow::Result<(
|
||||
interface_name, protocol_name, direction
|
||||
);
|
||||
let name_bstr = BSTR::from(&rule_name);
|
||||
unsafe {
|
||||
let _ = rules.Remove(&name_bstr); // Ignore errors, rule might not exist
|
||||
// Removing a missing rule succeeds. Keep trying the remaining
|
||||
// rules on failure, but report failures to the caller.
|
||||
if let Err(error) = unsafe { rules.Remove(&name_bstr) } {
|
||||
failures.push(format!("{rule_name}: {error}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::ensure!(
|
||||
failures.is_empty(),
|
||||
"failed to remove firewall rules: {}",
|
||||
failures.join("; ")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -415,8 +416,8 @@ pub fn list_interface_firewall_rules(interface_name: &str) -> anyhow::Result<Vec
|
||||
let rules = unsafe { policy.Rules()? };
|
||||
let mut found_rules = Vec::new();
|
||||
|
||||
// Check protocol-specific rules
|
||||
for protocol_name in ["TCP", "UDP", "ICMP"] {
|
||||
// Check protocol-specific and all-protocol rules
|
||||
for protocol_name in ["TCP", "UDP", "ICMP", "ALL"] {
|
||||
for direction in ["Inbound", "Outbound"] {
|
||||
let rule_name = format!(
|
||||
"EasyTier {} - {} Protocol ({})",
|
||||
@@ -428,17 +429,6 @@ pub fn list_interface_firewall_rules(interface_name: &str) -> anyhow::Result<Vec
|
||||
}
|
||||
}
|
||||
|
||||
// Check fallback protocol rules
|
||||
for direction in ["Inbound", "Outbound"] {
|
||||
let rule_name = format!(
|
||||
"EasyTier {} - All Protocols ({})",
|
||||
interface_name, direction
|
||||
);
|
||||
if check_rule_exists(&rules, &rule_name)? {
|
||||
found_rules.push(rule_name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found_rules)
|
||||
}
|
||||
|
||||
@@ -451,7 +441,10 @@ fn check_rule_exists(
|
||||
unsafe {
|
||||
match rules.Item(&name_bstr) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(_) => Ok(false),
|
||||
Err(error) if error.code() == ERROR_FILE_NOT_FOUND.to_hresult() => Ok(false),
|
||||
Err(error) => {
|
||||
Err(error).with_context(|| format!("failed to inspect firewall rule {rule_name}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,10 +459,32 @@ mod tests {
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Requires administrator privileges, ignored by default
|
||||
fn test_interface_firewall_rules() {
|
||||
let test_interface = "test_interface";
|
||||
#[cfg(feature = "tun")]
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Administrator privileges and wintun.dll"]
|
||||
async fn test_interface_firewall_rules() {
|
||||
use crate::{
|
||||
common::netns::NetNS,
|
||||
instance::virtual_nic::{VirtualNic, VirtualNicConfig},
|
||||
};
|
||||
use windows::Win32::System::Ole::{
|
||||
SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound,
|
||||
};
|
||||
|
||||
let test_interface = "et_test_firewall";
|
||||
let mut nic = VirtualNic::new(VirtualNicConfig::new(
|
||||
test_interface.to_owned(),
|
||||
1500,
|
||||
NetNS::new(None),
|
||||
));
|
||||
let _tunnel = nic.create_dev().await.unwrap();
|
||||
guarden::defer! {
|
||||
remove_interface_firewall_rules(test_interface).unwrap();
|
||||
}
|
||||
|
||||
// Removing missing rules is successful, including on repeated cleanup.
|
||||
remove_interface_firewall_rules(test_interface).unwrap();
|
||||
remove_interface_firewall_rules(test_interface).unwrap();
|
||||
|
||||
// Add firewall rules
|
||||
let add_result = add_interface_to_firewall_allowlist(test_interface);
|
||||
@@ -486,17 +501,31 @@ mod tests {
|
||||
|
||||
// Verify rules were created
|
||||
let rules = list_interface_firewall_rules(test_interface).unwrap();
|
||||
assert_eq!(rules.len(), 8);
|
||||
println!("Created {} firewall rules:", rules.len());
|
||||
for rule in &rules {
|
||||
println!(" - {}", rule);
|
||||
}
|
||||
|
||||
// Verify required protocol rules are all created
|
||||
let expected_protocols = ["TCP", "UDP", "ICMP"];
|
||||
let expected_directions = ["Inbound", "Outbound"];
|
||||
let expected_protocols = [("TCP", 6), ("UDP", 17), ("ICMP", 1), ("ALL", 256)];
|
||||
let expected_directions = [
|
||||
("Inbound", NET_FW_RULE_DIR_IN),
|
||||
("Outbound", NET_FW_RULE_DIR_OUT),
|
||||
];
|
||||
let _com = ComInitializer::new().unwrap();
|
||||
let policy: INetFwPolicy2 = unsafe {
|
||||
CoCreateInstance(
|
||||
&windows::Win32::NetworkManagement::WindowsFirewall::NetFwPolicy2,
|
||||
None,
|
||||
CLSCTX_ALL,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let collection = unsafe { policy.Rules().unwrap() };
|
||||
|
||||
for protocol in &expected_protocols {
|
||||
for direction in &expected_directions {
|
||||
for (protocol, number) in expected_protocols {
|
||||
for (direction, value) in expected_directions {
|
||||
let rule_name = format!(
|
||||
"EasyTier {} - {} Protocol ({})",
|
||||
test_interface, protocol, direction
|
||||
@@ -506,10 +535,46 @@ mod tests {
|
||||
"Missing required rule: {}",
|
||||
rule_name
|
||||
);
|
||||
unsafe {
|
||||
let rule = collection.Item(&BSTR::from(&rule_name)).unwrap();
|
||||
assert_eq!(rule.Protocol().unwrap(), number);
|
||||
assert_eq!(rule.Direction().unwrap(), value);
|
||||
assert_eq!(rule.Action().unwrap(), NET_FW_ACTION_ALLOW);
|
||||
assert_eq!(
|
||||
rule.Enabled().unwrap(),
|
||||
windows::Win32::Foundation::VARIANT_TRUE
|
||||
);
|
||||
assert_eq!(rule.Profiles().unwrap(), 7);
|
||||
assert!(rule.ApplicationName().unwrap().is_empty());
|
||||
assert_eq!(rule.LocalAddresses().unwrap().to_string(), "*");
|
||||
assert_eq!(rule.RemoteAddresses().unwrap().to_string(), "*");
|
||||
let interfaces = rule.Interfaces().unwrap();
|
||||
assert_eq!(
|
||||
interfaces.Anonymous.Anonymous.vt,
|
||||
VARENUM(VT_ARRAY.0 | VT_VARIANT.0)
|
||||
);
|
||||
let array = interfaces.Anonymous.Anonymous.Anonymous.parray;
|
||||
let index = SafeArrayGetLBound(array, 1).unwrap();
|
||||
assert_eq!(SafeArrayGetUBound(array, 1).unwrap(), index);
|
||||
let mut interface = VARIANT::default();
|
||||
SafeArrayGetElement(array, &index, &mut interface as *mut _ as *mut _).unwrap();
|
||||
assert_eq!(interface.Anonymous.Anonymous.vt, VT_BSTR);
|
||||
assert_eq!(
|
||||
interface.Anonymous.Anonymous.Anonymous.bstrVal.to_string(),
|
||||
test_interface
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("✓ All required protocol rules (TCP/UDP/ICMP) are present");
|
||||
println!("✓ All eight protocol rules and their properties are correct");
|
||||
|
||||
// Repeated starts retain the same rule names.
|
||||
add_interface_to_firewall_allowlist(test_interface).unwrap();
|
||||
assert_eq!(
|
||||
list_interface_firewall_rules(test_interface).unwrap(),
|
||||
rules
|
||||
);
|
||||
|
||||
// Remove firewall rules
|
||||
let remove_result = remove_interface_firewall_rules(test_interface);
|
||||
@@ -532,4 +597,23 @@ mod tests {
|
||||
test_interface
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a non-administrator token and preinstalled interface rules"]
|
||||
fn firewall_permission_failures_are_reported() {
|
||||
let interface = std::env::var("EASYTIER_TEST_FIREWALL_INTERFACE").unwrap();
|
||||
assert!(interface.starts_with("et_test_"));
|
||||
let before = list_interface_firewall_rules(&interface).unwrap();
|
||||
assert!(
|
||||
!before.is_empty(),
|
||||
"an administrator must install test rules first"
|
||||
);
|
||||
|
||||
assert!(add_interface_to_firewall_allowlist(&interface).is_err());
|
||||
let error = remove_interface_firewall_rules(&interface).unwrap_err();
|
||||
for name in &before {
|
||||
assert!(error.to_string().contains(name));
|
||||
}
|
||||
assert_eq!(list_interface_firewall_rules(&interface).unwrap(), before);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use std::os::windows::io::{FromRawHandle, OwnedHandle};
|
||||
|
||||
use windows::{
|
||||
Win32::{
|
||||
Devices::DeviceAndDriverInstallation::{
|
||||
CM_LOCATE_DEVNODE_NORMAL, CM_Locate_DevNodeW, CR_NO_SUCH_DEVNODE, CR_SUCCESS,
|
||||
},
|
||||
Foundation::{ERROR_ALREADY_EXISTS, GetLastError, HLOCAL, LocalFree},
|
||||
Security::{
|
||||
Authorization::{
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1,
|
||||
},
|
||||
PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES,
|
||||
},
|
||||
System::Threading::{CreateMutexExW, SYNCHRONIZATION_SYNCHRONIZE},
|
||||
},
|
||||
core::{HSTRING, w},
|
||||
};
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE};
|
||||
|
||||
fn reserve_name(name: &str) -> anyhow::Result<Option<OwnedHandle>> {
|
||||
let identity = uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_URL, name.to_lowercase().as_bytes());
|
||||
let object_name = HSTRING::from(format!("Global\\EasyTier.TunName.{identity}"));
|
||||
// Reserve the name by the object's lifetime, without thread ownership or
|
||||
// waiting. Windows also closes the handle if the process is terminated.
|
||||
let (handle, existed) = unsafe {
|
||||
// Service and desktop processes must be able to detect each other's
|
||||
// reservations. Grant SYNCHRONIZE only, without modification rights.
|
||||
let mut descriptor = PSECURITY_DESCRIPTOR::default();
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
w!("D:(A;;0x00100000;;;WD)"),
|
||||
SDDL_REVISION_1,
|
||||
&mut descriptor,
|
||||
None,
|
||||
)?;
|
||||
let attributes = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: descriptor.0,
|
||||
..Default::default()
|
||||
};
|
||||
let result = CreateMutexExW(
|
||||
Some(&attributes),
|
||||
&object_name,
|
||||
0,
|
||||
SYNCHRONIZATION_SYNCHRONIZE.0,
|
||||
);
|
||||
let existed = GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
LocalFree(Some(HLOCAL(descriptor.0)));
|
||||
let handle = result?;
|
||||
(OwnedHandle::from_raw_handle(handle.0), existed)
|
||||
};
|
||||
Ok((!existed).then_some(handle))
|
||||
}
|
||||
|
||||
fn interface_exists(name: &str) -> anyhow::Result<bool> {
|
||||
// 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}",
|
||||
)?;
|
||||
for guid in network.enum_keys() {
|
||||
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")?;
|
||||
if !existing_name.eq_ignore_ascii_case(name) {
|
||||
continue;
|
||||
}
|
||||
let instance_id: String = connection.get_value("PnpInstanceID")?;
|
||||
let mut dev_inst = 0;
|
||||
let result = unsafe {
|
||||
CM_Locate_DevNodeW(
|
||||
&mut dev_inst,
|
||||
&HSTRING::from(instance_id),
|
||||
CM_LOCATE_DEVNODE_NORMAL,
|
||||
)
|
||||
};
|
||||
match result {
|
||||
CR_SUCCESS => return Ok(true),
|
||||
CR_NO_SUCH_DEVNODE => continue,
|
||||
_ => anyhow::bail!("failed to locate network device {name}: {result:?}"),
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn reserve_tun_name(requested: &str) -> anyhow::Result<(String, OwnedHandle)> {
|
||||
if !requested.is_empty() {
|
||||
let handle = reserve_name(requested)?
|
||||
.ok_or_else(|| anyhow::anyhow!("TUN name {requested} is already in use"))?;
|
||||
return Ok((requested.to_owned(), handle));
|
||||
}
|
||||
|
||||
for index in 1..=u32::MAX {
|
||||
let name = format!("et_{index}");
|
||||
let Some(handle) = reserve_name(&name)? else {
|
||||
continue;
|
||||
};
|
||||
if !interface_exists(&name)? {
|
||||
return Ok((name, handle));
|
||||
}
|
||||
}
|
||||
anyhow::bail!("no unused TUN name available")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::reserve_tun_name;
|
||||
use std::sync::{Arc, Barrier};
|
||||
|
||||
#[test]
|
||||
fn explicit_name_reservation_is_case_insensitive_and_released() {
|
||||
let name = format!("et_test_reserve_{}", uuid::Uuid::new_v4().simple());
|
||||
let reservation = reserve_tun_name(&name).unwrap();
|
||||
let error = reserve_tun_name(&name.to_uppercase()).unwrap_err();
|
||||
assert!(error.to_string().contains("is already in use"));
|
||||
drop(reservation);
|
||||
assert!(reserve_tun_name(&name).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_default_names_are_unique_and_reuse_vacant_slots() {
|
||||
let barrier = Arc::new(Barrier::new(8));
|
||||
let mut reservations = std::thread::scope(|scope| {
|
||||
let threads: Vec<_> = (0..8)
|
||||
.map(|_| {
|
||||
let barrier = barrier.clone();
|
||||
scope.spawn(move || {
|
||||
barrier.wait();
|
||||
reserve_tun_name("").unwrap()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
threads
|
||||
.into_iter()
|
||||
.map(|thread| thread.join().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
reservations.sort_by_key(|(name, _)| name[3..].parse::<u32>().unwrap());
|
||||
assert!(
|
||||
reservations
|
||||
.windows(2)
|
||||
.all(|names| names[0].0 != names[1].0)
|
||||
);
|
||||
|
||||
let (name, handle) = reservations.remove(0);
|
||||
drop(handle);
|
||||
let reused = reserve_tun_name("").unwrap();
|
||||
assert_eq!(reused.0, name);
|
||||
}
|
||||
}
|
||||
@@ -333,80 +333,6 @@ impl RegistryManager {
|
||||
pub const NETBT_INTERFACE_PREFIX: &str =
|
||||
r"SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces\Tcpip_";
|
||||
|
||||
pub fn reg_delete_obsoleted_items(dev_name: &str) -> io::Result<()> {
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let profiles_key = hklm.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Profiles",
|
||||
KEY_ALL_ACCESS,
|
||||
)?;
|
||||
let unmanaged_key = hklm.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\NetworkList\\Signatures\\Unmanaged",
|
||||
KEY_ALL_ACCESS,
|
||||
)?;
|
||||
// collect subkeys to delete
|
||||
let mut keys_to_delete = Vec::new();
|
||||
let mut keys_to_delete_unmanaged = Vec::new();
|
||||
for subkey_name in profiles_key.enum_keys().filter_map(Result::ok) {
|
||||
let subkey = profiles_key.open_subkey(&subkey_name)?;
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("ProfileName") {
|
||||
Ok(profile_name) => {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete.push(subkey_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to read ProfileName for subkey {}: {}",
|
||||
subkey_name,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for subkey_name in unmanaged_key.enum_keys().filter_map(Result::ok) {
|
||||
let subkey = unmanaged_key.open_subkey(&subkey_name)?;
|
||||
// check if ProfileName contains "et"
|
||||
match subkey.get_value::<String, _>("Description") {
|
||||
Ok(profile_name) => {
|
||||
if profile_name.contains("et_")
|
||||
|| (!dev_name.is_empty() && dev_name == profile_name)
|
||||
{
|
||||
keys_to_delete_unmanaged.push(subkey_name);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to read ProfileName for subkey {}: {}",
|
||||
subkey_name,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// delete collected subkeys
|
||||
if !keys_to_delete.is_empty() {
|
||||
for subkey_name in keys_to_delete {
|
||||
match profiles_key.delete_subkey_all(&subkey_name) {
|
||||
Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name),
|
||||
Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
if !keys_to_delete_unmanaged.is_empty() {
|
||||
for subkey_name in keys_to_delete_unmanaged {
|
||||
match unmanaged_key.delete_subkey_all(&subkey_name) {
|
||||
Ok(_) => tracing::trace!("Successfully deleted subkey: {}", subkey_name),
|
||||
Err(e) => tracing::error!("Failed to delete subkey {}: {}", subkey_name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reg_change_catrgory_in_profile(dev_name: &str) -> io::Result<()> {
|
||||
use winreg::{RegKey, enums::HKEY_LOCAL_MACHINE, enums::KEY_ALL_ACCESS};
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
#[cfg(not(mobile))]
|
||||
use easytier_core::instance::CorePacketPlane;
|
||||
@@ -18,24 +15,24 @@ use super::MagicDnsRuntime;
|
||||
use crate::instance::shared_virtual_nic::ArcSharedVirtualNicRegistry;
|
||||
use crate::{common::global_ctx::ArcGlobalCtx, instance::virtual_nic::NicCtx};
|
||||
|
||||
struct NicCtxContainer {
|
||||
_nic_ctx: Option<Box<dyn Any + Send>>,
|
||||
magic_dns: MagicDnsRuntime,
|
||||
enum NicCtxContainer {
|
||||
Active {
|
||||
nic_ctx: Box<NicCtx>,
|
||||
magic_dns: MagicDnsRuntime,
|
||||
},
|
||||
PacketDrain(JoinSet<()>),
|
||||
}
|
||||
|
||||
impl NicCtxContainer {
|
||||
fn new(nic_ctx: NicCtx, magic_dns: MagicDnsRuntime) -> Self {
|
||||
Self {
|
||||
_nic_ctx: Some(Box::new(nic_ctx)),
|
||||
Self::Active {
|
||||
nic_ctx: Box::new(nic_ctx),
|
||||
magic_dns,
|
||||
}
|
||||
}
|
||||
|
||||
fn packet_drain(tasks: JoinSet<()>) -> Self {
|
||||
Self {
|
||||
_nic_ctx: Some(Box::new(tasks)),
|
||||
magic_dns: MagicDnsRuntime::default(),
|
||||
}
|
||||
Self::PacketDrain(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +64,18 @@ impl TunNicState {
|
||||
}
|
||||
|
||||
pub(super) async fn stop(&self) {
|
||||
let mut old = self.nic_ctx.lock().await.take();
|
||||
if let Some(nic) = old.as_mut() {
|
||||
nic.magic_dns.stop().await;
|
||||
let old = self.nic_ctx.lock().await.take();
|
||||
match old {
|
||||
Some(NicCtxContainer::Active {
|
||||
mut nic_ctx,
|
||||
mut magic_dns,
|
||||
}) => {
|
||||
magic_dns.stop().await;
|
||||
nic_ctx.shutdown().await;
|
||||
}
|
||||
Some(NicCtxContainer::PacketDrain(mut tasks)) => tasks.shutdown().await,
|
||||
None => {}
|
||||
}
|
||||
drop(old);
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
@@ -79,8 +83,10 @@ impl TunNicState {
|
||||
&self,
|
||||
) -> Option<crate::instance::shared_virtual_nic::SharedIfConfigClaims> {
|
||||
let nic = self.nic_ctx.lock().await;
|
||||
let nic = nic.as_ref()?._nic_ctx.as_ref()?.downcast_ref::<NicCtx>()?;
|
||||
nic.current_mobile_tun_claims().await
|
||||
match nic.as_ref()? {
|
||||
NicCtxContainer::Active { nic_ctx, .. } => nic_ctx.current_mobile_tun_claims().await,
|
||||
NicCtxContainer::PacketDrain(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn drain(&self) {
|
||||
@@ -194,6 +200,28 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{common::global_ctx::GlobalCtxEvent, instance::test_instance::TestInstance};
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_releases_packet_receiver_before_returning() {
|
||||
let state = TunNicState::empty();
|
||||
let (_sender, receiver) = tokio::sync::mpsc::channel(1);
|
||||
state
|
||||
.install_receiver(HostPacketReceiver::new(receiver))
|
||||
.unwrap();
|
||||
let receiver = state.receiver();
|
||||
state.drain().await;
|
||||
// Let the drain acquire the receiver and block waiting for a packet.
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
while receiver.try_lock().is_ok() {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
state.stop().await;
|
||||
assert!(receiver.try_lock().is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dhcp_preserves_publication_permit_and_cancelled_address() {
|
||||
let instance = TestInstance::new_with_process_runtime(
|
||||
|
||||
@@ -78,6 +78,7 @@ impl NativeTunRuntime {
|
||||
Self::report_static_ip_cancelled(&mut output);
|
||||
return;
|
||||
}
|
||||
nic_state.stop().await;
|
||||
let closed = Arc::new(Notify::new());
|
||||
let mut nic = NicCtx::new(
|
||||
global_ctx.clone(),
|
||||
|
||||
@@ -246,6 +246,8 @@ impl SharedVirtualNic {
|
||||
if let Some(dispatcher) = self.dispatcher.take() {
|
||||
dispatcher.shutdown_without_invalidation().await;
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
self.nic.lock().await.clear_windows_device();
|
||||
}
|
||||
|
||||
async fn apply_ifcfg_update(&self, next_ifcfg: &SharedIfConfig) -> Result<(), Error> {
|
||||
@@ -362,7 +364,10 @@ impl SharedVirtualNic {
|
||||
self.member_tunnel_table.clone(),
|
||||
self.valid.clone(),
|
||||
);
|
||||
self.sync_dispatcher_sources(&dispatcher).await?;
|
||||
if let Err(err) = self.sync_dispatcher_sources(&dispatcher).await {
|
||||
dispatcher.shutdown_without_invalidation().await;
|
||||
return Err(err);
|
||||
}
|
||||
self.dispatcher = Some(dispatcher);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ impl SharedVirtualNicNatTable {
|
||||
}
|
||||
|
||||
pub(super) struct SharedVirtualNicDispatcher {
|
||||
_task: AbortOnDropHandle<()>,
|
||||
task: AbortOnDropHandle<()>,
|
||||
control_sender: mpsc::UnboundedSender<SharedVirtualNicControl>,
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ impl SharedVirtualNicDispatcher {
|
||||
};
|
||||
|
||||
Self {
|
||||
_task: AbortOnDropHandle::new(SHARED_VIRTUAL_NIC_RUNTIME.spawn(task.run())),
|
||||
task: AbortOnDropHandle::new(SHARED_VIRTUAL_NIC_RUNTIME.spawn(task.run())),
|
||||
control_sender,
|
||||
}
|
||||
}
|
||||
@@ -566,7 +566,7 @@ impl SharedVirtualNicDispatcher {
|
||||
let task = SHARED_VIRTUAL_NIC_RUNTIME.spawn(task.run_for_mobile(first_open_sender));
|
||||
|
||||
let dispatcher = Self {
|
||||
_task: AbortOnDropHandle::new(task),
|
||||
task: AbortOnDropHandle::new(task),
|
||||
control_sender,
|
||||
};
|
||||
await_mobile_open(first_open_receiver)
|
||||
@@ -631,23 +631,25 @@ impl SharedVirtualNicDispatcher {
|
||||
|
||||
pub(super) async fn shutdown_without_invalidation(self) {
|
||||
let (ack, rx) = oneshot::channel();
|
||||
if self
|
||||
let sent = self
|
||||
.control_sender
|
||||
.send(SharedVirtualNicControl::Shutdown {
|
||||
invalidate: false,
|
||||
ack,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
.is_ok();
|
||||
|
||||
if tokio::time::timeout(DISPATCHER_SHUTDOWN_TIMEOUT, rx)
|
||||
.await
|
||||
.is_err()
|
||||
if sent
|
||||
&& tokio::time::timeout(DISPATCHER_SHUTDOWN_TIMEOUT, rx)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!("timed out shutting down shared virtual nic dispatcher");
|
||||
}
|
||||
// Even on send failure or timeout, wait for the task's TUN halves to
|
||||
// be dropped before releasing the device name for another owner.
|
||||
self.task.abort();
|
||||
let _ = self.task.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2025,7 +2027,7 @@ mod tests {
|
||||
},
|
||||
};
|
||||
let dispatcher = SharedVirtualNicDispatcher {
|
||||
_task: AbortOnDropHandle::new(tokio::spawn(task.run())),
|
||||
task: AbortOnDropHandle::new(tokio::spawn(task.run())),
|
||||
control_sender: control_sender.clone(),
|
||||
};
|
||||
(dispatcher, control_sender)
|
||||
@@ -2104,7 +2106,7 @@ mod tests {
|
||||
}
|
||||
assert!(!valid.load(Ordering::Acquire));
|
||||
assert!(member_tunnel_table.control_sender().is_none());
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher._task)
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher.task)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -2170,7 +2172,7 @@ mod tests {
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher._task)
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher.task)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -2210,7 +2212,7 @@ mod tests {
|
||||
}
|
||||
assert!(!valid.load(Ordering::Acquire));
|
||||
assert!(member_tunnel_table.control_sender().is_none());
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher._task)
|
||||
tokio::time::timeout(Duration::from_secs(1), dispatcher.task)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
@@ -3077,6 +3079,35 @@ mod tests {
|
||||
assert!(member_tunnel_table.control_sender().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatcher_shutdown_waits_for_resources_when_control_fails() {
|
||||
for receiver_closed in [true, false] {
|
||||
let (control_sender, control_receiver) = mpsc::unbounded_channel();
|
||||
let resource = Arc::new(());
|
||||
let weak = Arc::downgrade(&resource);
|
||||
let (ready, started) = oneshot::channel();
|
||||
let task = tokio::spawn(async move {
|
||||
let _resource = resource;
|
||||
let _receiver = if receiver_closed {
|
||||
drop(control_receiver);
|
||||
None
|
||||
} else {
|
||||
Some(control_receiver)
|
||||
};
|
||||
ready.send(()).unwrap();
|
||||
std::future::pending::<()>().await;
|
||||
});
|
||||
started.await.unwrap();
|
||||
let dispatcher = SharedVirtualNicDispatcher {
|
||||
task: AbortOnDropHandle::new(task),
|
||||
control_sender,
|
||||
};
|
||||
|
||||
dispatcher.shutdown_without_invalidation().await;
|
||||
assert!(weak.upgrade().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancelled_member_updates_finish_before_registration_cleanup() {
|
||||
use super::super::{SharedVirtualNic, SharedVirtualNicMember};
|
||||
@@ -3222,7 +3253,7 @@ mod tests {
|
||||
}
|
||||
});
|
||||
shared_nic.dispatcher = Some(SharedVirtualNicDispatcher {
|
||||
_task: AbortOnDropHandle::new(task),
|
||||
task: AbortOnDropHandle::new(task),
|
||||
control_sender,
|
||||
});
|
||||
let table = shared_nic.member_tunnel_table.clone();
|
||||
|
||||
@@ -7,6 +7,80 @@ use super::*;
|
||||
const MEMBER: SharedVirtualNicMemberId = uuid::Uuid::from_u128(1);
|
||||
const REGISTRATION: SharedVirtualNicMemberRegistrationId = uuid::Uuid::from_u128(10);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Administrator privileges and wintun.dll"]
|
||||
async fn idle_shared_nic_releases_name_while_registry_remains_alive() {
|
||||
use crate::{
|
||||
arch::windows::list_interface_firewall_rules,
|
||||
common::config::{ConfigLoader, TomlConfigLoader},
|
||||
instance::test_instance::TestInstance,
|
||||
};
|
||||
use easytier_core::process_runtime::CoreProcessRuntime;
|
||||
use std::time::Duration;
|
||||
|
||||
let name = "et_test_idle_shared";
|
||||
let config = || {
|
||||
let config =
|
||||
TomlConfigLoader::new_from_str("ipv4 = \"198.18.28.1/30\"\nlisteners = []").unwrap();
|
||||
let mut flags = config.get_flags();
|
||||
flags.dev_name = name.to_owned();
|
||||
config.set_flags(flags);
|
||||
config
|
||||
};
|
||||
let registry = TestInstance::new_shared_virtual_nic_registry();
|
||||
let mut first = TestInstance::new_with_process_runtime_and_shared_virtual_nic_registry(
|
||||
config(),
|
||||
CoreProcessRuntime::new(),
|
||||
registry.clone(),
|
||||
);
|
||||
first.run().await.unwrap();
|
||||
assert_eq!(list_interface_firewall_rules(name).unwrap().len(), 8);
|
||||
let cached = registry
|
||||
.lock()
|
||||
.await
|
||||
.get_by_dev_name_for_test(name)
|
||||
.unwrap();
|
||||
first.clear_resources().await;
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
let nic = cached.lock().await;
|
||||
if nic.member_registrations.is_empty() && nic.dispatcher.is_none() {
|
||||
break;
|
||||
}
|
||||
drop(nic);
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(list_interface_firewall_rules(name).unwrap().is_empty());
|
||||
|
||||
// An independent registry has the same name-allocation boundary as a
|
||||
// separate process. The previous registry deliberately remains alive.
|
||||
let mut second = TestInstance::new_with_process_runtime_and_shared_virtual_nic_registry(
|
||||
config(),
|
||||
CoreProcessRuntime::new(),
|
||||
TestInstance::new_shared_virtual_nic_registry(),
|
||||
);
|
||||
second.run().await.unwrap();
|
||||
assert_eq!(second.get_global_ctx().get_tun_device_name().unwrap(), name);
|
||||
let rules = list_interface_firewall_rules(name).unwrap();
|
||||
assert_eq!(rules.len(), 8);
|
||||
drop(first);
|
||||
drop(registry);
|
||||
drop(cached);
|
||||
assert_eq!(list_interface_firewall_rules(name).unwrap(), rules);
|
||||
second.clear_resources().await;
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
while !list_interface_firewall_rules(name).unwrap().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn virtual_nic_config(mtu: u32, netns: Option<&str>) -> VirtualNicConfig {
|
||||
VirtualNicConfig::new("et0".to_owned(), mtu, NetNS::new(netns.map(str::to_owned)))
|
||||
}
|
||||
|
||||
@@ -272,32 +272,43 @@ pub struct VirtualNic {
|
||||
|
||||
ifname: Option<String>,
|
||||
ifcfg: Box<dyn IfConfiguerTrait + Send + Sync + 'static>,
|
||||
|
||||
// Keep the name reserved through firewall cleanup. The device
|
||||
// itself may already have disappeared when its forwarding tasks stop.
|
||||
#[cfg(target_os = "windows")]
|
||||
name_reservation: Option<std::os::windows::io::OwnedHandle>,
|
||||
}
|
||||
|
||||
impl Drop for VirtualNic {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Some(ref ifname) = self.ifname {
|
||||
// Try to clean up firewall rules, but don't panic in destructor
|
||||
if let Err(error) = crate::arch::windows::remove_interface_firewall_rules(ifname) {
|
||||
tracing::warn!(
|
||||
%error,
|
||||
"failed to remove firewall rules for interface {}",
|
||||
ifname
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.clear_windows_device();
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualNic {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn clear_windows_device(&mut self) {
|
||||
if let Some(ifname) = self.ifname.take() {
|
||||
// Try to clean up firewall rules, but don't panic in destructor
|
||||
if let Err(error) = crate::arch::windows::remove_interface_firewall_rules(&ifname) {
|
||||
tracing::warn!(
|
||||
%error,
|
||||
"failed to remove firewall rules for interface {}",
|
||||
ifname
|
||||
);
|
||||
}
|
||||
}
|
||||
self.name_reservation.take();
|
||||
}
|
||||
|
||||
pub fn new(config: VirtualNicConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
ifname: None,
|
||||
ifcfg: Box::new(IfConfiger::default()),
|
||||
#[cfg(target_os = "windows")]
|
||||
name_reservation: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +559,12 @@ impl VirtualNic {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if self.name_reservation.is_none() {
|
||||
let (name, reservation) =
|
||||
crate::arch::windows::reserve_tun_name(&self.config.dev_name)?;
|
||||
self.config.dev_name = name;
|
||||
self.name_reservation = Some(reservation);
|
||||
}
|
||||
let dev_name = self.config.dev_name.clone();
|
||||
|
||||
match crate::arch::windows::add_self_to_firewall_allowlist() {
|
||||
@@ -560,30 +577,14 @@ impl VirtualNic {
|
||||
}
|
||||
}
|
||||
|
||||
match RegistryManager::reg_delete_obsoleted_items(&dev_name) {
|
||||
Ok(_) => tracing::trace!("delete successful!"),
|
||||
Err(e) => tracing::error!("An error occurred: {}", e),
|
||||
}
|
||||
|
||||
if !dev_name.is_empty() {
|
||||
config.tun_name(&dev_name);
|
||||
} else {
|
||||
use rand::distributions::Distribution as _;
|
||||
let c = crate::arch::windows::interface_count()?;
|
||||
let mut rng = rand::thread_rng();
|
||||
let s: String = rand::distributions::Alphanumeric
|
||||
.sample_iter(&mut rng)
|
||||
.take(4)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
.to_lowercase();
|
||||
|
||||
let random_dev_name = format!("et_{}_{}", c, s);
|
||||
config.tun_name(random_dev_name.clone());
|
||||
self.config.dev_name = random_dev_name;
|
||||
}
|
||||
|
||||
config.tun_name(&dev_name);
|
||||
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.skip_config(true);
|
||||
config.ring_cap(Some(std::cmp::min(
|
||||
config.min_ring_cap() * 32,
|
||||
@@ -1080,7 +1081,9 @@ impl NicCtx {
|
||||
mtu -= 20;
|
||||
}
|
||||
|
||||
VirtualNicConfig::new(flags.dev_name, mtu, global_ctx.net_ns.clone())
|
||||
let dev_name = flags.dev_name;
|
||||
|
||||
VirtualNicConfig::new(dev_name, mtu, global_ctx.net_ns.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn shared_route_backend_for_dns(&self) -> Option<NicBackend> {
|
||||
@@ -1126,7 +1129,8 @@ impl NicCtx {
|
||||
net_ns: NetNS::new(None),
|
||||
..nic_config
|
||||
};
|
||||
let backend = if cfg!(mobile) || !nic_config.dev_name.is_empty() {
|
||||
// Only an explicitly configured name opts into sharing.
|
||||
let backend = if cfg!(mobile) || !global_ctx.get_flags().dev_name.is_empty() {
|
||||
NicBackend::Shared(registry.lock().await.create_member(
|
||||
nic_config.dev_name.clone(),
|
||||
nic_config,
|
||||
@@ -1150,6 +1154,12 @@ impl NicCtx {
|
||||
self.backend.ifname().await
|
||||
}
|
||||
|
||||
pub(crate) async fn shutdown(&mut self) {
|
||||
// The forwarding tasks own the TUN stream and sink. Wait until both
|
||||
// release the device before another NIC with the same name is created.
|
||||
self.tasks.shutdown().await;
|
||||
}
|
||||
|
||||
async fn tun_ifname(&self) -> Result<String, Error> {
|
||||
self.backend
|
||||
.ifname()
|
||||
@@ -1561,11 +1571,6 @@ impl NicCtx {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut flags = self.global_ctx.get_flags();
|
||||
if flags.dev_name.is_empty() {
|
||||
flags.dev_name = ifname.clone();
|
||||
self.global_ctx.set_flags(flags);
|
||||
}
|
||||
let _ = RegistryManager::reg_change_catrgory_in_profile(&ifname);
|
||||
}
|
||||
|
||||
@@ -1710,6 +1715,109 @@ mod tests {
|
||||
|
||||
use super::{NicCtx, VirtualNic};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[rstest::rstest]
|
||||
#[case(None)]
|
||||
#[case(Some("et_test_restart"))]
|
||||
#[tokio::test]
|
||||
#[ignore = "requires Administrator privileges and wintun.dll"]
|
||||
async fn windows_device_identity_survives_recreation(#[case] dev_name: Option<&str>) {
|
||||
use crate::{
|
||||
arch::windows::list_interface_firewall_rules,
|
||||
common::{
|
||||
config::{ConfigLoader, TomlConfigLoader},
|
||||
ifcfg::RegistryManager,
|
||||
},
|
||||
instance::test_instance::TestInstance,
|
||||
};
|
||||
use easytier_core::process_runtime::CoreProcessRuntime;
|
||||
|
||||
let process = CoreProcessRuntime::new();
|
||||
let registry = TestInstance::new_shared_virtual_nic_registry();
|
||||
let new_instance = |config: TomlConfigLoader| {
|
||||
if let Some(dev_name) = dev_name {
|
||||
let mut flags = config.get_flags();
|
||||
flags.dev_name = dev_name.to_owned();
|
||||
config.set_flags(flags);
|
||||
}
|
||||
TestInstance::new_with_process_runtime_and_shared_virtual_nic_registry(
|
||||
config,
|
||||
process.clone(),
|
||||
registry.clone(),
|
||||
)
|
||||
};
|
||||
let config =
|
||||
TomlConfigLoader::new_from_str("ipv4 = \"198.18.26.1/30\"\nlisteners = []").unwrap();
|
||||
let mut first = new_instance(config);
|
||||
let saved_config = first.get_global_ctx().config.dump();
|
||||
first.run().await.unwrap();
|
||||
let name = first.get_global_ctx().get_tun_device_name().unwrap();
|
||||
let guid = RegistryManager::find_interface_guid(&name).unwrap();
|
||||
assert_eq!(list_interface_firewall_rules(&name).unwrap().len(), 8);
|
||||
// The resolved name is runtime state, not an explicit request to share
|
||||
// the device when this GlobalCtx next recreates its NIC.
|
||||
assert_eq!(
|
||||
first.get_global_ctx().get_flags().dev_name,
|
||||
dev_name.unwrap_or_default(),
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.lock()
|
||||
.await
|
||||
.get_by_dev_name_for_test(&name)
|
||||
.is_some(),
|
||||
dev_name.is_some(),
|
||||
);
|
||||
|
||||
// Keep B running while A is stopped and recreated, as in issue #110.
|
||||
let other_config =
|
||||
TomlConfigLoader::new_from_str("ipv4 = \"198.18.27.1/30\"\nlisteners = []").unwrap();
|
||||
let mut other = new_instance(other_config);
|
||||
other.run().await.unwrap();
|
||||
let other_name = other.get_global_ctx().get_tun_device_name().unwrap();
|
||||
let other_guid = RegistryManager::find_interface_guid(&other_name).unwrap();
|
||||
if dev_name.is_some() {
|
||||
assert_eq!(name, other_name);
|
||||
assert_eq!(guid, other_guid);
|
||||
} else {
|
||||
assert_ne!(name, other_name);
|
||||
assert_ne!(guid, other_guid);
|
||||
}
|
||||
first.clear_resources().await;
|
||||
|
||||
for _ in 0..10 {
|
||||
let config = TomlConfigLoader::new_from_str(&saved_config).unwrap();
|
||||
let mut restarted = new_instance(config);
|
||||
restarted.run().await.unwrap();
|
||||
assert_eq!(
|
||||
restarted.get_global_ctx().get_tun_device_name().unwrap(),
|
||||
name
|
||||
);
|
||||
assert_eq!(RegistryManager::find_interface_guid(&name).unwrap(), guid);
|
||||
assert_eq!(
|
||||
RegistryManager::find_interface_guid(&other_name).unwrap(),
|
||||
other_guid,
|
||||
);
|
||||
assert_eq!(list_interface_firewall_rules(&name).unwrap().len(), 8);
|
||||
restarted.clear_resources().await;
|
||||
assert_eq!(list_interface_firewall_rules(&other_name).unwrap().len(), 8);
|
||||
if dev_name.is_none() {
|
||||
assert!(list_interface_firewall_rules(&name).unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
other.clear_resources().await;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
while !list_interface_firewall_rules(&other_name)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn mobile_configuration_registers_ipv6_and_tracks_public_updates() {
|
||||
|
||||
Reference in new issue
Block a user