use crate::Message;
#[derive(Debug)]
pub struct Interface {
pub name: String,
pub is_up: bool,
pub index: u32,
pub master: Option<String>,
}
impl Interface {
pub fn from_enum(message: &Message) -> Option<Self> {
if let Message::InterfaceList(content) = message {
let mut it = content.split_ascii_whitespace();
let name = if let Some(s) = it.next() {
s
} else {
log::error!("ifc: unable to determine name in {content}");
return None;
};
let is_up = if let Some(s) = it.next() {
match s {
"up" => true,
"down" => false,
_ => {
log::error!("ifc: unknown state {s}");
return None;
}
}
} else {
log::error!("ifc: unable to determine state in {content}");
return None;
};
let mut index = -1_i32;
let mut master: Option<String> = None;
for s in it {
let s = s.trim_matches(|c: char| c == '(' || c == ')' || c == ' ');
if let Some(_idx) = s.strip_prefix("index=") {
index = _idx.parse().unwrap_or(-1);
} else if let Some(ms) = s.strip_prefix("master=") {
master = Some(ms.to_owned());
}
}
if index < 0 {
log::error!("ifc: did not find an appropriate index in {content}");
return None;
}
Some(Self {
name: name.into(),
is_up,
index: index as u32,
master,
})
} else {
log::error!("ifc: invoked Interface::from_enum on wrong message");
None
}
}
}
#[derive(Debug)]
pub struct InterfaceProperties {
pub iftype: InterfaceType,
flags: u32,
pub mtu: u32,
}
impl InterfaceProperties {
pub fn from_enum(message: &Message) -> Option<Self> {
if let Message::InterfaceFlags(content) = message {
let mut it = content.split_ascii_whitespace();
let mut flags = 0_u32;
let mut mtu = 0;
let iftype = if let Some(s) = it.next() {
match s {
"PtP" => InterfaceType::PointToPoint,
"MultiAccess" => InterfaceType::MultiAccess,
_ => InterfaceType::Unknown(s.to_owned()),
}
} else {
log::error!("ifc: did not find any iftype in {content}");
return None;
};
for token in content.split_ascii_whitespace() {
if let Some(_mtu) = token.strip_prefix("MTU=") {
if let Ok(m) = _mtu.parse::<u32>() {
mtu = m;
} else {
log::error!("ifc: found invalid mtu in line {content}");
return None;
}
} else {
match token {
"Broadcast" => flags |= IF_FLAG_BROADCAST,
"Multicast" => flags |= IF_FLAG_MULTICAST,
"AdminUp" => flags |= IF_FLAG_ADMIN_UP,
"AdminDown" => flags &= !IF_FLAG_ADMIN_UP,
"LinkUp" => flags |= IF_FLAG_LINK_UP,
"LinkDown" => flags &= !IF_FLAG_LINK_UP,
"Loopback" => flags |= IF_FLAG_LOOPBACK,
"Ignored" => flags |= IF_FLAG_IGNORED,
_ => {}
}
}
}
if mtu == 0 {
log::error!("ifc: did not find any iftype in {content}");
}
Some(InterfaceProperties { iftype, flags, mtu })
} else {
log::error!("ifc: invoked InterfaceProperties::from_enum on wrong message");
None
}
}
#[inline]
pub fn is_broadcast_set(&self) -> bool {
(self.flags & IF_FLAG_BROADCAST) != 0
}
#[inline]
pub fn is_multicast_set(&self) -> bool {
(self.flags & IF_FLAG_MULTICAST) != 0
}
#[inline]
pub fn is_admin_up(&self) -> bool {
(self.flags & IF_FLAG_ADMIN_UP) != 0
}
#[inline]
pub fn is_link_up(&self) -> bool {
(self.flags & IF_FLAG_LINK_UP) != 0
}
#[inline]
pub fn is_loopback(&self) -> bool {
(self.flags & IF_FLAG_LOOPBACK) != 0
}
#[inline]
pub fn is_ignored_for_routing(&self) -> bool {
(self.flags & IF_FLAG_IGNORED) != 0
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum InterfaceType {
PointToPoint,
MultiAccess,
Unknown(String),
}
#[derive(Debug)]
pub struct InterfaceAddress {
pub ip: String,
pub scope: String,
pub extra_info: Option<String>,
}
impl InterfaceAddress {
pub fn from_enum(message: &Message) -> Option<Vec<Self>> {
let mut addresses = vec![];
if let Message::InterfaceAddress(content) = message {
for line in content.lines() {
let mut it = line.split_ascii_whitespace();
let mut scope = "undef";
let mut extras = String::with_capacity(32);
let ip = if let Some(s) = it.next() {
s
} else {
log::error!("ifc: failed to find ip address in {line}");
return None;
};
let bc = |c| c == '(' || c == ')' || c == ' ';
while let Some(mut s) = it.next() {
s = s.trim_matches(bc);
if s == "scope" {
if let Some(sc) = it.next() {
scope = sc.trim_matches(bc).trim_matches(',');
} else {
log::error!("ifc: encountered scope but not value in {line}");
return None;
}
} else {
if !extras.is_empty() {
extras.push(' ');
}
extras.push_str(s);
}
}
if !extras.is_empty() {
extras = extras.trim_matches(',').into();
}
addresses.push(InterfaceAddress {
ip: ip.into(),
scope: scope.into(),
extra_info: if extras.is_empty() {
None
} else {
Some(extras)
},
})
}
Some(addresses)
} else {
log::error!("ifc: invoked InterfaceAddress::from_enum on wrong message");
None
}
}
}
pub struct InterfaceSummary {
pub name: String,
pub state: String,
pub ipv4_address: Option<String>,
pub ipv6_address: Option<String>,
}
impl InterfaceSummary {
pub fn from_enum(message: &Message) -> Option<Vec<Self>> {
if let Message::InterfaceSummary(content) = message {
let mut entries: Vec<Self> = vec![];
for line in content.lines() {
let mut it = line.split_ascii_whitespace();
let name: String = it.next()?.into();
let state: String = it.next()?.into();
let mut ipv4_address = None;
let mut ipv6_address = None;
for addr in it {
if addr.contains(':') {
ipv6_address = Some(addr.to_owned());
} else {
ipv4_address = Some(addr.to_owned());
}
}
entries.push(InterfaceSummary {
name,
state,
ipv4_address,
ipv6_address,
})
}
Some(entries)
} else {
log::error!("ifc: invoked InterfaceSummary::from_enum on wrong message");
None
}
}
}
const IF_FLAG_BROADCAST: u32 = 1 << 2;
const IF_FLAG_MULTICAST: u32 = 1 << 3;
const IF_FLAG_LOOPBACK: u32 = 1 << 5;
const IF_FLAG_IGNORED: u32 = 1 << 6;
const IF_FLAG_ADMIN_UP: u32 = 1 << 7;
const IF_FLAG_LINK_UP: u32 = 1 << 8;
#[cfg(test)]
mod tests {
use super::*;
use crate::Message;
#[test]
#[ignore]
fn test_invalid() {
let _ = env_logger::try_init();
assert!(
Interface::from_enum(&Message::Ok).is_none(),
"expected None from parsing invalid message type",
);
}
#[test]
fn test_interface_parsing_without_master() {
let _ = env_logger::try_init();
let message = Message::InterfaceList("eth0 up (index=2)".into());
let ifc = Interface::from_enum(&message).expect("failed to parse");
assert_eq!(ifc.name, "eth0");
assert!(ifc.is_up);
assert_eq!(ifc.index, 2);
assert!(ifc.master.is_none(), "was not expecting master");
}
#[test]
fn test_interface_parsing_with_master() {
let _ = env_logger::try_init();
let message = Message::InterfaceList("eth1 down (index=3 master=#2)".into());
let ifc = Interface::from_enum(&message).expect("failed to parse");
assert_eq!(ifc.name, "eth1");
assert!(!ifc.is_up);
assert_eq!(ifc.index, 3);
assert_eq!(ifc.master.expect("was expecting master"), "#2");
}
#[test]
fn test_interface_properties() {
let _ = env_logger::try_init();
let message = Message::InterfaceFlags(
"MultiAccess Broadcast Multicast AdminDown LinkUp MTU=9000".into(),
);
let props = InterfaceProperties::from_enum(&message).expect("failed to parse");
assert_eq!(props.iftype, InterfaceType::MultiAccess);
assert_eq!(props.mtu, 9000);
assert!(props.is_broadcast_set());
assert!(props.is_multicast_set());
assert!(!props.is_admin_up());
assert!(props.is_link_up());
}
#[test]
fn test_interface_address() {
let _ = env_logger::try_init();
let content = "\t172.30.0.12/16 (Preferred, scope site)\n\t172.29.1.15/32 (scope univ)\n\t172.29.1.16/32 (scope univ)\n\t172.29.1.17/32 (scope univ)\n\tfe80::4495:80ff:fe71:a791/64 (Preferred, scope link)\n\tfe80::4490::72/64 (scope univ)";
let message = Message::InterfaceAddress(content.into());
let addresses = InterfaceAddress::from_enum(&message).expect("failed to parse");
validate_address(&addresses[0], "172.30.0.12/16", "site", "Preferred");
validate_address(&addresses[1], "172.29.1.15/32", "univ", "");
validate_address(&addresses[2], "172.29.1.16/32", "univ", "");
validate_address(&addresses[3], "172.29.1.17/32", "univ", "");
validate_address(
&addresses[4],
"fe80::4495:80ff:fe71:a791/64",
"link",
"Preferred",
);
validate_address(&addresses[5], "fe80::4490::72/64", "univ", "");
}
#[test]
fn test_interface_summary() {
let _ = env_logger::try_init();
let content = "lo up 127.0.0.1/8 ::1/128\neth0 up 172.30.0.12/16 fe80::4495:80ff:fe71:a791/64\neth1 up 169.254.199.2/30";
let message = Message::InterfaceSummary(content.into());
let summaries = InterfaceSummary::from_enum(&message).expect("failed to parse");
assert_eq!(summaries[0].name, "lo");
assert_eq!(summaries[0].state, "up");
assert_eq!(summaries[0].ipv4_address.as_ref().unwrap(), "127.0.0.1/8");
assert_eq!(summaries[0].ipv6_address.as_ref().unwrap(), "::1/128");
assert_eq!(summaries[1].name, "eth0");
assert_eq!(summaries[1].state, "up");
assert_eq!(
summaries[1].ipv4_address.as_ref().unwrap(),
"172.30.0.12/16",
);
assert_eq!(
summaries[1].ipv6_address.as_ref().unwrap(),
"fe80::4495:80ff:fe71:a791/64",
);
assert_eq!(summaries[2].name, "eth1");
assert_eq!(summaries[2].state, "up");
assert_eq!(
summaries[2].ipv4_address.as_ref().unwrap(),
"169.254.199.2/30",
);
assert!(summaries[2].ipv6_address.is_none());
}
fn validate_address(address: &InterfaceAddress, ip: &str, scope: &str, extras: &str) {
assert_eq!(address.ip, ip);
assert_eq!(address.scope, scope);
if let Some(ref ei) = address.extra_info {
assert_eq!(ei, extras)
} else {
assert_eq!(extras, "", "expected empty extra_info");
}
}
}