From a63778854f7ef6bc582ef280505a16303873e69b Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Mon, 3 Feb 2025 15:13:50 +0800 Subject: [PATCH] use netlink instead of shell cmd to config ip (#593) --- .github/workflows/core.yml | 1 + Cargo.lock | 35 +- easytier/Cargo.toml | 8 +- easytier/src/common/ifcfg.rs | 417 -------------------- easytier/src/common/ifcfg/darwin.rs | 81 ++++ easytier/src/common/ifcfg/mod.rs | 127 ++++++ easytier/src/common/ifcfg/netlink.rs | 569 +++++++++++++++++++++++++++ easytier/src/common/ifcfg/route.rs | 133 +++++++ easytier/src/common/ifcfg/windows.rs | 166 ++++++++ easytier/src/tests/three_node.rs | 8 +- 10 files changed, 1115 insertions(+), 430 deletions(-) delete mode 100644 easytier/src/common/ifcfg.rs create mode 100644 easytier/src/common/ifcfg/darwin.rs create mode 100644 easytier/src/common/ifcfg/mod.rs create mode 100644 easytier/src/common/ifcfg/netlink.rs create mode 100644 easytier/src/common/ifcfg/route.rs create mode 100644 easytier/src/common/ifcfg/windows.rs diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index f274a6e..b4114a2 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -97,6 +97,7 @@ jobs: echo "GIT_DESC=$(git log -1 --format=%cd.%h --date=format:%Y-%m-%d_%H:%M:%S)" >> $GITHUB_ENV - name: Cargo cache + if: ${{ ! endsWith(matrix.TARGET, 'freebsd') }} uses: actions/cache@v4 with: path: | diff --git a/Cargo.lock b/Cargo.lock index 18fb409..650bc7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -925,9 +925,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" dependencies = [ "serde", ] @@ -1652,7 +1652,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-packet-generic", - "netlink-packet-route", + "netlink-packet-route 0.17.1", "netlink-packet-utils", "netlink-packet-wireguard", "netlink-sys", @@ -1913,8 +1913,12 @@ dependencies = [ "kcp-sys", "machine-uid", "mimalloc-rust", + "netlink-packet-core", + "netlink-packet-route 0.21.0", + "netlink-packet-utils", + "netlink-sys", "network-interface", - "nix 0.27.1", + "nix 0.29.0", "once_cell", "parking_lot", "percent-encoding", @@ -3637,9 +3641,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -4049,6 +4053,21 @@ dependencies = [ "netlink-packet-utils", ] +[[package]] +name = "netlink-packet-route" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +dependencies = [ + "anyhow", + "bitflags 2.8.0", + "byteorder", + "libc", + "log", + "netlink-packet-core", + "netlink-packet-utils", +] + [[package]] name = "netlink-packet-utils" version = "0.5.2" @@ -4077,9 +4096,9 @@ dependencies = [ [[package]] name = "netlink-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416060d346fbaf1f23f9512963e3e878f1a78e707cb699ba9215761754244307" +checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" dependencies = [ "bytes", "libc", diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 0365e64..5c7abf7 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -89,7 +89,7 @@ tun = { package = "tun-easytier", version = "1.1.1", features = [ "async", ], optional = true } # for net ns -nix = { version = "0.27", features = ["sched", "socket", "ioctl"] } +nix = { version = "0.29.0", features = ["sched", "socket", "ioctl", "net"] } uuid = { version = "1.5.0", features = [ "v4", @@ -197,6 +197,12 @@ prost-reflect = { version = "0.14.5", features = [ [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] machine-uid = "0.5.3" +[target.'cfg(any(target_os = "linux"))'.dependencies] +netlink-sys = "0.8.7" +netlink-packet-route = "0.21.0" +netlink-packet-core = { version = "0.7.0" } +netlink-packet-utils = "0.5.2" + [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.52", features = [ "Win32_Networking_WinSock", diff --git a/easytier/src/common/ifcfg.rs b/easytier/src/common/ifcfg.rs deleted file mode 100644 index a5de48f..0000000 --- a/easytier/src/common/ifcfg.rs +++ /dev/null @@ -1,417 +0,0 @@ -use std::net::Ipv4Addr; - -use async_trait::async_trait; -use tokio::process::Command; - -use super::error::Error; - -#[async_trait] -pub trait IfConfiguerTrait: Send + Sync { - async fn add_ipv4_route( - &self, - _name: &str, - _address: Ipv4Addr, - _cidr_prefix: u8, - ) -> Result<(), Error> { - Ok(()) - } - async fn remove_ipv4_route( - &self, - _name: &str, - _address: Ipv4Addr, - _cidr_prefix: u8, - ) -> Result<(), Error> { - Ok(()) - } - async fn add_ipv4_ip( - &self, - _name: &str, - _address: Ipv4Addr, - _cidr_prefix: u8, - ) -> Result<(), Error> { - Ok(()) - } - async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> { - Ok(()) - } - async fn remove_ip(&self, _name: &str, _ip: Option) -> Result<(), Error> { - Ok(()) - } - async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> { - return Ok(()); - } - async fn set_mtu(&self, _name: &str, _mtu: u32) -> Result<(), Error> { - Ok(()) - } -} - -fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr { - if prefix_length > 32 { - panic!("Invalid CIDR prefix length"); - } - - let subnet_mask: u32 = (!0u32) - .checked_shl(32 - u32::from(prefix_length)) - .unwrap_or(0); - Ipv4Addr::new( - ((subnet_mask >> 24) & 0xFF) as u8, - ((subnet_mask >> 16) & 0xFF) as u8, - ((subnet_mask >> 8) & 0xFF) as u8, - (subnet_mask & 0xFF) as u8, - ) -} - -async fn run_shell_cmd(cmd: &str) -> Result<(), Error> { - let cmd_out: std::process::Output; - let stdout: String; - let stderr: String; - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd_out = Command::new("cmd") - .stdin(std::process::Stdio::null()) - .arg("/C") - .arg(cmd) - .creation_flags(CREATE_NO_WINDOW) - .output() - .await?; - stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice()); - stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice()); - }; - - #[cfg(not(target_os = "windows"))] - { - cmd_out = Command::new("sh").arg("-c").arg(cmd).output().await?; - stdout = String::from_utf8_lossy(cmd_out.stdout.as_slice()).to_string(); - stderr = String::from_utf8_lossy(cmd_out.stderr.as_slice()).to_string(); - }; - - let ec = cmd_out.status.code(); - let succ = cmd_out.status.success(); - tracing::info!(?cmd, ?ec, ?succ, ?stdout, ?stderr, "run shell cmd"); - - if !cmd_out.status.success() { - return Err(Error::ShellCommandError(stdout + &stderr)); - } - Ok(()) -} - -pub struct MacIfConfiger {} -#[async_trait] -impl IfConfiguerTrait for MacIfConfiger { - async fn add_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd( - format!( - "route -n add {} -netmask {} -interface {} -hopcount 7", - address, - cidr_to_subnet_mask(cidr_prefix), - name - ) - .as_str(), - ) - .await - } - - async fn remove_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd( - format!( - "route -n delete {} -netmask {} -interface {}", - address, - cidr_to_subnet_mask(cidr_prefix), - name - ) - .as_str(), - ) - .await - } - - async fn add_ipv4_ip( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd( - format!( - "ifconfig {} {:?}/{:?} 10.8.8.8 up", - name, address, cidr_prefix, - ) - .as_str(), - ) - .await - } - - async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { - run_shell_cmd(format!("ifconfig {} {}", name, if up { "up" } else { "down" }).as_str()) - .await - } - - async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { - if ip.is_none() { - run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await - } else { - run_shell_cmd( - format!("ifconfig {} inet {} delete", name, ip.unwrap().to_string()).as_str(), - ) - .await - } - } - - async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { - run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await - } -} - -pub struct LinuxIfConfiger {} -#[async_trait] -impl IfConfiguerTrait for LinuxIfConfiger { - async fn add_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd( - format!( - "ip route add {}/{} dev {} metric 65535", - address, cidr_prefix, name - ) - .as_str(), - ) - .await - } - - async fn remove_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd(format!("ip route del {}/{} dev {}", address, cidr_prefix, name).as_str()) - .await - } - - async fn add_ipv4_ip( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd(format!("ip addr add {:?}/{:?} dev {}", address, cidr_prefix, name).as_str()) - .await - } - - async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { - run_shell_cmd(format!("ip link set {} {}", name, if up { "up" } else { "down" }).as_str()) - .await - } - - async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { - if ip.is_none() { - run_shell_cmd(format!("ip addr flush dev {}", name).as_str()).await - } else { - run_shell_cmd( - format!("ip addr del {:?} dev {}", ip.unwrap().to_string(), name).as_str(), - ) - .await - } - } - - async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { - run_shell_cmd(format!("ip link set dev {} mtu {}", name, mtu).as_str()).await - } -} - -#[cfg(target_os = "windows")] -pub struct WindowsIfConfiger {} - -#[cfg(target_os = "windows")] -impl WindowsIfConfiger { - pub fn get_interface_index(name: &str) -> Option { - crate::arch::windows::find_interface_index(name).ok() - } - - async fn list_ipv4(name: &str) -> Result, Error> { - use anyhow::Context; - use network_interface::NetworkInterfaceConfig; - use std::net::IpAddr; - let ret = network_interface::NetworkInterface::show().with_context(|| "show interface")?; - let addrs = ret - .iter() - .filter_map(|x| { - if x.name != name { - return None; - } - Some(x.addr.clone()) - }) - .flat_map(|x| x) - .map(|x| x.ip()) - .filter_map(|x| { - if let IpAddr::V4(ipv4) = x { - Some(ipv4) - } else { - None - } - }) - .collect::>(); - - Ok(addrs) - } - - async fn remove_one_ipv4(name: &str, ip: Ipv4Addr) -> Result<(), Error> { - run_shell_cmd( - format!( - "netsh interface ipv4 delete address {} address={}", - name, - ip.to_string() - ) - .as_str(), - ) - .await - } -} - -#[cfg(target_os = "windows")] -#[async_trait] -impl IfConfiguerTrait for WindowsIfConfiger { - async fn add_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - let Some(idx) = Self::get_interface_index(name) else { - return Err(Error::NotFound); - }; - run_shell_cmd( - format!( - "route ADD {} MASK {} 10.1.1.1 IF {} METRIC 9000", - address, - cidr_to_subnet_mask(cidr_prefix), - idx - ) - .as_str(), - ) - .await - } - - async fn remove_ipv4_route( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - let Some(idx) = Self::get_interface_index(name) else { - return Err(Error::NotFound); - }; - run_shell_cmd( - format!( - "route DELETE {} MASK {} IF {}", - address, - cidr_to_subnet_mask(cidr_prefix), - idx - ) - .as_str(), - ) - .await - } - - async fn add_ipv4_ip( - &self, - name: &str, - address: Ipv4Addr, - cidr_prefix: u8, - ) -> Result<(), Error> { - run_shell_cmd( - format!( - "netsh interface ipv4 add address {} address={} mask={}", - name, - address, - cidr_to_subnet_mask(cidr_prefix) - ) - .as_str(), - ) - .await - } - - async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { - run_shell_cmd( - format!( - "netsh interface set interface {} {}", - name, - if up { "enable" } else { "disable" } - ) - .as_str(), - ) - .await - } - - async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { - if ip.is_none() { - for ip in Self::list_ipv4(name).await?.iter() { - Self::remove_one_ipv4(name, *ip).await?; - } - Ok(()) - } else { - Self::remove_one_ipv4(name, ip.unwrap()).await - } - } - - async fn wait_interface_show(&self, name: &str) -> Result<(), Error> { - Ok( - tokio::time::timeout(std::time::Duration::from_secs(10), async move { - loop { - if let Some(idx) = Self::get_interface_index(name) { - tracing::info!(?name, ?idx, "Interface found"); - break; - } - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - Ok::<(), Error>(()) - }) - .await??, - ) - } - - async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { - let _ = run_shell_cmd( - format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(), - ) - .await; - run_shell_cmd( - format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(), - ) - .await - } -} - -pub struct DummyIfConfiger {} -#[async_trait] -impl IfConfiguerTrait for DummyIfConfiger {} - -#[cfg(any(target_os = "macos", target_os = "freebsd"))] -pub type IfConfiger = MacIfConfiger; - -#[cfg(target_os = "linux")] -pub type IfConfiger = LinuxIfConfiger; - -#[cfg(target_os = "windows")] -pub type IfConfiger = WindowsIfConfiger; - -#[cfg(not(any( - target_os = "macos", - target_os = "linux", - target_os = "windows", - target_os = "freebsd" -)))] -pub type IfConfiger = DummyIfConfiger; diff --git a/easytier/src/common/ifcfg/darwin.rs b/easytier/src/common/ifcfg/darwin.rs new file mode 100644 index 0000000..a496726 --- /dev/null +++ b/easytier/src/common/ifcfg/darwin.rs @@ -0,0 +1,81 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; + +use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait}; + +pub struct MacIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for MacIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n add {} -netmask {} -interface {} -hopcount 7", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "route -n delete {} -netmask {} -interface {}", + address, + cidr_to_subnet_mask(cidr_prefix), + name + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "ifconfig {} {:?}/{:?} 10.8.8.8 up", + name, address, cidr_prefix, + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd(format!("ifconfig {} {}", name, if up { "up" } else { "down" }).as_str()) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + run_shell_cmd(format!("ifconfig {} inet delete", name).as_str()).await + } else { + run_shell_cmd( + format!("ifconfig {} inet {} delete", name, ip.unwrap().to_string()).as_str(), + ) + .await + } + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await + } +} diff --git a/easytier/src/common/ifcfg/mod.rs b/easytier/src/common/ifcfg/mod.rs new file mode 100644 index 0000000..79a8ea2 --- /dev/null +++ b/easytier/src/common/ifcfg/mod.rs @@ -0,0 +1,127 @@ +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +mod darwin; +#[cfg(any(target_os = "linux"))] +mod netlink; +#[cfg(target_os = "windows")] +mod windows; + +mod route; + +use std::net::Ipv4Addr; + +use async_trait::async_trait; +use tokio::process::Command; + +use super::error::Error; + +#[async_trait] +pub trait IfConfiguerTrait: Send + Sync { + async fn add_ipv4_route( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn remove_ipv4_route( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn add_ipv4_ip( + &self, + _name: &str, + _address: Ipv4Addr, + _cidr_prefix: u8, + ) -> Result<(), Error> { + Ok(()) + } + async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> { + Ok(()) + } + async fn remove_ip(&self, _name: &str, _ip: Option) -> Result<(), Error> { + Ok(()) + } + async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> { + return Ok(()); + } + async fn set_mtu(&self, _name: &str, _mtu: u32) -> Result<(), Error> { + Ok(()) + } +} + +fn cidr_to_subnet_mask(prefix_length: u8) -> Ipv4Addr { + if prefix_length > 32 { + panic!("Invalid CIDR prefix length"); + } + + let subnet_mask: u32 = (!0u32) + .checked_shl(32 - u32::from(prefix_length)) + .unwrap_or(0); + Ipv4Addr::new( + ((subnet_mask >> 24) & 0xFF) as u8, + ((subnet_mask >> 16) & 0xFF) as u8, + ((subnet_mask >> 8) & 0xFF) as u8, + (subnet_mask & 0xFF) as u8, + ) +} + +async fn run_shell_cmd(cmd: &str) -> Result<(), Error> { + let cmd_out: std::process::Output; + let stdout: String; + let stderr: String; + #[cfg(target_os = "windows")] + { + const CREATE_NO_WINDOW: u32 = 0x08000000; + cmd_out = Command::new("cmd") + .stdin(std::process::Stdio::null()) + .arg("/C") + .arg(cmd) + .creation_flags(CREATE_NO_WINDOW) + .output() + .await?; + stdout = crate::utils::utf8_or_gbk_to_string(cmd_out.stdout.as_slice()); + stderr = crate::utils::utf8_or_gbk_to_string(cmd_out.stderr.as_slice()); + }; + + #[cfg(not(target_os = "windows"))] + { + cmd_out = Command::new("sh").arg("-c").arg(cmd).output().await?; + stdout = String::from_utf8_lossy(cmd_out.stdout.as_slice()).to_string(); + stderr = String::from_utf8_lossy(cmd_out.stderr.as_slice()).to_string(); + }; + + let ec = cmd_out.status.code(); + let succ = cmd_out.status.success(); + tracing::info!(?cmd, ?ec, ?succ, ?stdout, ?stderr, "run shell cmd"); + + if !cmd_out.status.success() { + return Err(Error::ShellCommandError(stdout + &stderr)); + } + Ok(()) +} + +pub struct DummyIfConfiger {} +#[async_trait] +impl IfConfiguerTrait for DummyIfConfiger {} + +#[cfg(any(target_os = "linux"))] +pub type IfConfiger = netlink::NetlinkIfConfiger; + +#[cfg(any(target_os = "macos", target_os = "freebsd"))] +pub type IfConfiger = darwin::MacIfConfiger; + +#[cfg(target_os = "windows")] +pub type IfConfiger = windows::WindowsIfConfiger; + +#[cfg(not(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "freebsd", +)))] +pub type IfConfiger = DummyIfConfiger; diff --git a/easytier/src/common/ifcfg/netlink.rs b/easytier/src/common/ifcfg/netlink.rs new file mode 100644 index 0000000..297ab2f --- /dev/null +++ b/easytier/src/common/ifcfg/netlink.rs @@ -0,0 +1,569 @@ +use std::{ + ffi::CString, + fmt::Debug, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + num::NonZero, + os::fd::AsRawFd, +}; + +use anyhow::Context; +use async_trait::async_trait; +use cidr::IpInet; +use netlink_packet_core::{ + NetlinkDeserializable, NetlinkHeader, NetlinkMessage, NetlinkPayload, NetlinkSerializable, + NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_REQUEST, +}; +use netlink_packet_route::{ + address::{AddressAttribute, AddressMessage}, + route::{ + RouteAddress, RouteAttribute, RouteHeader, RouteMessage, RouteProtocol, RouteScope, + RouteType, + }, + AddressFamily, RouteNetlinkMessage, +}; +use netlink_sys::{protocols::NETLINK_ROUTE, Socket, SocketAddr}; +use nix::{ + ifaddrs::getifaddrs, + libc::{self, ifreq, ioctl, Ioctl, SIOCGIFFLAGS, SIOCGIFMTU, SIOCSIFFLAGS, SIOCSIFMTU}, + net::if_::InterfaceFlags, + sys::socket::SockaddrLike as _, +}; +use pnet::ipnetwork::ip_mask_to_prefix; + +use super::{route::Route, Error, IfConfiguerTrait}; + +pub(crate) fn dummy_socket() -> Result { + Ok(std::net::UdpSocket::bind("0:0")?) +} + +fn build_ifreq(name: &str) -> ifreq { + let c_str = CString::new(name).unwrap(); + let mut ifr: ifreq = unsafe { std::mem::zeroed() }; + let name_bytes = c_str.as_bytes_with_nul(); + for (i, &b) in name_bytes.iter().enumerate() { + ifr.ifr_name[i] = b as libc::c_char; + } + ifr +} + +fn send_netlink_req( + req: T, + flags: u16, +) -> Result { + let mut socket = Socket::new(NETLINK_ROUTE)?; + socket.bind_auto()?; + socket.connect(&SocketAddr::new(0, 0))?; + + let mut req: NetlinkMessage = + NetlinkMessage::new(NetlinkHeader::default(), NetlinkPayload::InnerMessage(req)); + req.header.flags = flags; + + req.finalize(); + let mut buf = vec![0; req.header.length as _]; + req.serialize(&mut buf); + + tracing::debug!("net link request >>> {:?}", req); + socket.send(&buf, 0)?; + + Ok(socket) +} + +fn send_netlink_req_and_wait_one_resp( + req: T, +) -> Result<(), Error> { + let socket = send_netlink_req(req, NLM_F_ACK | NLM_F_CREATE | NLM_F_REQUEST)?; + let resp = socket.recv_from_full()?; + let ret = NetlinkMessage::::deserialize(&resp.0) + .with_context(|| "Failed to deserialize netlink message")?; + + tracing::debug!("net link response <<< {:?}", ret); + + match ret.payload { + NetlinkPayload::Error(e) => { + if e.code == NonZero::new(0) { + return Ok(()); + } else { + return Err(e.to_io().into()); + } + } + p => { + tracing::error!("Unexpected netlink response: {:?}", p); + return Err(anyhow::anyhow!("Unexpected netlink response").into()); + } + } +} + +fn addr_to_ip(addr: RouteAddress) -> Option { + match addr { + RouteAddress::Inet(addr) => Some(addr.into()), + RouteAddress::Inet6(addr) => Some(addr.into()), + _ => None, + } +} + +impl From for Route { + fn from(msg: RouteMessage) -> Self { + let mut gateway = None; + let mut source = None; + let mut source_hint = None; + let mut destination = None; + let mut ifindex = None; + let mut metric = None; + + for attr in msg.attributes { + match attr { + RouteAttribute::Source(addr) => { + source = addr_to_ip(addr); + } + RouteAttribute::PrefSource(addr) => { + source_hint = addr_to_ip(addr); + } + RouteAttribute::Destination(addr) => { + destination = addr_to_ip(addr); + } + RouteAttribute::Gateway(addr) => { + gateway = addr_to_ip(addr); + } + RouteAttribute::Oif(i) => { + ifindex = Some(i); + } + RouteAttribute::Priority(priority) => { + metric = Some(priority); + } + _ => {} + } + } + // rtnetlink gives None instead of 0.0.0.0 for the default route, but we'll convert to 0 here to make it match the other platforms + let destination = destination.unwrap_or_else(|| match msg.header.address_family { + AddressFamily::Inet => Ipv4Addr::UNSPECIFIED.into(), + AddressFamily::Inet6 => Ipv6Addr::UNSPECIFIED.into(), + _ => panic!("invalid destination family"), + }); + Self { + destination, + prefix: msg.header.destination_prefix_length, + source, + source_prefix: msg.header.source_prefix_length, + source_hint, + gateway, + ifindex, + table: msg.header.table, + metric, + } + } +} + +pub struct NetlinkIfConfiger {} + +impl NetlinkIfConfiger { + fn get_interface_index(name: &str) -> Result { + let name = CString::new(name).with_context(|| "failed to convert interface name")?; + match unsafe { libc::if_nametoindex(name.as_ptr()) } { + 0 => Err(std::io::Error::last_os_error().into()), + n => Ok(n), + } + } + + fn get_prefix_len(name: &str, ip: Ipv4Addr) -> Result { + let addrs = Self::list_addresses(name)?; + for addr in addrs { + if addr.address() == IpAddr::V4(ip) { + return Ok(addr.network_length()); + } + } + Err(Error::NotFound) + } + + fn remove_one_ip(name: &str, ip: Ipv4Addr, prefix_len: u8) -> Result<(), Error> { + let mut message = AddressMessage::default(); + message.header.prefix_len = prefix_len; + message.header.index = NetlinkIfConfiger::get_interface_index(name)?; + message.header.family = AddressFamily::Inet; + + message + .attributes + .push(AddressAttribute::Address(std::net::IpAddr::V4(ip))); + + send_netlink_req_and_wait_one_resp::(RouteNetlinkMessage::DelAddress( + message, + )) + } + + pub(crate) fn mtu_op>( + name: &str, + op: T, + value: libc::c_int, + ) -> Result + where + >::Error: Debug, + { + let dummy_socket = dummy_socket()?; + + let mut ifr: ifreq = build_ifreq(name); + + unsafe { + ifr.ifr_ifru.ifru_mtu = value; + + // 使用ioctl获取MTU + if ioctl(dummy_socket.as_raw_fd(), op.try_into().unwrap(), &ifr) != 0 { + return Err(std::io::Error::last_os_error().into()); + } + } + + Ok(unsafe { ifr.ifr_ifru.ifru_mtu as u32 }) + } + + fn mtu(name: &str) -> Result { + Self::mtu_op(name, SIOCGIFMTU, 0) + } + + pub fn list_addresses(name: &str) -> Result, Error> { + let mut result = vec![]; + + for interface in getifaddrs() + .with_context(|| "failed to call getifaddrs")? + .filter(|x| x.interface_name == name) + { + let (Some(address), Some(netmask)) = (interface.address, interface.netmask) else { + continue; + }; + + use nix::sys::socket::AddressFamily::{Inet, Inet6}; + + let (address, netmask) = match (address.family(), netmask.family()) { + (Some(Inet), Some(Inet)) => ( + IpAddr::V4(address.as_sockaddr_in().unwrap().ip().into()), + IpAddr::V4(netmask.as_sockaddr_in().unwrap().ip().into()), + ), + (Some(Inet6), Some(Inet6)) => ( + IpAddr::V6(address.as_sockaddr_in6().unwrap().ip()), + IpAddr::V6(netmask.as_sockaddr_in6().unwrap().ip()), + ), + (_, _) => continue, + }; + + let prefix = ip_mask_to_prefix(netmask).unwrap(); + + result.push(IpInet::new(address, prefix).unwrap()); + } + Ok(result) + } + + pub(crate) fn set_flags_op>( + name: &str, + op: T, + flags: InterfaceFlags, + ) -> Result + where + >::Error: Debug, + { + let mut req = build_ifreq(name); + req.ifr_ifru.ifru_flags = flags.bits() as _; + + let socket = dummy_socket()?; + + unsafe { + if ioctl(socket.as_raw_fd(), op.try_into().unwrap(), &req) != 0 { + return Err(std::io::Error::last_os_error().into()); + } + Ok(InterfaceFlags::from_bits_truncate( + req.ifr_ifru.ifru_flags as _, + )) + } + } + + pub(crate) fn set_flags(name: &str, flags: InterfaceFlags) -> Result { + Self::set_flags_op(name, SIOCSIFFLAGS, flags) + } + + pub(crate) fn get_flags(name: &str) -> Result { + Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty()) + } + + fn list_routes() -> Result, Error> { + let mut message = RouteMessage::default(); + + message.header.table = RouteHeader::RT_TABLE_UNSPEC; + message.header.protocol = RouteProtocol::Unspec; + + message.header.scope = RouteScope::Universe; + message.header.kind = RouteType::Unicast; + + message.header.address_family = AddressFamily::Inet; + message.header.destination_prefix_length = 0; + message.header.source_prefix_length = 0; + + let s = send_netlink_req( + RouteNetlinkMessage::GetRoute(message), + NLM_F_REQUEST | NLM_F_DUMP, + )?; + + let mut ret_vec = vec![]; + + let mut resp = Vec::::new(); + loop { + if resp.len() == 0 { + let (new_resp, _) = s.recv_from_full()?; + resp = new_resp; + } + let ret = NetlinkMessage::::deserialize(&resp) + .with_context(|| "Failed to deserialize netlink message")?; + resp = resp.split_off(ret.buffer_len()); + + tracing::debug!("net link response <<< {:?}", ret); + + match ret.payload { + NetlinkPayload::Error(e) => { + if e.code == NonZero::new(0) { + continue; + } else { + return Err(e.to_io().into()); + } + } + NetlinkPayload::InnerMessage(RouteNetlinkMessage::NewRoute(m)) => { + tracing::debug!("net link response <<< {:?}", m); + ret_vec.push(m); + } + NetlinkPayload::Done(_) => { + break; + } + p => { + tracing::error!("Unexpected netlink response: {:?}", p); + return Err(anyhow::anyhow!("Unexpected netlink response").into()); + } + } + } + + Ok(ret_vec) + } +} + +#[async_trait] +impl IfConfiguerTrait for NetlinkIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let mut message = RouteMessage::default(); + + message.header.table = RouteHeader::RT_TABLE_MAIN; + message.header.protocol = RouteProtocol::Static; + message.header.scope = RouteScope::Universe; + message.header.kind = RouteType::Unicast; + message.header.address_family = AddressFamily::Inet; + // metric + message.attributes.push(RouteAttribute::Priority(65535)); + // output interface + message + .attributes + .push(RouteAttribute::Oif(NetlinkIfConfiger::get_interface_index( + name, + )?)); + // source address + message.header.destination_prefix_length = cidr_prefix; + message + .attributes + .push(RouteAttribute::Destination(RouteAddress::Inet(address))); + + send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message)) + } + + async fn remove_ipv4_route( + &self, + _name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let routes = Self::list_routes()?; + + for msg in routes { + let other_route: Route = msg.clone().into(); + if other_route.destination == std::net::IpAddr::V4(address) + && other_route.prefix == cidr_prefix + { + send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg))?; + return Ok(()); + } + } + + Ok(()) + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let mut message = AddressMessage::default(); + + message.header.prefix_len = cidr_prefix; + message.header.index = NetlinkIfConfiger::get_interface_index(name)?; + message.header.family = AddressFamily::Inet; + + message + .attributes + .push(AddressAttribute::Address(std::net::IpAddr::V4(address))); + + // for IPv4 the IFA_LOCAL address can be set to the same value as + // IFA_ADDRESS + message + .attributes + .push(AddressAttribute::Local(std::net::IpAddr::V4(address))); + + // set the IFA_BROADCAST address as well + if cidr_prefix == 32 { + message + .attributes + .push(AddressAttribute::Broadcast(address)); + } else { + let ip_addr = u32::from(address); + let brd = Ipv4Addr::from((0xffff_ffff_u32) >> u32::from(cidr_prefix) | ip_addr); + message.attributes.push(AddressAttribute::Broadcast(brd)); + }; + + send_netlink_req_and_wait_one_resp::(RouteNetlinkMessage::NewAddress( + message, + )) + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + let mut flags = Self::get_flags(name)?; + flags.set(InterfaceFlags::IFF_UP, up); + Self::set_flags(name, flags)?; + Ok(()) + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + let addrs = Self::list_addresses(name)?; + for addr in addrs { + if let IpAddr::V4(ipv4) = addr.address() { + Self::remove_one_ip(name, ipv4, addr.network_length())?; + } + } + } else { + let ip = ip.unwrap(); + let prefix_len = Self::get_prefix_len(name, ip)?; + Self::remove_one_ip(name, ip, prefix_len)?; + } + + Ok(()) + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + Self::mtu_op(name, SIOCSIFMTU, mtu as libc::c_int)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const DUMMY_IFACE_NAME: &str = "dummy"; + + fn run_cmd(cmd: &str) -> String { + let output = std::process::Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("failed to execute process"); + String::from_utf8(output.stdout).unwrap() + } + + struct PrepareEnv {} + impl PrepareEnv { + fn new() -> Self { + let _ = run_cmd(&format!("sudo ip link add {} type dummy", DUMMY_IFACE_NAME)); + PrepareEnv {} + } + } + + impl Drop for PrepareEnv { + fn drop(&mut self) { + let _ = run_cmd(&format!("sudo ip link del {}", DUMMY_IFACE_NAME)); + } + } + + #[serial_test::serial] + #[tokio::test] + async fn addr_test() { + let _prepare_env = PrepareEnv::new(); + let ifcfg = NetlinkIfConfiger {}; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ifcfg + .add_ipv4_ip(DUMMY_IFACE_NAME, "10.44.44.4".parse().unwrap(), 24) + .await + .unwrap(); + + let addrs = NetlinkIfConfiger::list_addresses(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(addrs.len(), 1); + assert_eq!( + addrs[0].address(), + IpAddr::V4("10.44.44.4".parse().unwrap()) + ); + assert_eq!(addrs[0].network_length(), 24); + + NetlinkIfConfiger::remove_one_ip(DUMMY_IFACE_NAME, "10.44.44.4".parse().unwrap(), 24) + .unwrap(); + + let addrs = NetlinkIfConfiger::list_addresses(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(addrs.len(), 0); + + let old_mtu = NetlinkIfConfiger::mtu(DUMMY_IFACE_NAME).unwrap(); + assert_ne!(old_mtu, 0); + + let new_mtu = old_mtu + 1; + ifcfg.set_mtu(DUMMY_IFACE_NAME, new_mtu).await.unwrap(); + + let mtu = NetlinkIfConfiger::mtu(DUMMY_IFACE_NAME).unwrap(); + assert_eq!(mtu, new_mtu); + + ifcfg + .set_link_status(DUMMY_IFACE_NAME, false) + .await + .unwrap(); + ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap(); + } + + #[serial_test::serial] + #[tokio::test] + async fn route_test() { + let _prepare_env = PrepareEnv::new(); + let ret = NetlinkIfConfiger::list_routes().unwrap(); + + let ifcfg = NetlinkIfConfiger {}; + println!("{:?}", ret); + + ifcfg.set_link_status(DUMMY_IFACE_NAME, true).await.unwrap(); + + ifcfg + .add_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24) + .await + .unwrap(); + + let routes = NetlinkIfConfiger::list_routes() + .unwrap() + .into_iter() + .map(Route::from) + .map(|x| x.destination) + .collect::>(); + assert!(routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap()))); + + ifcfg + .remove_ipv4_route(DUMMY_IFACE_NAME, "10.5.5.0".parse().unwrap(), 24) + .await + .unwrap(); + let routes = NetlinkIfConfiger::list_routes() + .unwrap() + .into_iter() + .map(Route::from) + .map(|x| x.destination) + .collect::>(); + assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap()))); + } +} diff --git a/easytier/src/common/ifcfg/route.rs b/easytier/src/common/ifcfg/route.rs new file mode 100644 index 0000000..5e428b0 --- /dev/null +++ b/easytier/src/common/ifcfg/route.rs @@ -0,0 +1,133 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Route { + /// Network address of the destination. `0.0.0.0` with a prefix of `0` is considered a default route. + pub destination: IpAddr, + + /// Length of network prefix in the destination address. + pub prefix: u8, + + /// The address of the next hop of this route. + /// + /// On macOS, this must be `Some` if ifindex is `None` + pub gateway: Option, + + /// The index of the local interface through which the next hop of this route may be reached. + /// + /// On macOS, this must be `Some` if gateway is `None` + pub ifindex: Option, + + #[cfg(target_os = "linux")] + /// The routing table this route belongs to. + pub table: u8, + + /// Network address of the source. + #[cfg(target_os = "linux")] + pub source: Option, + + /// Prefix length of the source address. + #[cfg(target_os = "linux")] + pub source_prefix: u8, + + /// Source address hint. Does not influence routing. + #[cfg(target_os = "linux")] + pub source_hint: Option, + + #[cfg(any(target_os = "windows", target_os = "linux"))] + /// The route metric offset value for this route. + pub metric: Option, + + #[cfg(target_os = "windows")] + /// Luid of the local interface through which the next hop of this route may be reached. + /// + /// If luid is specified, ifindex is optional. + pub luid: Option, +} + +impl Route { + /// Create a route that matches a given destination network. + /// + /// Either the gateway or interface should be set before attempting to add to a routing table. + pub fn new(destination: IpAddr, prefix: u8) -> Self { + Self { + destination, + prefix, + gateway: None, + ifindex: None, + #[cfg(target_os = "linux")] + // default to main table + table: 254, + #[cfg(target_os = "linux")] + source: None, + #[cfg(target_os = "linux")] + source_prefix: 0, + #[cfg(target_os = "linux")] + source_hint: None, + #[cfg(any(target_os = "windows", target_os = "linux"))] + metric: None, + #[cfg(target_os = "windows")] + luid: None, + } + } + + /// Set the next next hop gateway for this route. + pub fn with_gateway(mut self, gateway: IpAddr) -> Self { + self.gateway = Some(gateway); + self + } + + /// Set the index of the local interface through which the next hop of this route should be reached. + pub fn with_ifindex(mut self, ifindex: u32) -> Self { + self.ifindex = Some(ifindex); + self + } + + /// Set table the route will be installed in. + #[cfg(target_os = "linux")] + pub fn with_table(mut self, table: u8) -> Self { + self.table = table; + self + } + + /// Set source. + #[cfg(target_os = "linux")] + pub fn with_source(mut self, source: IpAddr, prefix: u8) -> Self { + self.source = Some(source); + self.source_prefix = prefix; + self + } + + /// Set source hint. + #[cfg(target_os = "linux")] + pub fn with_source_hint(mut self, hint: IpAddr) -> Self { + self.source_hint = Some(hint); + self + } + + /// Set route metric. + #[cfg(any(target_os = "windows", target_os = "linux"))] + pub fn with_metric(mut self, metric: u32) -> Self { + self.metric = Some(metric); + self + } + + /// Set luid of the local interface through which the next hop of this route should be reached. + #[cfg(target_os = "windows")] + pub fn with_luid(mut self, luid: u64) -> Self { + self.luid = Some(luid); + self + } + + /// Get the netmask covering the network portion of the destination address. + pub fn mask(&self) -> IpAddr { + match self.destination { + IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::from( + u32::MAX.checked_shl(32 - self.prefix as u32).unwrap_or(0), + )), + IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::from( + u128::MAX.checked_shl(128 - self.prefix as u32).unwrap_or(0), + )), + } + } +} diff --git a/easytier/src/common/ifcfg/windows.rs b/easytier/src/common/ifcfg/windows.rs new file mode 100644 index 0000000..3abdc2c --- /dev/null +++ b/easytier/src/common/ifcfg/windows.rs @@ -0,0 +1,166 @@ +use std::net::Ipv4Addr; + +use async_trait::async_trait; + +use super::{cidr_to_subnet_mask, run_shell_cmd, Error, IfConfiguerTrait}; + +pub struct WindowsIfConfiger {} + +impl WindowsIfConfiger { + pub fn get_interface_index(name: &str) -> Option { + crate::arch::windows::find_interface_index(name).ok() + } + + async fn list_ipv4(name: &str) -> Result, Error> { + use anyhow::Context; + use network_interface::NetworkInterfaceConfig; + use std::net::IpAddr; + let ret = network_interface::NetworkInterface::show().with_context(|| "show interface")?; + let addrs = ret + .iter() + .filter_map(|x| { + if x.name != name { + return None; + } + Some(x.addr.clone()) + }) + .flat_map(|x| x) + .map(|x| x.ip()) + .filter_map(|x| { + if let IpAddr::V4(ipv4) = x { + Some(ipv4) + } else { + None + } + }) + .collect::>(); + + Ok(addrs) + } + + async fn remove_one_ipv4(name: &str, ip: Ipv4Addr) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface ipv4 delete address {} address={}", + name, + ip.to_string() + ) + .as_str(), + ) + .await + } +} + +#[cfg(target_os = "windows")] +#[async_trait] +impl IfConfiguerTrait for WindowsIfConfiger { + async fn add_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let Some(idx) = Self::get_interface_index(name) else { + return Err(Error::NotFound); + }; + run_shell_cmd( + format!( + "route ADD {} MASK {} 10.1.1.1 IF {} METRIC 9000", + address, + cidr_to_subnet_mask(cidr_prefix), + idx + ) + .as_str(), + ) + .await + } + + async fn remove_ipv4_route( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + let Some(idx) = Self::get_interface_index(name) else { + return Err(Error::NotFound); + }; + run_shell_cmd( + format!( + "route DELETE {} MASK {} IF {}", + address, + cidr_to_subnet_mask(cidr_prefix), + idx + ) + .as_str(), + ) + .await + } + + async fn add_ipv4_ip( + &self, + name: &str, + address: Ipv4Addr, + cidr_prefix: u8, + ) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface ipv4 add address {} address={} mask={}", + name, + address, + cidr_to_subnet_mask(cidr_prefix) + ) + .as_str(), + ) + .await + } + + async fn set_link_status(&self, name: &str, up: bool) -> Result<(), Error> { + run_shell_cmd( + format!( + "netsh interface set interface {} {}", + name, + if up { "enable" } else { "disable" } + ) + .as_str(), + ) + .await + } + + async fn remove_ip(&self, name: &str, ip: Option) -> Result<(), Error> { + if ip.is_none() { + for ip in Self::list_ipv4(name).await?.iter() { + Self::remove_one_ipv4(name, *ip).await?; + } + Ok(()) + } else { + Self::remove_one_ipv4(name, ip.unwrap()).await + } + } + + async fn wait_interface_show(&self, name: &str) -> Result<(), Error> { + Ok( + tokio::time::timeout(std::time::Duration::from_secs(10), async move { + loop { + if let Some(idx) = Self::get_interface_index(name) { + tracing::info!(?name, ?idx, "Interface found"); + break; + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + Ok::<(), Error>(()) + }) + .await??, + ) + } + + async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> { + let _ = run_shell_cmd( + format!("netsh interface ipv6 set subinterface {} mtu={}", name, mtu).as_str(), + ) + .await; + run_shell_cmd( + format!("netsh interface ipv4 set subinterface {} mtu={}", name, mtu).as_str(), + ) + .await + } +} diff --git a/easytier/src/tests/three_node.rs b/easytier/src/tests/three_node.rs index 03994ab..7e5bf07 100644 --- a/easytier/src/tests/three_node.rs +++ b/easytier/src/tests/three_node.rs @@ -526,10 +526,10 @@ pub async fn proxy_three_node_disconnect_test(#[values("tcp", "wg")] proto: &str .find(|r| r.peer_id == inst4.peer_id()) .is_none() }, - // 0 down - // [1, 6) send ping - // [3, 8) ping fail and close connection - Duration::from_millis(8300), + // 0 down, assume last packet is recv in -0.01 + // [2, 7) send ping + // [4, 9) ping fail and close connection + Duration::from_millis(9300), ) .await; set_link_status("net_d", true);