use crate::libc_getips::get_iface_addrs;
use crate::Provider;
use crate::{Error, IpType, Result};
use async_trait::async_trait;
use log::{debug, info};
use std::net::IpAddr;
use std::process::{Output, Stdio};
use std::str::FromStr;
use tokio::process::Command;
#[derive(Debug, Clone)]
pub struct LocalIpv6CommandProvider {
nic: String,
prefer_permanent: bool,
}
impl LocalIpv6CommandProvider {
#[must_use]
pub fn new(nic: &str, permanent: bool) -> Self {
Self {
nic: nic.to_string(),
prefer_permanent: permanent,
}
}
}
#[async_trait]
impl Provider for LocalIpv6CommandProvider {
async fn get_addr(&self) -> Result<IpAddr> {
let out = chain_ip_cmd_until_succeed(&self.nic).await?;
let out_br = out.stdout.split(|c| *c == b'\n');
let mut addrs: Vec<(IpAddr, bool)> = Vec::with_capacity(4);
for line in out_br {
let line = String::from_utf8(line.to_vec())?;
let fields: Vec<String> = line.split_whitespace().map(ToString::to_string).collect();
if fields.len() > 1 && fields[0] == "inet6" {
let address_stripped = match fields[1].split_once('/') {
Some((addr, _prefixlen)) => addr,
None => &fields[1],
};
if let Ok(addr6) = IpAddr::from_str(address_stripped) {
let is_perm = fields.iter().any(|f| f == "secured" || f == "mngtmpaddr")
&& fields.iter().all(|f| f != "temporary");
if !addr6.is_loopback() {
addrs.push((addr6, is_perm));
}
}
}
}
if addrs.is_empty() {
debug!("Short-circuting NoAddress because an ip command succeeded without addresses");
Err(Error::NoAddress)
} else {
Ok(addrs
.iter()
.filter(|(_, is_perm)| *is_perm == self.prefer_permanent)
.map(|(addr, _)| *addr)
.next()
.unwrap_or(addrs[0].0))
}
}
fn get_type(&self) -> IpType {
IpType::Ipv6
}
}
async fn chain_ip_cmd_until_succeed(nic: &str) -> Result<Output> {
let commands = [
(
"ip",
vec![
"address", "show", "dev", nic,
"scope", "global",
],
),
("ifconfig", vec!["-L", nic, "inet6"]),
("ifconfig", vec![nic]),
];
let mut last_error: Option<Error> = None;
for (cmd, args) in commands {
let mut command = Command::new(cmd);
command.stdout(Stdio::piped());
debug!("Running command {cmd:?} with arguments {args:?}");
let output = command.args(&args).output().await;
match output {
Ok(output) => {
if output.status.success() {
return Ok(output);
}
debug!(
"Command {:?} failed with status: {}",
command, output.status
);
last_error = Some(Error::NonZeroExit(output.status));
}
Err(exec_error) => {
debug!("Command {command:?} failed to be executed: {exec_error}");
last_error = Some(Error::IoError(exec_error));
}
}
}
info!("None of the commands to extract the IPv6 address succeeded.");
Err(last_error.unwrap())
}
macro_rules! cast_ipv4 {
($id: expr) => {
if let IpAddr::V4(ip) = $id {
ip
} else {
unreachable!()
}
};
}
macro_rules! cast_ipv6 {
($id: expr) => {
if let IpAddr::V6(ip) = $id {
ip
} else {
unreachable!()
}
};
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn filter_nonroute_ipv4(addr: &&IpAddr) -> bool {
let remove = !addr.is_loopback() && !cast_ipv4!(addr).is_link_local() && !addr.is_unspecified();
if remove {
debug!("Removing address {addr:?} because it is loopback/link local/unspecified");
}
remove
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn filter_nonroute_ipv6(addr: &&IpAddr) -> bool {
!addr.is_loopback()
&& !addr.is_unspecified()
&& (cast_ipv6!(addr).segments()[0] & 0xffc0) != 0xfe80
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn filter_nonlocal_ipv4(addr: &&&IpAddr) -> bool {
!cast_ipv4!(addr).is_private()
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn filter_nonlocal_ipv6(_addr: &&&IpAddr) -> bool {
false
}
#[derive(Debug, Clone)]
pub struct LocalLibcProvider {
nic: Option<String>,
ip_type: IpType,
}
impl LocalLibcProvider {
#[must_use]
pub fn new(nic: Option<&str>, ip_type: IpType) -> Self {
Self {
nic: nic.map(ToString::to_string),
ip_type,
}
}
}
#[async_trait]
impl Provider for LocalLibcProvider {
async fn get_addr(&self) -> Result<IpAddr> {
let addrs = get_iface_addrs(Some(self.ip_type), self.nic.as_deref())?;
let addrs: Vec<&IpAddr> = addrs
.iter()
.filter(if self.ip_type == IpType::Ipv4 {
filter_nonroute_ipv4
} else {
filter_nonroute_ipv6
})
.collect();
let first_non_local_addr: Option<&&IpAddr> =
addrs.iter().find(if self.ip_type == IpType::Ipv4 {
filter_nonlocal_ipv4
} else {
filter_nonlocal_ipv6
});
first_non_local_addr.map_or_else(|| Ok(*addrs[0]), |addr| Ok(**addr))
}
fn get_type(&self) -> IpType {
self.ip_type
}
}