use netlink instead of shell cmd to config ip (#593)
This commit is contained in:
1
.github/workflows/core.yml
vendored
1
.github/workflows/core.yml
vendored
@@ -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: |
|
||||
|
||||
35
Cargo.lock
generated
35
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<Ipv4Addr>) -> 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<Ipv4Addr>) -> 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<Ipv4Addr>) -> 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<u32> {
|
||||
crate::arch::windows::find_interface_index(name).ok()
|
||||
}
|
||||
|
||||
async fn list_ipv4(name: &str) -> Result<Vec<Ipv4Addr>, 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::<Vec<_>>();
|
||||
|
||||
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<Ipv4Addr>) -> 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;
|
||||
81
easytier/src/common/ifcfg/darwin.rs
Normal file
81
easytier/src/common/ifcfg/darwin.rs
Normal file
@@ -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<Ipv4Addr>) -> 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
|
||||
}
|
||||
}
|
||||
127
easytier/src/common/ifcfg/mod.rs
Normal file
127
easytier/src/common/ifcfg/mod.rs
Normal file
@@ -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<Ipv4Addr>) -> 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;
|
||||
569
easytier/src/common/ifcfg/netlink.rs
Normal file
569
easytier/src/common/ifcfg/netlink.rs
Normal file
@@ -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<std::net::UdpSocket, Error> {
|
||||
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<T: NetlinkDeserializable + NetlinkSerializable + Debug>(
|
||||
req: T,
|
||||
flags: u16,
|
||||
) -> Result<Socket, Error> {
|
||||
let mut socket = Socket::new(NETLINK_ROUTE)?;
|
||||
socket.bind_auto()?;
|
||||
socket.connect(&SocketAddr::new(0, 0))?;
|
||||
|
||||
let mut req: NetlinkMessage<T> =
|
||||
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<T: NetlinkDeserializable + NetlinkSerializable + Debug>(
|
||||
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::<T>::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<IpAddr> {
|
||||
match addr {
|
||||
RouteAddress::Inet(addr) => Some(addr.into()),
|
||||
RouteAddress::Inet6(addr) => Some(addr.into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RouteMessage> 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<u32, Error> {
|
||||
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<u8, Error> {
|
||||
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>(RouteNetlinkMessage::DelAddress(
|
||||
message,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn mtu_op<T: TryInto<Ioctl>>(
|
||||
name: &str,
|
||||
op: T,
|
||||
value: libc::c_int,
|
||||
) -> Result<u32, Error>
|
||||
where
|
||||
<T as TryInto<Ioctl>>::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<u32, Error> {
|
||||
Self::mtu_op(name, SIOCGIFMTU, 0)
|
||||
}
|
||||
|
||||
pub fn list_addresses(name: &str) -> Result<Vec<IpInet>, 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<T: TryInto<Ioctl>>(
|
||||
name: &str,
|
||||
op: T,
|
||||
flags: InterfaceFlags,
|
||||
) -> Result<InterfaceFlags, Error>
|
||||
where
|
||||
<T as TryInto<Ioctl>>::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<InterfaceFlags, Error> {
|
||||
Self::set_flags_op(name, SIOCSIFFLAGS, flags)
|
||||
}
|
||||
|
||||
pub(crate) fn get_flags(name: &str) -> Result<InterfaceFlags, Error> {
|
||||
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
|
||||
}
|
||||
|
||||
fn list_routes() -> Result<Vec<RouteMessage>, 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::<u8>::new();
|
||||
loop {
|
||||
if resp.len() == 0 {
|
||||
let (new_resp, _) = s.recv_from_full()?;
|
||||
resp = new_resp;
|
||||
}
|
||||
let ret = NetlinkMessage::<RouteNetlinkMessage>::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>(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<Ipv4Addr>) -> 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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap())));
|
||||
}
|
||||
}
|
||||
133
easytier/src/common/ifcfg/route.rs
Normal file
133
easytier/src/common/ifcfg/route.rs
Normal file
@@ -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<IpAddr>,
|
||||
|
||||
/// 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<u32>,
|
||||
|
||||
#[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<IpAddr>,
|
||||
|
||||
/// 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<IpAddr>,
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
/// The route metric offset value for this route.
|
||||
pub metric: Option<u32>,
|
||||
|
||||
#[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<u64>,
|
||||
}
|
||||
|
||||
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),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
166
easytier/src/common/ifcfg/windows.rs
Normal file
166
easytier/src/common/ifcfg/windows.rs
Normal file
@@ -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<u32> {
|
||||
crate::arch::windows::find_interface_index(name).ok()
|
||||
}
|
||||
|
||||
async fn list_ipv4(name: &str) -> Result<Vec<Ipv4Addr>, 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::<Vec<_>>();
|
||||
|
||||
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<Ipv4Addr>) -> 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user