Files
EasyTier/easytier/src/common/ifcfg/mod.rs
T
KKRainbow 571d416e71 fix(tun): preserve shared NIC ownership and failure invariants
Keep mobile address and route claims in dispatcher tables without
applying desktop interface configuration to synthetic TUN names.

Limit the Magic DNS overlap exemption to route claims and remove the
shared route retry that bypassed claim accounting after invalidation.

Invalidate and stop dispatchers on failed mobile TUN replacement or I/O.
Bound TUN writes so stalled devices release pending source updates and
member cleanup instead of retaining an unusable shared dispatcher.

Treat missing Darwin and Windows addresses and routes as successful
deletions while preserving permission and other configuration errors.

Add regressions for claim conflicts, mobile configuration, failed TUN
replacement, stalled writes, and platform deletion error handling.
2026-10-13 10:23:08 +08:00

328 lines
9.9 KiB
Rust

#![cfg_attr(
not(any(feature = "public-ipv6-provider", feature = "tun")),
allow(dead_code)
)]
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd"
))]
mod darwin;
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
mod netlink;
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
mod netlink_wire;
#[cfg(target_os = "windows")]
mod win;
#[cfg(target_os = "windows")]
mod windows;
use std::net::{Ipv4Addr, Ipv6Addr};
use async_trait::async_trait;
use cidr::{Ipv4Inet, Ipv6Inet};
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd"
))]
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,
_cost: Option<i32>,
) -> Result<(), Error> {
Ok(())
}
async fn add_ipv4_route_with_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.add_ipv4_route(name, address, cidr_prefix, cost).await
}
async fn remove_ipv4_route(
&self,
_name: &str,
_address: Ipv4Addr,
_cidr_prefix: u8,
) -> Result<(), Error> {
Ok(())
}
async fn remove_ipv4_route_with_cost_and_source_hint(
&self,
name: &str,
address: Ipv4Addr,
cidr_prefix: u8,
_cost: Option<i32>,
_source_hint: Option<Ipv4Addr>,
) -> Result<(), Error> {
self.remove_ipv4_route(name, address, cidr_prefix).await
}
async fn add_ipv4_ip(
&self,
_name: &str,
_address: Ipv4Addr,
_cidr_prefix: u8,
) -> Result<(), Error> {
Ok(())
}
async fn add_ipv6_route(
&self,
_name: &str,
_address: Ipv6Addr,
_cidr_prefix: u8,
_cost: Option<i32>,
) -> Result<(), Error> {
Ok(())
}
async fn remove_ipv6_route(
&self,
_name: &str,
_address: Ipv6Addr,
_cidr_prefix: u8,
) -> Result<(), Error> {
Ok(())
}
async fn add_ipv6_ip(
&self,
_name: &str,
_address: Ipv6Addr,
_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<Ipv4Inet>) -> Result<(), Error> {
Ok(())
}
async fn remove_ipv6(&self, _name: &str, _ip: Option<Ipv6Inet>) -> 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(())
}
}
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd"
))]
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,
)
}
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd"
))]
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::string::utf8_or_gbk_to_string(cmd_out.stdout.as_slice());
stderr = crate::utils::string::utf8_or_gbk_to_string(cmd_out.stderr.as_slice());
};
#[cfg(not(target_os = "windows"))]
{
cmd_out = Command::new("sh")
.env("LC_ALL", "C")
.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(())
}
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd",
test
))]
fn ignore_removed_shell_cmd_not_found(result: Result<(), Error>) -> Result<(), Error> {
match result {
Err(Error::ShellCommandError(output))
if !output.trim().is_empty()
&& output
.lines()
.filter(|line| !line.trim().is_empty())
.all(|line| {
(line.starts_with("ifconfig: ")
&& line.ends_with(": Can't assign requested address"))
|| (line.starts_with("delete ") && line.ends_with(": not in table"))
|| line == "route: route has not been found"
|| line == "route: writing to routing socket: not in table"
|| line == "route: writing to routing socket: No such process"
}) =>
{
Ok(())
}
other => other,
}
}
#[derive(Default)]
pub struct DummyIfConfiger {}
#[async_trait]
impl IfConfiguerTrait for DummyIfConfiger {}
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub type IfConfiger = netlink::NetlinkIfConfiger;
#[cfg(all(target_os = "linux", not(feature = "linux-netlink")))]
pub type IfConfiger = DummyIfConfiger;
#[cfg(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "freebsd"
))]
pub type IfConfiger = darwin::MacIfConfiger;
#[cfg(target_os = "windows")]
pub type IfConfiger = windows::WindowsIfConfiger;
#[cfg(not(any(
all(target_os = "macos", not(feature = "macos-ne")),
target_os = "linux",
target_os = "windows",
target_os = "freebsd",
)))]
pub type IfConfiger = DummyIfConfiger;
#[cfg(target_os = "windows")]
pub use windows::RegistryManager;
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) use netlink_wire::RouteMessage;
#[cfg(all(
target_os = "linux",
feature = "linux-netlink",
feature = "public-ipv6-provider"
))]
pub(crate) use netlink_wire::RouteType;
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
netlink::NetlinkIfConfiger::list_ipv6_route_messages()
}
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
netlink::NetlinkIfConfiger::get_interface_index(name)
}
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) fn add_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
netlink::NetlinkIfConfiger::add_ipv6_ndp_proxy(name, address)
}
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) fn remove_ipv6_ndp_proxy(name: &str, address: Ipv6Addr) -> Result<(), Error> {
netlink::NetlinkIfConfiger::remove_ipv6_ndp_proxy(name, address)
}
#[cfg(all(target_os = "linux", feature = "linux-netlink"))]
pub(crate) fn list_ipv6_ndp_proxy(
name: &str,
) -> Result<std::collections::BTreeSet<Ipv6Addr>, Error> {
netlink::NetlinkIfConfiger::list_ipv6_ndp_proxy(name)
}
#[cfg(test)]
mod tests {
use super::{Error, ignore_removed_shell_cmd_not_found};
#[test]
fn removed_shell_cmd_ignores_missing_addresses_and_routes() {
for output in [
"ifconfig: ioctl (SIOCDIFADDR): Can't assign requested address\n",
"ifconfig: ioctl (SIOCDIFADDR_IN6): Can't assign requested address\n",
"delete net 10.0.0.0: gateway tun0: not in table\n",
"delete net 10.0.0.0: gateway tun0 fib 0: not in table\nroute: route has not been found\n",
"delete net 10.0.0.0: gateway tun0 fib 0: not in table\nroute: writing to routing socket: No such process\n",
"delete net 10.0.0.0: gateway tun0: not in table\nroute: writing to routing socket: not in table\n",
] {
assert!(
ignore_removed_shell_cmd_not_found(Err(Error::ShellCommandError(
output.to_owned()
)))
.is_ok()
);
}
}
#[test]
fn removed_shell_cmd_preserves_other_errors() {
for output in [
"ifconfig: ioctl (SIOCDIFADDR): Operation not permitted\n",
"route: writing to routing socket: Permission denied\n",
"route: route has not been found: Permission denied\n",
"route: route has not been found: No such process\n",
"delete net 10.0.0.0: gateway tun0: not in table\nroute: Permission denied\n",
"delete net 10.0.0.0: gateway tun0: not in table\nroute: writing to routing socket: No such process\nroute: Permission denied\n",
"unexpected command: not in table\n",
"",
] {
assert!(matches!(
ignore_removed_shell_cmd_not_found(Err(Error::ShellCommandError(output.to_owned()))),
Err(Error::ShellCommandError(actual)) if actual == output
));
}
assert!(matches!(
ignore_removed_shell_cmd_not_found(Err(std::io::Error::from(
std::io::ErrorKind::PermissionDenied
)
.into())),
Err(Error::IOError(_))
));
}
}