nispor-1.2.19/.cargo_vcs_info.json0000644000000001450000000000100124400ustar { "git": { "sha1": "4e21b5f1a0f49b1acf24aed0812004b79d3b6484" }, "path_in_vcs": "src/lib" }nispor-1.2.19/Cargo.toml0000644000000031160000000000100104370ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.58" name = "nispor" version = "1.2.19" authors = ["Gris Ge "] description = "Unified interface for Linux network state querying" homepage = "https://github.com/nispor/nispor" keywords = ["network"] categories = [ "network-programming", "os", ] license = "Apache-2.0" repository = "https://github.com/nispor/nispor" [lib] name = "nispor" crate-type = ["lib"] path = "lib.rs" [dependencies.ethtool] version = "0.2.5" [dependencies.futures] version = "0.3.21" [dependencies.libc] version = "0.2.126" [dependencies.log] version = "0.4.17" [dependencies.mptcp-pm] version = "0.1.3" [dependencies.netlink-packet-route] version = "0.19.0" [dependencies.netlink-packet-utils] version = "0.5.2" [dependencies.netlink-sys] version = "0.8.6" [dependencies.rtnetlink] version = "0.14.0" [dependencies.serde] version = "1.0.137" features = ["derive"] [dependencies.serde_json] version = "1.0.81" [dependencies.tokio] version = "1.19.2" features = [ "macros", "rt", ] [dev-dependencies.pretty_assertions] version = "1.2.1" [dev-dependencies.serde_yaml] version = "0.9" nispor-1.2.19/Cargo.toml.orig000064400000000000000000000015171046102023000141230ustar 00000000000000[package] name = "nispor" version = "1.2.19" authors = ["Gris Ge "] license = "Apache-2.0" edition = "2021" description = "Unified interface for Linux network state querying" homepage = "https://github.com/nispor/nispor" repository = "https://github.com/nispor/nispor" keywords = ["network"] categories = ["network-programming", "os"] rust-version = "1.58" [lib] name = "nispor" path = "lib.rs" crate-type = ["lib"] [dependencies] serde = { version = "1.0.137", features = ["derive"] } serde_json = "1.0.81" rtnetlink = "0.14.0" netlink-packet-route = "0.19.0" netlink-sys = "0.8.6" netlink-packet-utils = "0.5.2" ethtool = "0.2.5" mptcp-pm = "0.1.3" tokio = { version = "1.19.2", features = ["macros", "rt"] } futures = "0.3.21" libc = "0.2.126" log = "0.4.17" [dev-dependencies] serde_yaml = "0.9" pretty_assertions = "1.2.1" nispor-1.2.19/conf/bond.rs000064400000000000000000000024351046102023000134510ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use netlink_packet_route::link::{ InfoBond, InfoData, InfoKind, LinkAttribute, LinkInfo, }; use rtnetlink::Handle; use serde::{Deserialize, Serialize}; use crate::{BondMode, NisporError}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BondConf { pub mode: Option, } impl BondConf { pub(crate) async fn create( &self, handle: &Handle, name: &str, ) -> Result<(), NisporError> { // Unlink bridge, rust-rtnetlink does not support bond creation out of // box. let mut req = handle.link().add(); let mutator = req.message_mut(); let mode = self.mode.unwrap_or_default(); let info = LinkAttribute::LinkInfo(vec![ LinkInfo::Kind(InfoKind::Bond), LinkInfo::Data(InfoData::Bond(vec![InfoBond::Mode(mode.into())])), ]); mutator.attributes.push(info); mutator .attributes .push(LinkAttribute::IfName(name.to_string())); match req.execute().await { Ok(_) => Ok(()), Err(e) => Err(NisporError::bug(format!( "Failed to create new bond '{}': {}", &name, e ))), } } } nispor-1.2.19/conf/bridge.rs000064400000000000000000000012061046102023000137560ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use rtnetlink::Handle; use serde::{Deserialize, Serialize}; use crate::NisporError; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BridgeConf {} impl BridgeConf { pub(crate) async fn create( handle: &Handle, name: &str, ) -> Result<(), NisporError> { match handle.link().add().bridge(name.to_string()).execute().await { Ok(_) => Ok(()), Err(e) => Err(NisporError::bug(format!( "Failed to create new bridge '{}': {}", &name, e ))), } } } nispor-1.2.19/conf/iface.rs000064400000000000000000000037021046102023000135740ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::{super::mac::mac_str_to_raw, inter_ifaces::change_ifaces}; use crate::{ BondConf, BridgeConf, Iface, IfaceState, IfaceType, IpConf, NisporError, VethConf, VlanConf, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct IfaceConf { pub name: String, #[serde(default = "default_iface_state_in_conf")] pub state: IfaceState, #[serde(rename = "type")] pub iface_type: Option, pub controller: Option, pub ipv4: Option, pub ipv6: Option, pub mac_address: Option, pub veth: Option, pub bridge: Option, pub vlan: Option, pub bond: Option, } impl IfaceConf { pub async fn apply(&self, cur_iface: &Iface) -> Result<(), NisporError> { log::warn!( "WARN: IfaceConf::apply() is deprecated, \ please use NetConf::apply() instead" ); let ifaces = vec![self]; let mut cur_ifaces = HashMap::new(); cur_ifaces.insert(self.name.to_string(), cur_iface.clone()); change_ifaces(&ifaces, &cur_ifaces).await } } fn default_iface_state_in_conf() -> IfaceState { IfaceState::Up } pub(crate) async fn change_iface_state( handle: &rtnetlink::Handle, index: u32, up: bool, ) -> Result<(), NisporError> { if up { handle.link().set(index).up().execute().await?; } else { handle.link().set(index).down().execute().await?; } Ok(()) } pub(crate) async fn change_iface_mac( handle: &rtnetlink::Handle, index: u32, mac_address: &str, ) -> Result<(), NisporError> { change_iface_state(handle, index, false).await?; handle .link() .set(index) .address(mac_str_to_raw(mac_address)?) .execute() .await?; Ok(()) } nispor-1.2.19/conf/inter_ifaces.rs000064400000000000000000000156431046102023000151670ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use rtnetlink::new_connection; use super::{ iface::{change_iface_mac, change_iface_state}, ip::change_ips, }; use crate::{ BondConf, BridgeConf, Iface, IfaceConf, IfaceState, IfaceType, NisporError, VlanConf, }; pub(crate) async fn delete_ifaces( ifaces: &[(&str, u32)], ) -> Result<(), NisporError> { let (connection, handle, _) = new_connection()?; tokio::spawn(connection); for (iface_name, iface_index) in ifaces { if let Err(e) = handle.link().del(*iface_index).execute().await { return Err(NisporError::bug(format!( "Failed to delete interface {iface_name} \ with index {iface_index}: {e}" ))); } } Ok(()) } pub(crate) async fn create_ifaces( ifaces: &[&IfaceConf], cur_iface_name_2_index: &HashMap, ) -> Result<(), NisporError> { let (connection, handle, _) = new_connection()?; tokio::spawn(connection); for iface in ifaces { log::debug!("Creating interface {}", iface.name); match iface.iface_type { Some(IfaceType::Bridge) => { BridgeConf::create(&handle, &iface.name).await?; } Some(IfaceType::Veth) => { if let Some(veth_conf) = &iface.veth { veth_conf.create(&handle, &iface.name).await?; } } Some(IfaceType::Bond) => { if let Some(bond_conf) = iface.bond.as_ref() { bond_conf.create(&handle, &iface.name).await?; } else { BondConf::default().create(&handle, &iface.name).await?; } } Some(IfaceType::Vlan) => { if let Some(vlan_conf) = &iface.vlan { if let Some(base_iface_index) = cur_iface_name_2_index.get(&vlan_conf.base_iface) { VlanConf::create( &handle, &iface.name, vlan_conf.vlan_id, *base_iface_index, ) .await?; } else { return Err(NisporError::invalid_argument(format!( "Base interface {} for VLAN {} not found", &vlan_conf.base_iface, iface.name ))); } } } Some(_) => { return Err(NisporError::invalid_argument(format!( "Cannot create unsupported interface {:?}", &iface ))); } None => { return Err(NisporError::invalid_argument(format!( "No interface type defined for new interface {:?}", &iface ))); } } } Ok(()) } pub(crate) async fn change_ifaces( ifaces: &[&IfaceConf], cur_ifaces: &HashMap, ) -> Result<(), NisporError> { let (connection, handle, _) = new_connection()?; tokio::spawn(connection); for iface in ifaces { log::debug!("Changing interface {}", iface.name); } change_ifaces_mac(&handle, ifaces, cur_ifaces).await?; change_ifaces_controller(&handle, ifaces, cur_ifaces).await?; change_ifaces_state(&handle, ifaces, cur_ifaces).await?; change_ips(&handle, ifaces, cur_ifaces).await?; Ok(()) } async fn change_ifaces_state( handle: &rtnetlink::Handle, ifaces: &[&IfaceConf], cur_ifaces: &HashMap, ) -> Result<(), NisporError> { for iface in ifaces { if let Some(cur_iface) = cur_ifaces.get(&iface.name) { if cur_iface.state != iface.state { if iface.state == IfaceState::Up { change_iface_state(handle, cur_iface.index, true).await?; } else if iface.state == IfaceState::Down { change_iface_state(handle, cur_iface.index, false).await?; } else { return Err(NisporError::invalid_argument(format!( "Unsupported interface state in NetConf: {}", iface.state ))); } } } } Ok(()) } async fn change_ifaces_controller( handle: &rtnetlink::Handle, ifaces: &[&IfaceConf], cur_ifaces: &HashMap, ) -> Result<(), NisporError> { for iface in ifaces { if let Some(cur_iface) = cur_ifaces.get(&iface.name) { if cur_iface.controller != iface.controller { match &iface.controller { Some(ref ctrl_name) => match cur_ifaces.get(ctrl_name) { None => { return Err(NisporError::invalid_argument( format!( "Controller interface {} not found", &ctrl_name ), )); } Some(ctrl_iface) => { handle .link() .set(cur_iface.index) .controller(ctrl_iface.index) .execute() .await?; } }, None => { handle .link() .set(cur_iface.index) .nocontroller() .execute() .await?; } } } } else { return Err(NisporError::invalid_argument(format!( "Interface {} not found", iface.name ))); } } Ok(()) } async fn change_ifaces_mac( handle: &rtnetlink::Handle, ifaces: &[&IfaceConf], cur_ifaces: &HashMap, ) -> Result<(), NisporError> { for iface in ifaces { if let Some(mac_addr) = &iface.mac_address { if let Some(cur_iface) = cur_ifaces.get(&iface.name) { if cur_iface.state != IfaceState::Down { // We can only change MAC address when link down change_iface_state(handle, cur_iface.index, false).await?; } change_iface_mac(handle, cur_iface.index, mac_addr).await?; if cur_iface.state == IfaceState::Up && iface.state == IfaceState::Up { // Restore the interface state change_iface_state(handle, cur_iface.index, true).await?; } } } } Ok(()) } nispor-1.2.19/conf/ip.rs000064400000000000000000000147211046102023000131400ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::net::IpAddr; use std::str::FromStr; use netlink_packet_route::{ address::{AddressAttribute, AddressMessage, CacheInfo}, AddressFamily, }; use serde::{Deserialize, Serialize}; use super::super::query::is_ipv6_addr; use crate::{Iface, IfaceConf, IpFamily, Ipv4Info, Ipv6Info, NisporError}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct IpConf { pub addresses: Vec, } impl From<&Ipv4Info> for IpConf { fn from(info: &Ipv4Info) -> Self { let mut addrs = Vec::new(); for addr_info in &info.addresses { if addr_info.valid_lft == "forever" { addrs.push(IpAddrConf { remove: false, address: addr_info.address.clone(), prefix_len: addr_info.prefix_len, preferred_lft: addr_info.preferred_lft.clone(), valid_lft: addr_info.valid_lft.clone(), }); } } Self { addresses: addrs } } } impl From<&Ipv6Info> for IpConf { fn from(info: &Ipv6Info) -> Self { let mut addrs = Vec::new(); for addr_info in &info.addresses { if addr_info.valid_lft == "forever" { addrs.push(IpAddrConf { remove: false, address: addr_info.address.clone(), prefix_len: addr_info.prefix_len, preferred_lft: addr_info.preferred_lft.clone(), valid_lft: addr_info.valid_lft.clone(), }); } } Self { addresses: addrs } } } #[derive( Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Clone, Default, )] #[non_exhaustive] pub struct IpAddrConf { #[serde(default)] pub remove: bool, pub address: String, pub prefix_len: u8, #[serde(default)] pub valid_lft: String, #[serde(default)] pub preferred_lft: String, } impl IpConf { pub async fn apply( &self, handle: &rtnetlink::Handle, cur_iface: &Iface, family: IpFamily, ) -> Result<(), NisporError> { log::warn!("WARN: Deprecated, please use NetConf::apply() instead"); let iface = match family { IpFamily::Ipv4 => IfaceConf { ipv4: Some(self.clone()), ..Default::default() }, IpFamily::Ipv6 => IfaceConf { ipv6: Some(self.clone()), ..Default::default() }, }; let ifaces = vec![&iface]; let mut cur_ifaces = HashMap::new(); cur_ifaces.insert(cur_iface.name.clone(), cur_iface.clone()); change_ips(handle, &ifaces, &cur_ifaces).await } } pub(crate) async fn change_ips( handle: &rtnetlink::Handle, ifaces: &[&IfaceConf], cur_ifaces: &HashMap, ) -> Result<(), NisporError> { for iface in ifaces { if let Some(cur_iface) = cur_ifaces.get(&iface.name) { if let Some(ip_conf) = iface.ipv4.as_ref() { apply_ip_conf(handle, cur_iface.index, ip_conf, IpFamily::Ipv4) .await?; } if let Some(ip_conf) = iface.ipv6.as_ref() { apply_ip_conf(handle, cur_iface.index, ip_conf, IpFamily::Ipv6) .await?; } } } Ok(()) } async fn apply_ip_conf( handle: &rtnetlink::Handle, iface_index: u32, ip_conf: &IpConf, ip_family: IpFamily, ) -> Result<(), NisporError> { for addr_conf in &ip_conf.addresses { if addr_conf.remove { let mut nl_msg = AddressMessage::default(); nl_msg.header.index = iface_index; nl_msg.header.prefix_len = addr_conf.prefix_len; nl_msg.header.family = match ip_family { IpFamily::Ipv4 => AddressFamily::Inet, IpFamily::Ipv6 => AddressFamily::Inet6, }; nl_msg.attributes.push(AddressAttribute::Address( ip_addr_str_to_enum(&addr_conf.address)?, )); if let Err(e) = handle.address().del(nl_msg).execute().await { if let rtnetlink::Error::NetlinkError(ref e) = e { if e.raw_code() == -libc::EADDRNOTAVAIL { return Ok(()); } } return Err(e.into()); } } else { let mut req = handle .address() .add( iface_index, ip_addr_str_to_enum(&addr_conf.address)?, addr_conf.prefix_len, ) .replace(); if is_dynamic_ip(&addr_conf.preferred_lft, &addr_conf.valid_lft) { handle_dynamic_ip( req.message_mut(), &addr_conf.preferred_lft, &addr_conf.valid_lft, )?; } req.execute().await?; } } Ok(()) } fn ip_addr_str_to_enum(address: &str) -> Result { Ok(if is_ipv6_addr(address) { IpAddr::V6(std::net::Ipv6Addr::from_str(address)?) } else { IpAddr::V4(std::net::Ipv4Addr::from_str(address)?) }) } fn is_dynamic_ip(preferred_lft: &str, valid_lft: &str) -> bool { (preferred_lft != "forever" && !preferred_lft.is_empty()) || (valid_lft != "forever" && !valid_lft.is_empty()) } fn gen_cache_info( preferred_lft: &str, valid_lft: &str, ) -> Result { let mut ret = CacheInfo::default(); ret.ifa_preferred = parse_lft_sec("preferred_lft", preferred_lft)?; ret.ifa_valid = parse_lft_sec("valid_lft", valid_lft)?; Ok(ret) } fn handle_dynamic_ip( nl_msg: &mut AddressMessage, preferred_lft: &str, valid_lft: &str, ) -> Result<(), NisporError> { nl_msg .attributes .push(AddressAttribute::CacheInfo(gen_cache_info( preferred_lft, valid_lft, )?)); Ok(()) } fn parse_lft_sec(name: &str, lft_str: &str) -> Result { let e = NisporError::invalid_argument(format!( "Invalid {name} format: expect format 50sec, got {lft_str}" )); match lft_str.strip_suffix("sec") { Some(a) => a.parse().map_err(|_| { log::error!("{}", e); e }), None => { log::error!("{}", e); Err(e) } } } nispor-1.2.19/conf/mod.rs000064400000000000000000000007601046102023000133050ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod bond; mod bridge; mod iface; mod inter_ifaces; mod ip; mod route; mod veth; mod vlan; pub use self::bond::BondConf; pub use self::bridge::BridgeConf; pub use self::iface::IfaceConf; pub use self::ip::{IpAddrConf, IpConf}; pub use self::route::RouteConf; pub use self::veth::VethConf; pub use self::vlan::VlanConf; pub(crate) use self::inter_ifaces::{ change_ifaces, create_ifaces, delete_ifaces, }; pub(crate) use self::route::apply_routes_conf; nispor-1.2.19/conf/route.rs000064400000000000000000000076641046102023000136760ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::net::IpAddr; use netlink_packet_route::{ route::{self as rt, RouteAddress, RouteAttribute, RouteMessage}, AddressFamily, }; use serde::{Deserialize, Serialize}; use super::super::query::{parse_ip_addr_str, parse_ip_net_addr_str}; use crate::{NisporError, RouteProtocol}; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct RouteConf { #[serde(default)] pub remove: bool, pub dst: String, pub oif: Option, pub via: Option, pub metric: Option, pub table: Option, pub protocol: Option, } pub(crate) async fn apply_routes_conf( routes: &[RouteConf], iface_name_2_index: &HashMap, ) -> Result<(), NisporError> { let (connection, handle, _) = rtnetlink::new_connection()?; tokio::spawn(connection); for route in routes { apply_route_conf(&handle, route, iface_name_2_index).await?; } Ok(()) } async fn apply_route_conf( handle: &rtnetlink::Handle, route: &RouteConf, iface_name_2_index: &HashMap, ) -> Result<(), NisporError> { let mut nl_msg = RouteMessage::default(); nl_msg.header.kind = rt::RouteType::Unicast; if let Some(p) = route.protocol { nl_msg.header.protocol = p.into(); } else { nl_msg.header.protocol = rt::RouteProtocol::Static; } nl_msg.header.scope = rt::RouteScope::Universe; nl_msg.header.table = rt::RouteHeader::RT_TABLE_MAIN; let (dst_addr, dst_prefix) = parse_ip_net_addr_str(route.dst.as_str())?; nl_msg.header.destination_prefix_length = dst_prefix; match dst_addr { IpAddr::V4(addr) => { nl_msg.header.address_family = AddressFamily::Inet; nl_msg .attributes .push(RouteAttribute::Destination(RouteAddress::Inet(addr))); } IpAddr::V6(addr) => { nl_msg.header.address_family = AddressFamily::Inet6; nl_msg .attributes .push(RouteAttribute::Destination(RouteAddress::Inet6(addr))); } }; if let Some(t) = route.table.as_ref() { nl_msg.header.table = *t; } if let Some(m) = route.metric.as_ref() { nl_msg.attributes.push(RouteAttribute::Priority(*m)); } if let Some(oif) = route.oif.as_deref() { if let Some(iface_index) = iface_name_2_index.get(oif) { nl_msg.attributes.push(RouteAttribute::Iif(*iface_index)); } else { let e = NisporError::invalid_argument(format!( "Interface {oif} does not exist" )); log::error!("{}", e); return Err(e); } } if let Some(via) = route.via.as_deref() { match parse_ip_addr_str(via)? { IpAddr::V4(i) => { nl_msg .attributes .push(RouteAttribute::Gateway(RouteAddress::Inet(i))); } IpAddr::V6(i) => { nl_msg .attributes .push(RouteAttribute::Gateway(RouteAddress::Inet6(i))); } }; } if route.remove { if let Err(e) = handle.route().del(nl_msg).execute().await { if let rtnetlink::Error::NetlinkError(ref e) = e { if e.raw_code() == -libc::ESRCH { return Ok(()); } } return Err(e.into()); } } else { let mut req = handle.route().add(); req.message_mut().header = nl_msg.header; req.message_mut().attributes = nl_msg.attributes; if let Err(e) = req.execute().await { if let rtnetlink::Error::NetlinkError(ref e) = e { if e.raw_code() == -libc::EEXIST { return Ok(()); } } return Err(e.into()); } } Ok(()) } nispor-1.2.19/conf/veth.rs000064400000000000000000000012231046102023000134670ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use rtnetlink::Handle; use crate::{NisporError, VethInfo}; pub type VethConf = VethInfo; impl VethConf { pub(crate) async fn create( &self, handle: &Handle, name: &str, ) -> Result<(), NisporError> { match handle .link() .add() .veth(name.to_string(), self.peer.clone()) .execute() .await { Ok(_) => Ok(()), Err(e) => Err(NisporError::bug(format!( "Failed to create new veth pair '{}' '{}': {}", &name, &self.peer, e ))), } } } nispor-1.2.19/conf/vlan.rs000064400000000000000000000015121046102023000134620ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use rtnetlink::Handle; use serde::{Deserialize, Serialize}; use crate::NisporError; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VlanConf { pub vlan_id: u16, pub base_iface: String, } impl VlanConf { pub(crate) async fn create( handle: &Handle, name: &str, vlan_id: u16, base_iface_index: u32, ) -> Result<(), NisporError> { match handle .link() .add() .vlan(name.to_string(), base_iface_index, vlan_id) .execute() .await { Ok(_) => Ok(()), Err(e) => Err(NisporError::bug(format!( "Failed to create new vlan '{}': {}", &name, e ))), } } } nispor-1.2.19/error.rs000064400000000000000000000075001046102023000127310ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use ethtool::EthtoolError; use libc::{EEXIST, EPERM}; use netlink_packet_utils::DecodeError; use serde::Serialize; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "snake_case")] pub enum ErrorKind { InvalidArgument, NetlinkError, NisporBug, PermissionDeny, } impl std::fmt::Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } #[derive(Debug, Clone, Serialize)] #[non_exhaustive] pub struct NisporError { pub kind: ErrorKind, pub msg: String, } impl NisporError { pub(crate) fn bug(message: String) -> NisporError { NisporError { kind: ErrorKind::NisporBug, msg: message, } } pub(crate) fn permission_deny(message: String) -> NisporError { NisporError { kind: ErrorKind::PermissionDeny, msg: message, } } pub(crate) fn invalid_argument(message: String) -> NisporError { NisporError { kind: ErrorKind::InvalidArgument, msg: message, } } } impl std::fmt::Display for NisporError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.msg) } } impl std::error::Error for NisporError { /* TODO */ } impl std::convert::From for NisporError { fn from(e: rtnetlink::Error) -> Self { match e { rtnetlink::Error::NetlinkError(netlink_err) => { match netlink_err.raw_code().abs() { EEXIST => NisporError::bug(format!( "Got netlink EEXIST error: {netlink_err}" )), EPERM => { NisporError::permission_deny(format!("{netlink_err}",)) } _ => NisporError::bug(format!( "Got netlink unknown error: code {}, msg: {}", netlink_err.raw_code(), netlink_err, )), } } _ => NisporError { kind: ErrorKind::NetlinkError, msg: e.to_string(), }, } } } impl std::convert::From for NisporError { fn from(e: EthtoolError) -> Self { NisporError { kind: ErrorKind::NetlinkError, msg: e.to_string(), } } } impl std::convert::From for NisporError { fn from(e: std::ffi::FromBytesWithNulError) -> Self { NisporError { kind: ErrorKind::NisporBug, msg: format!("FromBytesWithNulError: {e}"), } } } impl std::convert::From for NisporError { fn from(e: std::str::Utf8Error) -> Self { NisporError { kind: ErrorKind::NisporBug, msg: format!("Utf8Error: {e}"), } } } impl std::convert::From for NisporError { fn from(e: DecodeError) -> Self { NisporError { kind: ErrorKind::NetlinkError, msg: e.to_string(), } } } impl std::convert::From for NisporError { fn from(e: std::io::Error) -> Self { NisporError { kind: ErrorKind::NisporBug, msg: e.to_string(), } } } impl std::convert::From for NisporError { fn from(e: std::net::AddrParseError) -> Self { NisporError { kind: ErrorKind::InvalidArgument, msg: e.to_string(), } } } impl std::convert::From for NisporError { fn from(e: mptcp_pm::MptcpPathManagerError) -> Self { NisporError { kind: ErrorKind::NetlinkError, msg: e.to_string(), } } } nispor-1.2.19/filter/iface.rs000064400000000000000000000030021046102023000141250ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] /// The `NetStateIfaceFilter::default()` will retrieve full information. /// To query only interested part, please use `NetStateIfaceFilter::minimum()` /// along with additional property set to `Some()`. pub struct NetStateIfaceFilter { /// Only specified interface. By default: None(all interfaces) pub iface_name: Option, /// Include IP Address information. By default: true pub include_ip_address: bool, /// Include SR-IOV VF information or not. By default: true pub include_sriov_vf_info: bool, /// Include Bridge VLAN information or not. By default: true pub include_bridge_vlan: bool, /// Include ethool information or not. By default: true pub include_ethtool: bool, /// Include mptcp information or not. By default: true pub include_mptcp: bool, } impl Default for NetStateIfaceFilter { fn default() -> Self { Self { iface_name: None, include_ip_address: true, include_sriov_vf_info: true, include_bridge_vlan: true, include_ethtool: true, include_mptcp: true, } } } impl NetStateIfaceFilter { pub fn minimum() -> Self { Self { iface_name: None, include_ip_address: false, include_sriov_vf_info: false, include_bridge_vlan: false, include_ethtool: false, include_mptcp: false, } } } nispor-1.2.19/filter/mod.rs000064400000000000000000000005461046102023000136470ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod iface; mod net_state; mod route; mod route_rule; pub(crate) use self::route::{ apply_kernel_route_filter, should_drop_by_filter, }; pub use self::iface::NetStateIfaceFilter; pub use self::net_state::NetStateFilter; pub use self::route::NetStateRouteFilter; pub use self::route_rule::NetStateRouteRuleFilter; nispor-1.2.19/filter/net_state.rs000064400000000000000000000032751046102023000150600ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{ NetStateIfaceFilter, NetStateRouteFilter, NetStateRouteRuleFilter, }; #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] /// The `NetStateFilter::default()` will retrieve full information. /// To query only the interested part, please use `NetStateFilter::minimum()` /// with proper sub-filter set with `Some()`. pub struct NetStateFilter { /// Filter applied to interfaces, default is NetStateIfaceFilter::default() /// -- all interface with full information. /// When set to None, no interface will be included in result. pub iface: Option, /// Filter applied to route entries, default is /// NetStateRouteFilter::default() -- full routes information. /// When set to None, no route will be included in result. pub route: Option, /// Filter applied to route rule entries, default is /// NetStateRouteRuleFilter::default() -- full route rule infromation. /// When set to None, no route rule will be included in result. pub route_rule: Option, } impl Default for NetStateFilter { fn default() -> Self { Self { iface: Some(NetStateIfaceFilter::default()), route: Some(NetStateRouteFilter::default()), route_rule: Some(NetStateRouteRuleFilter::default()), } } } impl NetStateFilter { /// Return a filter excluding all information. /// Could be used to query interested information only by setting /// the sub-filter to `Some()`. pub fn minimum() -> Self { Self { iface: None, route: None, route_rule: None, } } } nispor-1.2.19/filter/route.rs000064400000000000000000000052431046102023000142250ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::route::RouteAttribute; use rtnetlink::RouteGetRequest; use crate::{NisporError, Route, RouteProtocol, RouteScope}; #[derive(Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct NetStateRouteFilter { /// Returned routes will only contain routes from specified protocol. pub protocol: Option, /// Returned routes will only contain routes from specified scope. pub scope: Option, /// Returned routes will only contain routes next hop to specified /// interface. pub oif: Option, /// Returned routes will only contain routes in specified route table. pub table: Option, } pub(crate) fn apply_kernel_route_filter( handle: &mut RouteGetRequest, filter: &NetStateRouteFilter, iface_name2index: &HashMap, ) -> Result<(), NisporError> { let rt_nlmsg = handle.message_mut(); if let Some(protocol) = filter.protocol { rt_nlmsg.header.protocol = protocol.into(); } if let Some(scope) = filter.scope { rt_nlmsg.header.scope = scope.into(); } if let Some(oif) = filter.oif.as_ref() { match iface_name2index.get(oif) { Some(index) => { rt_nlmsg.attributes.push(RouteAttribute::Oif(*index)) } None => { let e = NisporError::invalid_argument(format!( "Interface {oif} not found" )); log::error!("{}", e); return Err(e); } } } if let Some(table) = filter.table { rt_nlmsg .attributes .push(RouteAttribute::Table(table.into())); } Ok(()) } pub(crate) fn should_drop_by_filter( route: &Route, filter: &NetStateRouteFilter, has_kernel_filter: bool, ) -> bool { // The RT_SCOPE_UNIVERSE is 0 which means wildcard in kernel, we need to // do filter at userspace. if Some(&RouteScope::Universe) == filter.scope.as_ref() { route.scope != RouteScope::Universe } else { if !has_kernel_filter && ((filter.protocol.is_some() && filter.protocol != Some(route.protocol)) || (filter.scope.is_some() && filter.scope.as_ref() != Some(&route.scope)) || (filter.oif.is_some() && filter.oif.as_ref() != route.oif.as_ref()) || (filter.table.is_some() && filter.table.as_ref().map(|i| (*i).into()) != Some(route.table))) { return true; } false } } nispor-1.2.19/filter/route_rule.rs000064400000000000000000000002451046102023000152510ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 #[derive(Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct NetStateRouteRuleFilter { // Place holder } nispor-1.2.19/integ_tests/base_info.rs000064400000000000000000000014311046102023000160520ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; #[test] fn test_iface_info_loopback() { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces["lo"]; assert_eq!(iface.iface_type, crate::IfaceType::Loopback); assert_eq!(iface.state, crate::IfaceState::Unknown); assert_eq!(iface.mtu, 65536); assert_eq!(&iface.mac_address, "00:00:00:00:00:00"); assert_eq!(iface.max_mtu, None); assert_eq!(iface.min_mtu, None); assert_eq!(iface.driver, None); // loopback device is driver-less assert_eq!( iface.flags, &[ crate::IfaceFlag::Loopback, crate::IfaceFlag::LowerUp, crate::IfaceFlag::Running, crate::IfaceFlag::Up, ] ); } nispor-1.2.19/integ_tests/bond.rs000064400000000000000000000070241046102023000150530ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use super::utils::assert_value_match; use crate::{BondMode, NetConf, NetState}; const IFACE_NAME: &str = "bond99"; const PORT1_NAME: &str = "eth1"; const PORT2_NAME: &str = "eth2"; const EXPECTED_BOND_IFACE: &str = r#"--- name: bond99 iface_type: bond bond: subordinates: - eth1 - eth2 mode: balance-rr miimon: 0 updelay: 0 downdelay: 0 use_carrier: true arp_interval: 0 arp_all_targets: any arp_validate: none primary_reselect: always resend_igmp: 1 all_subordinates_active: dropped min_links: 0 lp_interval: 1 packets_per_subordinate: 1 peer_notif_delay: 0 "#; const EXPECTED_PORT1_INFO: &str = r#"--- subordinate_state: active mii_status: link_up link_failure_count: 0 perm_hwaddr: "00:23:45:67:89:1a" queue_id: 0"#; const EXPECTED_PORT2_INFO: &str = r#"--- subordinate_state: active mii_status: link_up link_failure_count: 0 perm_hwaddr: "00:23:45:67:89:1b" queue_id: 0"#; #[test] fn test_get_iface_bond_yaml() { with_bond_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let port1 = &state.ifaces[PORT1_NAME]; let port2 = &state.ifaces[PORT2_NAME]; assert_value_match(EXPECTED_BOND_IFACE, &iface); assert_value_match(EXPECTED_PORT1_INFO, &port1.bond_subordinate); assert_value_match(EXPECTED_PORT2_INFO, &port2.bond_subordinate); assert_eq!(port1.controller, Some("bond99".to_string())); assert_eq!(port2.controller, Some("bond99".to_string())); assert_eq!(port1.controller_type, Some(crate::ControllerType::Bond)); assert_eq!(port2.controller_type, Some(crate::ControllerType::Bond)); }); } fn with_bond_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("bond"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const BOND_CREATE_YML: &str = r#"--- ifaces: - name: bond99 type: bond bond: mode: active-backup - name: veth1 type: veth controller: bond99 veth: peer: veth1.ep - name: veth1.ep type: veth state: up"#; const BOND_PORT_REMOVE_YML: &str = r#"--- ifaces: - name: veth1 type: veth veth: peer: veth1.ep"#; const BOND_DELETE_YML: &str = r#"--- ifaces: - name: bond99 state: absent - name: veth1 state: absent"#; #[test] fn test_create_delete_bond() { let net_conf: NetConf = serde_yaml::from_str(BOND_CREATE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(&iface.iface_type, &crate::IfaceType::Bond); assert_eq!( &iface.bond.as_ref().unwrap().subordinates, &vec!["veth1".to_string()] ); assert_eq!(&iface.bond.as_ref().unwrap().mode, &BondMode::ActiveBackup); let net_conf: NetConf = serde_yaml::from_str(BOND_PORT_REMOVE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(&iface.iface_type, &crate::IfaceType::Bond); let empty_vec: Vec = Vec::new(); assert_eq!(&iface.bond.as_ref().unwrap().subordinates, &empty_vec); let net_conf: NetConf = serde_yaml::from_str(BOND_DELETE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); assert_eq!(None, state.ifaces.get(IFACE_NAME)); } nispor-1.2.19/integ_tests/bridge.rs000064400000000000000000000133031046102023000153620ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use crate::{NetConf, NetState}; use super::utils::assert_value_match; const IFACE_NAME: &str = "br0"; const PORT1_NAME: &str = "eth1"; const PORT2_NAME: &str = "eth2"; // On Archlinux where HZ == 300, these properties will be rounded up by // `jiffies_to_clock_t()` of kernel: // * ageing_time // * hello_time // * forward_delay // * max_age // * multicast_last_member_interval // * multicast_membership_interval // * multicast_querier_interval // * multicast_query_interval // * multicast_query_response_interval // Hence we skip those from testing // Ubuntu is 250 HZ, which hold a subset of above list. const EXPECTED_BRIDGE_INFO: &str = r#"--- name: br0 iface_type: bridge bridge: ports: - eth1 - eth2 bridge_id: 8000.00234567891c group_fwd_mask: 0 root_id: 8000.00234567891c root_port: 0 root_path_cost: 0 topology_change: false topology_change_detected: false tcn_timer: 0 topology_change_timer: 0 group_addr: "01:80:c2:00:00:00" nf_call_iptables: false nf_call_ip6tables: false nf_call_arptables: false vlan_filtering: false vlan_protocol: 802.1q default_pvid: 1 vlan_stats_enabled: false vlan_stats_per_host: false stp_state: disabled hello_timer: 0 priority: 32768 multicast_router: temp_query multicast_snooping: true multicast_query_use_ifaddr: false multicast_querier: false multicast_stats_enabled: false multicast_hash_elasticity: 16 multicast_hash_max: 4096 multicast_last_member_count: 2 multicast_startup_query_count: 2 multicast_igmp_version: 2 multicast_mld_version: 1"#; const EXPECTED_PORT1_BRIDGE_INFO: &str = r#"--- stp_state: forwarding stp_priority: 32 stp_path_cost: 2 hairpin_mode: false bpdu_guard: false root_block: false multicast_fast_leave: false learning: true unicast_flood: true proxyarp: false proxyarp_wifi: false designated_root: 8000.00234567891c designated_bridge: 8000.00234567891c designated_port: 32769 designated_cost: 0 port_id: "0x8001" port_no: "0x1" change_ack: false config_pending: false message_age_timer: 0 forward_delay_timer: 0 hold_timer: 0 multicast_router: temp_query multicast_flood: true multicast_to_unicast: false vlan_tunnel: false broadcast_flood: true group_fwd_mask: 0 neigh_suppress: false isolated: false mrp_ring_open: false mcast_eht_hosts_limit: 512 mcast_eht_hosts_cnt: 0 vlans: - vid: 1 is_pvid: true is_egress_untagged: true"#; const EXPECTED_PORT2_BRIDGE_INFO: &str = r#"--- stp_state: forwarding stp_priority: 32 stp_path_cost: 2 hairpin_mode: false bpdu_guard: false root_block: false multicast_fast_leave: false learning: true unicast_flood: true proxyarp: false proxyarp_wifi: false designated_root: 8000.00234567891c designated_bridge: 8000.00234567891c designated_port: 32770 designated_cost: 0 port_id: "0x8002" port_no: "0x2" change_ack: false config_pending: false message_age_timer: 0 forward_delay_timer: 0 hold_timer: 0 multicast_router: temp_query multicast_flood: true multicast_to_unicast: false vlan_tunnel: false broadcast_flood: true group_fwd_mask: 0 neigh_suppress: false isolated: false mrp_ring_open: false mcast_eht_hosts_limit: 512 mcast_eht_hosts_cnt: 0 vlans: - vid: 1 is_pvid: true is_egress_untagged: true"#; #[test] fn test_get_br_iface_yaml() { with_br_iface(|| { let mut state = NetState::retrieve().unwrap(); let iface = state.ifaces.get_mut(IFACE_NAME).unwrap(); if let Some(ref mut bridge_info) = iface.bridge { bridge_info.gc_timer = None; // Below value is not supported by RHEL 8 and Ubuntu CI bridge_info.multi_bool_opt = None; // Below value is different between CI and RHEL/CentOS 8 // https://blog.grisge.info/posts/br_on_250hz_kernel/ bridge_info.multicast_startup_query_interval = None; } let port1 = state.ifaces.get_mut(PORT1_NAME).unwrap(); if let Some(ref mut port_info) = port1.bridge_port { port_info.forward_delay_timer = 0; // Below values are not supported by Github CI Ubuntu 20.04 port_info.mrp_in_open = None; } let port2 = state.ifaces.get_mut(PORT2_NAME).unwrap(); if let Some(ref mut port_info) = port2.bridge_port { port_info.forward_delay_timer = 0; port_info.mrp_in_open = None; } let iface = &state.ifaces[IFACE_NAME]; let port1 = &state.ifaces[PORT1_NAME]; let port2 = &state.ifaces[PORT2_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Bridge); assert_value_match(EXPECTED_BRIDGE_INFO, iface); assert_value_match(EXPECTED_PORT1_BRIDGE_INFO, &port1.bridge_port); assert_value_match(EXPECTED_PORT2_BRIDGE_INFO, &port2.bridge_port); }); } fn with_br_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("br"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const BRIDGE_CREATE_YML: &str = r#"--- ifaces: - name: br0 type: bridge"#; const BRIDGE_DELETE_YML: &str = r#"--- ifaces: - name: br0 type: bridge state: absent"#; #[test] fn test_create_delete_bridge() { let net_conf: NetConf = serde_yaml::from_str(BRIDGE_CREATE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(&iface.iface_type, &crate::IfaceType::Bridge); let net_conf: NetConf = serde_yaml::from_str(BRIDGE_DELETE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); assert_eq!(None, state.ifaces.get(IFACE_NAME)); } nispor-1.2.19/integ_tests/bridge_vlan_filter.rs000064400000000000000000000072301046102023000177510ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use crate::NetState; use super::utils::assert_value_match; const IFACE_NAME: &str = "br0"; const PORT1_NAME: &str = "eth1"; const PORT2_NAME: &str = "eth2"; const EXPECTED_PORT1_BRIDGE_INFO: &str = r#"--- stp_state: forwarding stp_priority: 32 stp_path_cost: 2 hairpin_mode: false bpdu_guard: false root_block: false multicast_fast_leave: false learning: true unicast_flood: true proxyarp: false proxyarp_wifi: false designated_root: 8000.00234567891c designated_bridge: 8000.00234567891c designated_port: 32769 designated_cost: 0 port_id: "0x8001" port_no: "0x1" change_ack: false config_pending: false message_age_timer: 0 forward_delay_timer: 0 hold_timer: 0 multicast_router: temp_query multicast_flood: true multicast_to_unicast: false vlan_tunnel: false broadcast_flood: true group_fwd_mask: 0 neigh_suppress: false isolated: false mrp_ring_open: false mcast_eht_hosts_limit: 512 mcast_eht_hosts_cnt: 0 vlans: - vid: 1 is_pvid: false is_egress_untagged: true - vid: 10 is_pvid: true is_egress_untagged: true"#; const EXPECTED_PORT2_BRIDGE_INFO: &str = r#"--- stp_state: forwarding stp_priority: 32 stp_path_cost: 2 hairpin_mode: false bpdu_guard: false root_block: false multicast_fast_leave: false learning: true unicast_flood: true proxyarp: false proxyarp_wifi: false designated_root: 8000.00234567891c designated_bridge: 8000.00234567891c designated_port: 32770 designated_cost: 0 port_id: "0x8002" port_no: "0x2" change_ack: false config_pending: false message_age_timer: 0 forward_delay_timer: 0 hold_timer: 0 multicast_router: temp_query multicast_flood: true multicast_to_unicast: false vlan_tunnel: false broadcast_flood: true group_fwd_mask: 0 neigh_suppress: false isolated: false mrp_ring_open: false mcast_eht_hosts_limit: 512 mcast_eht_hosts_cnt: 0 vlans: - vid: 1 is_pvid: true is_egress_untagged: true - vid_range: - 2 - 4094 is_pvid: false is_egress_untagged: false"#; static BR_SELF_VLAN: &str = r#" - vid: 1 is_pvid: false is_egress_untagged: true - vid: 11 is_pvid: true is_egress_untagged: true"#; #[test] fn test_get_br_vlan_filter_iface_yaml() { with_br_with_vlan_filter_iface(|| { let mut state = NetState::retrieve().unwrap(); let port1 = state.ifaces.get_mut(PORT1_NAME).unwrap(); if let Some(ref mut port_info) = port1.bridge_port { port_info.forward_delay_timer = 0; // Below values are not supported by Github CI Ubuntu 20.04 port_info.mrp_in_open = None; } let port2 = state.ifaces.get_mut(PORT2_NAME).unwrap(); if let Some(ref mut port_info) = port2.bridge_port { port_info.forward_delay_timer = 0; // Below values are not supported by Github CI Ubuntu 20.04 port_info.mrp_in_open = None; } let iface = state.ifaces.get(IFACE_NAME).unwrap(); if let Some(bridge_info) = &iface.bridge { assert_eq!(bridge_info.vlan_filtering, Some(true)) } assert_value_match(BR_SELF_VLAN, iface.bridge_vlan.as_ref().unwrap()); let port1 = &state.ifaces[PORT1_NAME]; let port2 = &state.ifaces[PORT2_NAME]; assert_value_match(EXPECTED_PORT1_BRIDGE_INFO, &port1.bridge_port); assert_value_match(EXPECTED_PORT2_BRIDGE_INFO, &port2.bridge_port); }); } fn with_br_with_vlan_filter_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("brv"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/dummy.rs000064400000000000000000000012741046102023000152650ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; const IFACE_NAME: &str = "dummy1"; #[test] fn test_get_iface_dummy_yaml() { with_dummy_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Dummy); }); } fn with_dummy_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("dummy"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/ethtool.rs000064400000000000000000000123201046102023000156020ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME0: &str = "sim0"; const IFACE_NAME1: &str = "sim1"; const EXPECTED_PAUSE_INFO: &str = r#"--- rx: true tx: true auto_negotiate: false"#; const EXPECTED_FEATURE_INFO: &str = r#"--- fixed: esp-hw-offload: false esp-tx-csum-hw-offload: false fcoe-mtu: false highdma: true hsr-dup-offload: false hsr-fwd-offload: false hsr-tag-ins-offload: false hsr-tag-rm-offload: false hw-tc-offload: false l2-fwd-offload: false loopback: true macsec-hw-offload: false netns-local: true rx-all: false rx-checksum: true rx-fcs: false rx-gro-hw: false rx-hashing: false rx-lro: false rx-ntuple-filter: false rx-udp_tunnel-port-offload: false rx-vlan-filter: false rx-vlan-hw-parse: false rx-vlan-stag-filter: false rx-vlan-stag-hw-parse: false tls-hw-record: false tls-hw-rx-offload: false tls-hw-tx-offload: false tx-checksum-fcoe-crc: false tx-checksum-ip-generic: true tx-checksum-ipv4: false tx-checksum-ipv6: false tx-checksum-sctp: true tx-esp-segmentation: false tx-fcoe-segmentation: false tx-gre-csum-segmentation: false tx-gre-segmentation: false tx-gso-partial: false tx-gso-robust: false tx-ipxip4-segmentation: false tx-ipxip6-segmentation: false tx-lockless: true tx-nocache-copy: false tx-scatter-gather-fraglist: true tx-tunnel-remcsum-segmentation: false tx-udp_tnl-csum-segmentation: false tx-udp_tnl-segmentation: false tx-vlan-hw-insert: false tx-vlan-stag-hw-insert: false vlan-challenged: true changeable: rx-gro: true rx-gro-list: false rx-udp-gro-forwarding: false tx-generic-segmentation: true tx-sctp-segmentation: true tx-tcp-ecn-segmentation: true tx-tcp-mangleid-segmentation: true tx-tcp-segmentation: true tx-tcp6-segmentation: true"#; #[test] fn test_get_ethtool_pause_yaml() { with_netdevsim_iface(|| { let state = NetState::retrieve().unwrap(); let iface0 = &state.ifaces[IFACE_NAME0]; let iface1 = &state.ifaces[IFACE_NAME1]; assert_eq!(&iface0.iface_type, &crate::IfaceType::Ethernet); assert_eq!(&iface1.iface_type, &crate::IfaceType::Ethernet); assert_value_match( EXPECTED_PAUSE_INFO, &iface0.ethtool.as_ref().unwrap().pause, ); assert_value_match( EXPECTED_PAUSE_INFO, &iface1.ethtool.as_ref().unwrap().pause, ); }); } #[test] fn test_get_ethtool_feature_yaml_of_loopback() { let mut state = NetState::retrieve().unwrap(); let iface = state.ifaces.get_mut("lo").unwrap(); // These property value is different between Github CI and my Archlinux iface .ethtool .as_mut() .unwrap() .features .as_mut() .map(|features| features.fixed.remove("tx-gso-list")); iface .ethtool .as_mut() .unwrap() .features .as_mut() .map(|features| features.changeable.remove("tx-gso-list")); iface .ethtool .as_mut() .unwrap() .features .as_mut() .map(|features| features.fixed.remove("tx-udp-segmentation")); iface .ethtool .as_mut() .unwrap() .features .as_mut() .map(|features| features.changeable.remove("tx-udp-segmentation")); assert_eq!(&iface.iface_type, &crate::IfaceType::Loopback); assert_value_match( EXPECTED_FEATURE_INFO, &iface.ethtool.as_ref().unwrap().features, ); } // TODO: There is no way to test the ethtool ring. fn with_netdevsim_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("sim"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const IFACE_TUN_NAME: &str = "tun1"; const EXPECTED_ETHTOOL_COALESCE: &str = r#"--- rx_max_frames: 60"#; const EXPECTED_ETHTOOL_LINK_MODE: &str = r#"--- auto_negotiate: false ours: [] duplex: full"#; #[test] fn test_get_ethtool_coalesce_yaml() { with_tun_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_TUN_NAME]; assert_value_match( EXPECTED_ETHTOOL_COALESCE, &iface.ethtool.as_ref().unwrap().coalesce, ); }); } #[test] fn test_get_ethtool_link_mode_yaml() { with_tun_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_TUN_NAME]; assert_value_match( EXPECTED_ETHTOOL_LINK_MODE, &iface.ethtool.as_ref().unwrap().link_mode, ); assert!( iface .ethtool .as_ref() .unwrap() .link_mode .as_ref() .unwrap() .speed >= 10 ) }); } fn with_tun_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("tun"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/hsr.rs000064400000000000000000000017371046102023000147320ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use crate::NetState; use super::utils::assert_value_match; const IFACE_NAME: &str = "hsr0"; // seq_nr has been excluded as it is non-deterministic const EXPECTED_HSR_INFO: &str = r#"--- name: hsr0 iface_type: hsr hsr: port1: eth1 port2: eth2 supervision_addr: 01:15:4e:00:01:2d multicast_spec: 0 version: 0 protocol: prp"#; #[test] fn test_get_hsr_iface_yaml() { with_hsr_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Hsr); assert_value_match(EXPECTED_HSR_INFO, iface); }); } fn with_hsr_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("hsr"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/ip.rs000064400000000000000000000147771046102023000145560ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use super::utils::assert_value_match; use crate::{NetConf, NetState}; const IFACE_NAME: &str = "veth1"; fn with_veth_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("veth"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const ADD_IP_CONF: &str = r#"--- ifaces: - name: veth1 ipv4: addresses: - address: "192.0.2.1" prefix_len: 24 ipv6: addresses: - address: "2001:db8:a::9" prefix_len: 64"#; const ADD_IP_CONF_DYNAMIC: &str = r#"--- ifaces: - name: veth1 ipv4: addresses: - address: "192.0.2.1" prefix_len: 24 valid_lft: 120sec preferred_lft: 60sec ipv6: addresses: - address: "2001:db8:a::9" prefix_len: 64 valid_lft: 121sec preferred_lft: 61sec"#; const DEL_IP_CONF: &str = r#"--- ifaces: - name: veth1 ipv4: addresses: - address: "192.0.2.1" prefix_len: 24 remove: true ipv6: addresses: - address: "2001:db8:a::9" prefix_len: 64 remove: true"#; const EXPECTED_IPV4_INFO: &str = r#"--- addresses: - address: 192.0.2.1 prefix_len: 24 valid_lft: forever preferred_lft: forever"#; const EXPECTED_IPV4_DYNAMIC_INFO: &str = r#"--- addresses: - address: 192.0.2.1 prefix_len: 24 valid_lft: 115sec preferred_lft: 55sec"#; const EXPECTED_IPV6_INFO: &str = r#"--- addresses: - address: "2001:db8:a::9" prefix_len: 64 valid_lft: forever preferred_lft: forever flags: - permanent - address: "fe80::223:45ff:fe67:891a" prefix_len: 64 valid_lft: forever preferred_lft: forever flags: - permanent"#; const EXPECTED_IPV6_DYNAMIC_INFO: &str = r#"--- addresses: - address: "2001:db8:a::9" prefix_len: 64 valid_lft: 116sec preferred_lft: 56sec flags: [] - address: "fe80::223:45ff:fe67:891a" prefix_len: 64 valid_lft: forever preferred_lft: forever flags: - permanent"#; const EXPECTED_EMPTY_IPV6_INFO: &str = r#"--- addresses: - address: "fe80::223:45ff:fe67:891a" prefix_len: 64 valid_lft: forever preferred_lft: forever"#; #[test] fn test_add_and_remove_ip() { with_veth_iface(|| { let conf: NetConf = serde_yaml::from_str(ADD_IP_CONF).unwrap(); conf.apply().unwrap(); wait_ipv6_dad(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_value_match(EXPECTED_IPV4_INFO, &iface.ipv4); assert_value_match(EXPECTED_IPV6_INFO, &iface.ipv6); let conf: NetConf = serde_yaml::from_str(DEL_IP_CONF).unwrap(); conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_eq!(iface.ipv4, None); assert_value_match(EXPECTED_EMPTY_IPV6_INFO, &iface.ipv6); }); } #[test] fn test_add_and_remove_dynamic_ip() { with_veth_iface(|| { let conf: NetConf = serde_yaml::from_str(ADD_IP_CONF_DYNAMIC).unwrap(); conf.apply().unwrap(); wait_ipv6_dad(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_value_match(EXPECTED_IPV4_DYNAMIC_INFO, &iface.ipv4); assert_value_match(EXPECTED_IPV6_DYNAMIC_INFO, &iface.ipv6); let conf: NetConf = serde_yaml::from_str(DEL_IP_CONF).unwrap(); conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_eq!(iface.ipv4, None); assert_value_match(EXPECTED_EMPTY_IPV6_INFO, &iface.ipv6); }); } #[test] fn test_add_dynamic_ip_repeat() { with_veth_iface(|| { let conf: NetConf = serde_yaml::from_str(ADD_IP_CONF_DYNAMIC).unwrap(); conf.apply().unwrap(); conf.apply().unwrap(); std::thread::sleep(std::time::Duration::from_secs(2)); conf.apply().unwrap(); wait_ipv6_dad(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_value_match(EXPECTED_IPV4_DYNAMIC_INFO, &iface.ipv4); assert_value_match(EXPECTED_IPV6_DYNAMIC_INFO, &iface.ipv6); }); } fn with_ipv6_token(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("ipv6token"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } #[test] fn test_ipv6_token() { with_ipv6_token(|| { let state = NetState::retrieve().unwrap(); let iface = state.ifaces.get("eth1").unwrap(); assert_eq!( iface .ipv6 .as_ref() .and_then(|i| i.token.as_ref()) .map(|i| i.to_string()), Some("::fac1".to_string()) ); }) } fn with_ipv6_p2p(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("ipv6p2p"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const EXPECTED_IPV6_P2P_INFO: &str = r#"--- addresses: - address: "2001:db8:f::1" prefix_len: 128 valid_lft: forever preferred_lft: forever flags: - permanent peer: 2001:db8:f::2 peer_prefix_len: 64 - address: "fe80::223:45ff:fe67:891a" prefix_len: 64 valid_lft: forever preferred_lft: forever flags: - permanent"#; #[test] fn test_ipv6_p2p() { with_ipv6_p2p(|| { wait_ipv6_dad(); let state = NetState::retrieve().unwrap(); let iface = state.ifaces.get("eth1").unwrap(); assert_value_match(EXPECTED_IPV6_P2P_INFO, &iface.ipv6); }) } fn wait_ipv6_dad() { std::thread::sleep(std::time::Duration::from_secs(5)); } nispor-1.2.19/integ_tests/mac_vlan.rs000064400000000000000000000016521046102023000157120ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "mac0"; const EXPECTED_MAC_VLAN_STATE: &str = r#"--- base_iface: eth1 mode: source flags: 0 allowed_mac_addresses: - "00:23:45:67:89:1d" - "00:23:45:67:89:1c""#; #[test] fn test_get_macvlan_iface_yaml() { with_macvlan_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::MacVlan); assert_value_match(EXPECTED_MAC_VLAN_STATE, &iface.mac_vlan); }); } fn with_macvlan_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("macvlan"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/mac_vtap.rs000064400000000000000000000016541046102023000157260ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "macvtap0"; const EXPECTED_MAC_VTAP_INFO: &str = r#"--- base_iface: eth1 mode: source flags: 0 allowed_mac_addresses: - "00:23:45:67:89:1c" - "00:23:45:67:89:1b""#; #[test] fn test_get_macvtap_iface_yaml() { with_macvtap_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::MacVtap); assert_value_match(EXPECTED_MAC_VTAP_INFO, &iface.mac_vtap); }); } fn with_macvtap_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("macvtap"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/macsec.rs000064400000000000000000000021651046102023000153650ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use crate::NetState; use super::utils::assert_value_match; const IFACE_NAME: &str = "macsec0"; // SCI has been excluded from the state as it is non-deterministic const EXPECTED_MACSEC_INFO: &str = r#"--- name: macsec0 iface_type: mac_sec macsec: port: 0 icv_len: 16 cipher: gcm-aes128 window: 0 encoding_sa: 0 encrypt: true protect: true send_sci: true end_station: false scb: false replay_protect: false validate: strict base_iface: eth1"#; #[test] fn test_get_macsec_iface_yaml() { with_macsec_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::MacSec); assert_value_match(EXPECTED_MACSEC_INFO, iface); }); } fn with_macsec_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("macsec"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/mod.rs000064400000000000000000000010451046102023000147050ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod utils; #[cfg(test)] mod base_info; #[cfg(test)] mod bond; #[cfg(test)] mod bridge; #[cfg(test)] mod bridge_vlan_filter; #[cfg(test)] mod dummy; #[cfg(test)] mod ethtool; #[cfg(test)] mod hsr; #[cfg(test)] mod ip; #[cfg(test)] mod mac_vlan; #[cfg(test)] mod mac_vtap; #[cfg(test)] mod macsec; #[cfg(test)] mod route; #[cfg(test)] mod route_rule; #[cfg(test)] mod tap; #[cfg(test)] mod tun; #[cfg(test)] mod veth; #[cfg(test)] mod vlan; #[cfg(test)] mod vrf; #[cfg(test)] mod vxlan; #[cfg(test)] mod xfrm; nispor-1.2.19/integ_tests/route.rs000064400000000000000000000136231046102023000152710ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{NetConf, NetState, RouteProtocol}; use super::utils::assert_value_match; const TEST_ROUTE_DST_V4: &str = "198.51.100.0/24"; const TEST_ROUTE_DST_V6: &str = "2001:db8:e::/64"; const EXPECTED_MULTIPATH_YAML_OUTPUT: &str = r#"--- - address_family: ipv6 tos: 0 table: 254 protocol: static scope: universe route_type: unicast flags: [] dst: "2001:db8:e::/64" cache_clntref: 0 cache_last_use: 0 cache_expires: 0 cache_error: 0 cache_used: 0 cache_id: 0 cache_ts: 0 cache_ts_age: 0 metric: 1024 preference: medium multipath: - via: "2001:db8:f::254" iface: eth1 weight: 1 flags: - on_link - via: "2001:db8:f::253" iface: eth1 weight: 256 flags: - on_link - address_family: ipv4 tos: 0 table: 254 protocol: static scope: universe route_type: unicast flags: [] dst: 198.51.100.0/24 multipath: - via: 192.0.2.254 iface: eth1 weight: 1 flags: - on_link - via: 192.0.2.253 iface: eth1 weight: 256 flags: - on_link"#; const EXPECTED_YAML_OUTPUT: &str = r#"--- - address_family: ipv4 tos: 0 table: 254 protocol: dhcp scope: universe route_type: unicast flags: [] oif: veth1 gateway: 192.0.2.3 metric: 500 - address_family: ipv4 tos: 0 table: 254 protocol: dhcp scope: universe route_type: unicast flags: [] dst: 198.51.100.0/24 oif: veth1 gateway: 192.0.2.2 metric: 501 - address_family: ipv6 tos: 0 table: 254 protocol: dhcp scope: universe route_type: unicast flags: [] oif: veth1 gateway: "2001:db8:a::3" cache_clntref: 0 cache_last_use: 0 cache_expires: 0 cache_error: 0 cache_used: 0 cache_id: 0 cache_ts: 0 cache_ts_age: 0 metric: 502 preference: medium - address_family: ipv6 tos: 0 table: 254 protocol: dhcp scope: universe route_type: unicast flags: [] dst: "2001:db8:e::/64" oif: veth1 gateway: "2001:db8:a::2" cache_clntref: 0 cache_last_use: 0 cache_expires: 0 cache_error: 0 cache_used: 0 cache_id: 0 cache_ts: 0 cache_ts_age: 0 metric: 503 preference: medium"#; const ADD_ROUTE_YML: &str = r#"--- routes: - dst: 0.0.0.0/0 oif: veth1 via: 192.0.2.3 metric: 500 protocol: dhcp table: 254 - dst: 198.51.100.0/24 oif: veth1 via: 192.0.2.2 metric: 501 protocol: dhcp table: 254 - dst: ::/0 oif: veth1 via: 2001:db8:a::3 metric: 502 protocol: dhcp table: 254 - dst: 2001:db8:e::/64 oif: veth1 via: 2001:db8:a::2 metric: 503 protocol: dhcp table: 254"#; const REMOVE_ROUTE_YML: &str = r#"--- routes: - dst: 0.0.0.0/0 oif: veth1 via: 192.0.2.3 metric: 500 protocol: dhcp table: 254 remove: true - dst: 198.51.100.0/24 oif: veth1 via: 192.0.2.2 metric: 501 protocol: dhcp table: 254 remove: true - dst: ::/0 oif: veth1 via: 2001:db8:a::3 metric: 502 protocol: dhcp table: 254 remove: true - dst: 2001:db8:e::/64 oif: veth1 via: 2001:db8:a::2 metric: 503 protocol: dhcp table: 254 remove: true"#; #[test] fn test_add_remove_route_yaml() { with_veth_static_ip(|| { let net_conf: NetConf = serde_yaml::from_str(ADD_ROUTE_YML).unwrap(); net_conf.apply().unwrap(); // Apply twice to test whether crate ignore duplicate error. net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let mut expected_routes = Vec::new(); for route in state.routes { if RouteProtocol::Dhcp == route.protocol && route.oif.as_deref() == Some("veth1") { expected_routes.push(route) } } expected_routes.sort_unstable_by_key(|r| r.metric); assert_value_match(EXPECTED_YAML_OUTPUT, &expected_routes); let net_conf: NetConf = serde_yaml::from_str(REMOVE_ROUTE_YML).unwrap(); net_conf.apply().unwrap(); // Apply twice to test whether crate ignore the not found error. net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let mut expected_routes = Vec::new(); for route in state.routes { if RouteProtocol::Dhcp == route.protocol && route.oif.as_deref() == Some("veth1") { expected_routes.push(route) } } assert!(expected_routes.is_empty()); }) } const VETH_STATIC_IP_CONF: &str = r#"--- ifaces: - name: veth1 type: veth veth: peer: veth1.ep ipv4: addresses: - address: "192.0.2.1" prefix_len: 24 ipv6: addresses: - address: "2001:db8:a::9" prefix_len: 64"#; const VETH_ABSENT_CONF: &str = r#"--- ifaces: - name: veth1 type: veth state: absent"#; fn with_veth_static_ip(test: T) where T: FnOnce() + std::panic::UnwindSafe, { let net_conf: NetConf = serde_yaml::from_str(VETH_STATIC_IP_CONF).unwrap(); net_conf.apply().unwrap(); let result = std::panic::catch_unwind(|| { test(); }); let net_conf: NetConf = serde_yaml::from_str(VETH_ABSENT_CONF).unwrap(); net_conf.apply().unwrap(); assert!(result.is_ok()) } #[test] fn test_get_route_yaml() { with_route_test_iface(|| { let state = NetState::retrieve().unwrap(); let mut expected_routes = Vec::new(); for route in state.routes { if Some(TEST_ROUTE_DST_V4.into()) == route.dst || Some(TEST_ROUTE_DST_V6.into()) == route.dst { expected_routes.push(route) } } assert_value_match(EXPECTED_MULTIPATH_YAML_OUTPUT, &expected_routes); }); } fn with_route_test_iface(test: T) where T: FnOnce() + std::panic::UnwindSafe, { super::utils::set_network_environment("route"); let result = std::panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/route_rule.rs000064400000000000000000000031371046102023000163170ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use std::panic; use super::utils::assert_value_match; const TEST_TABLE_ID: u32 = 100; const EXPECTED_YAML_OUTPUT: &str = r#"--- - action: blackhole address_family: ipv6 flags: 0 tos: 0 table: 100 dst: 2001:db8:f::252/128 src: 2001:db8:f::255/128 iif: eth1 oif: eth2 priority: 998 - action: table address_family: ipv6 flags: 0 tos: 16 table: 100 dst: "2001:db8:f::253/128" src: "2001:db8:f::254/128" iif: eth1 oif: eth2 priority: 999 - action: unreachable address_family: ipv4 flags: 0 tos: 0 dst: 192.0.2.2/32 src: 192.0.2.1/32 iif: eth1 oif: eth2 priority: 998 - action: table address_family: ipv4 flags: 0 tos: 16 table: 100 dst: 192.0.2.2/32 src: 192.0.2.1/32 iif: eth1 oif: eth2 priority: 999"#; #[test] fn test_get_route_rule_yaml() { with_route_rule_test_iface(|| { let state = NetState::retrieve().unwrap(); let mut expected_rules = Vec::new(); for mut rule in state.rules { if Some(TEST_TABLE_ID) == rule.table { // Travis CI Ubuntu 18.04 does not support protocol. rule.protocol = None; expected_rules.push(rule) } } assert_value_match(EXPECTED_YAML_OUTPUT, &expected_rules); }); } fn with_route_rule_test_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("rule"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/tap.rs000064400000000000000000000016241046102023000147150ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "tap1"; const EXPECTED_TAP_INFO: &str = r#"--- mode: tap owner: 1001 group: 0 pi: false vnet_hdr: true multi_queue: true persist: true num_queues: 0 num_disabled_queues: 0"#; #[test] fn test_get_tap_iface_yaml() { with_tap_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Tun); assert_value_match(EXPECTED_TAP_INFO, &iface.tun); }); } fn with_tap_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("tap"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/tun.rs000064400000000000000000000016241046102023000147370ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "tun1"; const EXPECTED_TUN_INFO: &str = r#"--- mode: tun owner: 1001 group: 0 pi: false vnet_hdr: true multi_queue: true persist: true num_queues: 0 num_disabled_queues: 0"#; #[test] fn test_get_tun_iface_yaml() { with_tun_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Tun); assert_value_match(EXPECTED_TUN_INFO, &iface.tun); }); } fn with_tun_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("tun"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/utils.rs000064400000000000000000000033471046102023000152750ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::process::Command; use pretty_assertions::assert_eq; pub(crate) fn clear_network_environment() { cmd_exec("../../tools/test_env", vec!["rm"]); } pub(crate) fn set_network_environment(env_type: &str) { assert!(cmd_exec("../../tools/test_env", vec![env_type])); } pub(crate) fn cmd_exec(command: &str, args: Vec<&str>) -> bool { let mut proc = Command::new(command); for argument in args.iter() { proc.arg(argument); } let status = proc.status().expect("failed to execute the command"); status.success() } pub(crate) fn assert_value_match(expected_state: &str, current_state: &T) where T: serde::Serialize, { let current = serde_json::to_value(current_state).unwrap(); let expected: serde_json::Value = serde_yaml::from_str(expected_state).unwrap(); _assert_value_match(&expected, ¤t); } fn _assert_value_match( expected: &serde_json::Value, current: &serde_json::Value, ) { println!("Asserting expected: {expected:?}"); println!("Asserting current: {current:?}"); match expected { serde_json::Value::Object(expected_map) => { for (k, v) in expected_map.iter() { println!("Asserting key '{k}'"); _assert_value_match(v, current.get(k).unwrap()); } } serde_json::Value::Array(expected_array) => { let current_array = current.as_array().unwrap(); assert_eq!(expected_array.len(), current_array.len()); for (i, v) in expected_array.iter().enumerate() { _assert_value_match(v, ¤t_array[i]); } } _ => { assert_eq!(expected, current) } } } nispor-1.2.19/integ_tests/veth.rs000064400000000000000000000053251046102023000151010ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{IfaceState, NetConf, NetState}; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "veth1"; const EXPECTED_VETH_INFO: &str = r#"--- peer: veth1.ep"#; #[test] fn test_get_veth_iface_yaml() { with_veth_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; let iface_type = &iface.iface_type; assert_eq!(iface_type, &crate::IfaceType::Veth); assert_value_match(EXPECTED_VETH_INFO, &iface.veth); }); } fn with_veth_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("veth"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const VETH_CREATE_YML: &str = r#"--- ifaces: - name: veth1 type: veth mac_address: 00:23:45:67:89:1a veth: peer: veth1.ep - name: veth1.ep type: veth "#; const VETH_CHANGE_MAC_YML: &str = r#"--- ifaces: - name: veth1 type: veth mac_address: 00:23:45:67:89:2a"#; const VETH_DOWN_YML: &str = r#"--- ifaces: - name: veth1 type: veth state: down"#; const VETH_DELETE_YML: &str = r#"--- ifaces: - name: veth1 type: veth state: absent"#; #[test] fn test_create_down_delete_veth() { let net_conf: NetConf = serde_yaml::from_str(VETH_CREATE_YML).unwrap(); net_conf.apply().unwrap(); // Wait 1 second for veth to up std::thread::sleep(std::time::Duration::from_secs(1)); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(&iface.iface_type, &crate::IfaceType::Veth); assert_eq!(iface.veth.as_ref().unwrap().peer, "veth1.ep"); assert_eq!(iface.state, IfaceState::Up); assert_eq!(iface.mac_address, "00:23:45:67:89:1a".to_string()); // Change the MAC should have the interface as UP state let net_conf: NetConf = serde_yaml::from_str(VETH_CHANGE_MAC_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.state, IfaceState::Up); assert_eq!(iface.mac_address, "00:23:45:67:89:2a".to_string()); let net_conf: NetConf = serde_yaml::from_str(VETH_DOWN_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.state, IfaceState::Down); let net_conf: NetConf = serde_yaml::from_str(VETH_DELETE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); assert_eq!(None, state.ifaces.get(IFACE_NAME)); } nispor-1.2.19/integ_tests/vlan.rs000064400000000000000000000043671046102023000151000ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::{NetConf, NetState}; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "eth1.101"; const EXPECTED_VLAN_INFO: &str = r#"--- vlan_id: 101 protocol: 802.1q base_iface: eth1 is_reorder_hdr: true is_gvrp: false is_loose_binding: false is_mvrp: false is_bridge_binding: false"#; #[test] fn test_get_vlan_iface_yaml() { with_vlan_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Vlan); assert_value_match(EXPECTED_VLAN_INFO, &iface.vlan); }); } fn with_vlan_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("vlan"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } const VETH_CREATE_YML: &str = r#"--- ifaces: - name: veth1 type: veth veth: peer: veth1.ep - name: veth1.ep type: veth"#; const VETH_DELETE_YML: &str = r#"--- ifaces: - name: veth1 type: veth state: absent"#; const VLAN_CREATE_YML: &str = r#"--- ifaces: - name: veth1.99 type: vlan vlan: base_iface: veth1 vlan_id: 99"#; const VLAN_DELETE_YML: &str = r#"--- ifaces: - name: veth1.99 type: vlan state: absent"#; #[test] fn test_create_delete_vlan() { let net_conf: NetConf = serde_yaml::from_str(VETH_CREATE_YML).unwrap(); net_conf.apply().unwrap(); let net_conf: NetConf = serde_yaml::from_str(VLAN_CREATE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); let iface = &state.ifaces["veth1.99"]; assert_eq!(&iface.iface_type, &crate::IfaceType::Vlan); assert_eq!(iface.vlan.as_ref().unwrap().vlan_id, 99); assert_eq!(iface.vlan.as_ref().unwrap().base_iface.as_str(), "veth1"); let net_conf: NetConf = serde_yaml::from_str(VLAN_DELETE_YML).unwrap(); net_conf.apply().unwrap(); let state = NetState::retrieve().unwrap(); assert_eq!(None, state.ifaces.get("veth1.99")); let net_conf: NetConf = serde_yaml::from_str(VETH_DELETE_YML).unwrap(); net_conf.apply().unwrap(); } nispor-1.2.19/integ_tests/vrf.rs000064400000000000000000000015041046102023000147230ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "vrf0"; const EXPECTED_VRF_INFO: &str = r#"--- table_id: 10 subordinates: - eth1 - eth2"#; #[test] fn test_get_vrf_iface_yaml() { with_vrf_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Vrf); assert_value_match(EXPECTED_VRF_INFO, &iface.vrf); }); } fn with_vrf_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("vrf"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/vxlan.rs000064400000000000000000000023731046102023000152630ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NetState; use pretty_assertions::assert_eq; use std::panic; use super::utils::assert_value_match; const IFACE_NAME: &str = "vxlan0"; const EXPECTED_VXLAN_INFO: &str = r#"--- remote: 8.8.8.8 vxlan_id: 101 base_iface: eth1 local: 1.1.1.1 ttl: 0 tos: 0 learning: true ageing: 300 max_address: 0 src_port_min: 0 src_port_max: 0 proxy: false rsc: false l2miss: false l3miss: false dst_port: 4789 udp_check_sum: true udp6_zero_check_sum_tx: false udp6_zero_check_sum_rx: false remote_check_sum_tx: false remote_check_sum_rx: false gbp: false remote_check_sum_no_partial: false collect_metadata: false label: 0 gpe: false ttl_inherit: false df: 0"#; #[test] fn test_get_vxlan_iface_yaml() { with_vxlan_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Vxlan); assert_value_match(EXPECTED_VXLAN_INFO, &iface.vxlan); }); } fn with_vxlan_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("vxlan"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/integ_tests/xfrm.rs000064400000000000000000000015421046102023000151040ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::panic; use pretty_assertions::assert_eq; use crate::NetState; use super::utils::assert_value_match; const IFACE_NAME: &str = "xfrm1"; const EXPECTED_XFRM_INFO: &str = r#"--- name: xfrm1 iface_type: xfrm xfrm: base_iface: eth1 iface_id: 99"#; #[test] fn test_get_xfrm_iface_yaml() { with_xfrm_iface(|| { let state = NetState::retrieve().unwrap(); let iface = &state.ifaces[IFACE_NAME]; assert_eq!(iface.iface_type, crate::IfaceType::Xfrm); assert_value_match(EXPECTED_XFRM_INFO, iface); }); } fn with_xfrm_iface(test: T) where T: FnOnce() + panic::UnwindSafe, { super::utils::set_network_environment("xfrm"); let result = panic::catch_unwind(|| { test(); }); super::utils::clear_network_environment(); assert!(result.is_ok()) } nispor-1.2.19/lib.rs000064400000000000000000000037441046102023000123540ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod error; #[cfg(test)] mod integ_tests; mod mac; // Since rust 1.62, the `#[default]` can be used for setting default value of // `#[derive(Default)]` for enum. The cargo clippy will complain if we impl the // Default by ourselves. But currently nispor minimum rust version is 1.58, // hence we suppress the clippy warning here. mod conf; mod filter; mod net_conf; mod net_state; mod netlink; #[allow(clippy::derivable_impls)] mod query; pub use crate::conf::{ BondConf, BridgeConf, IfaceConf, IpAddrConf, IpConf, RouteConf, VethConf, VlanConf, }; pub use crate::error::{ErrorKind, NisporError}; pub use crate::filter::{ NetStateFilter, NetStateIfaceFilter, NetStateRouteFilter, NetStateRouteRuleFilter, }; pub use crate::net_conf::NetConf; pub use crate::net_state::NetState; pub use crate::query::{ AddressFamily, BondAdInfo, BondAdSelect, BondAllSubordinatesActive, BondArpValidate, BondFailOverMac, BondInfo, BondLacpRate, BondMiiStatus, BondMode, BondModeArpAllTargets, BondPrimaryReselect, BondSubordinateInfo, BondSubordinateState, BondXmitHashPolicy, BridgeInfo, BridgePortInfo, BridgePortMulticastRouterType, BridgePortStpState, BridgeStpState, BridgeVlanEntry, BridgeVlanProtocol, ControllerType, EthtoolCoalesceInfo, EthtoolFeatureInfo, EthtoolInfo, EthtoolLinkModeDuplex, EthtoolLinkModeInfo, EthtoolPauseInfo, EthtoolRingInfo, HsrInfo, HsrProtocol, Iface, IfaceFlag, IfaceState, IfaceType, IpFamily, IpoibInfo, IpoibMode, Ipv4AddrInfo, Ipv4Info, Ipv6AddrFlag, Ipv6AddrInfo, Ipv6Info, MacSecCipherId, MacSecInfo, MacSecOffload, MacSecValidate, MacVlanInfo, MacVlanMode, MacVtapInfo, MacVtapMode, Mptcp, MptcpAddress, MptcpAddressFlag, MultipathRoute, MultipathRouteFlags, Route, RouteProtocol, RouteRule, RouteScope, RouteType, RuleAction, SriovInfo, TunInfo, TunMode, VethInfo, VfInfo, VfLinkState, VfState, VlanInfo, VlanProtocol, VrfInfo, VrfSubordinateInfo, VxlanInfo, XfrmInfo, }; nispor-1.2.19/mac.rs000064400000000000000000000024441046102023000123420ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::fmt::Write; use crate::NisporError; pub(crate) const ETH_ALEN: usize = libc::ETH_ALEN as usize; pub(crate) const INFINIBAND_ALEN: usize = 20; pub(crate) fn parse_as_mac( mac_len: usize, data: &[u8], ) -> Result { if data.len() < mac_len { return Err(NisporError::bug("wrong size at mac parsing".into())); } let mut rt = String::new(); for (i, m) in data.iter().enumerate().take(mac_len) { write!(rt, "{m:02x}").ok(); if i != mac_len - 1 { rt.push(':'); } } Ok(rt) } pub(crate) fn mac_str_to_raw(mac_addr: &str) -> Result, NisporError> { let mac_addr = mac_addr.to_string().replace([':', '-'], ""); let mut mac_raw: Vec = Vec::new(); let mac_addr = mac_addr.replace(':', ""); let mut chars = mac_addr.chars().peekable(); while chars.peek().is_some() { let chunk: String = chars.by_ref().take(2).collect(); match u8::from_str_radix(&chunk, 16) { Ok(i) => mac_raw.push(i), Err(e) => { return Err(NisporError::invalid_argument(format!( "Invalid hex string for MAC address {mac_addr}: {e}" ))); } } } Ok(mac_raw) } nispor-1.2.19/net_conf.rs000064400000000000000000000043371046102023000134000ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use serde::{Deserialize, Serialize}; use tokio::runtime; use super::{ conf::{apply_routes_conf, change_ifaces, create_ifaces, delete_ifaces}, query::{get_iface_name2index, get_ifaces}, }; use crate::{IfaceConf, IfaceState, NisporError, RouteConf}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct NetConf { pub ifaces: Option>, pub routes: Option>, } impl NetConf { pub fn apply(&self) -> Result<(), NisporError> { let rt = runtime::Builder::new_current_thread().enable_io().build()?; rt.block_on(self.apply_async()) } pub async fn apply_async(&self) -> Result<(), NisporError> { if let Some(ref ifaces) = &self.ifaces { let cur_iface_name_2_index = get_iface_name2index().await?; let mut new_ifaces = Vec::new(); let mut del_ifaces = Vec::new(); let mut chg_ifaces = Vec::new(); for iface in ifaces { let cur_iface_index = cur_iface_name_2_index.get(&iface.name); if iface.state == IfaceState::Absent { if let Some(cur_iface_index) = cur_iface_index { del_ifaces .push((iface.name.as_str(), *cur_iface_index)); } } else if cur_iface_index.is_none() { new_ifaces.push(iface); chg_ifaces.push(iface); } else { chg_ifaces.push(iface); } } delete_ifaces(&del_ifaces).await?; if !new_ifaces.is_empty() { create_ifaces(&new_ifaces, &cur_iface_name_2_index).await?; } if !chg_ifaces.is_empty() { let cur_ifaces = get_ifaces(None).await?; change_ifaces(&chg_ifaces, &cur_ifaces).await?; } } if let Some(routes) = self.routes.as_ref() { if !routes.is_empty() { let cur_iface_name_2_index = get_iface_name2index().await?; apply_routes_conf(routes, &cur_iface_name_2_index).await?; } } Ok(()) } } nispor-1.2.19/net_state.rs000064400000000000000000000045311046102023000135670ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use serde::{Deserialize, Serialize}; use tokio::runtime; use super::query::{ get_ifaces, get_mptcp, get_route_rules, get_routes, merge_mptcp_info, }; use crate::{ Iface, Mptcp, NetStateFilter, NetStateIfaceFilter, NisporError, Route, RouteRule, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct NetState { pub ifaces: HashMap, pub routes: Vec, pub rules: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub mptcp: Option, } impl NetState { pub fn retrieve() -> Result { Self::retrieve_with_filter(&NetStateFilter::default()) } // TODO: autoconvert NetState to NetConf and provide apply() here pub fn retrieve_with_filter( filter: &NetStateFilter, ) -> Result { let rt = runtime::Builder::new_current_thread().enable_io().build()?; rt.block_on(Self::retrieve_with_filter_async(filter)) } pub async fn retrieve_with_filter_async( filter: &NetStateFilter, ) -> Result { let mut ifaces = if filter.iface.is_none() { get_ifaces(Some(&NetStateIfaceFilter::minimum())).await? } else { get_ifaces(filter.iface.as_ref()).await? }; let mut ifname_to_index = HashMap::new(); for iface in ifaces.values() { ifname_to_index.insert(iface.name.clone(), iface.index); } let routes = if filter.route.is_some() { get_routes(&ifname_to_index, filter.route.as_ref()).await? } else { Vec::new() }; let rules = if filter.route_rule.is_some() { get_route_rules().await? } else { Vec::new() }; let mptcp = if filter.iface.as_ref().map(|f| f.include_mptcp) == Some(true) { let mut mptcp = get_mptcp().await?; merge_mptcp_info(&mut ifaces, &mut mptcp); Some(mptcp) } else { None }; if filter.iface.is_none() { ifaces = HashMap::new(); } Ok(NetState { ifaces, routes, rules, mptcp, }) } } nispor-1.2.19/netlink/bridge.rs000064400000000000000000000141621046102023000145020ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use netlink_packet_route::link::{BridgeId, InfoBridge}; use super::super::mac::{parse_as_mac, ETH_ALEN}; use crate::{BridgeInfo, NisporError}; pub(crate) fn parse_bridge_info( infos: &[InfoBridge], ) -> Result { let mut bridge_info = BridgeInfo::default(); for info in infos { if let InfoBridge::ForwardDelay(d) = info { bridge_info.forward_delay = Some(*d); } else if let InfoBridge::HelloTime(d) = info { bridge_info.hello_time = Some(*d); } else if let InfoBridge::MaxAge(d) = info { bridge_info.max_age = Some(*d); } else if let InfoBridge::AgeingTime(d) = info { bridge_info.ageing_time = Some(*d); } else if let InfoBridge::StpState(d) = info { bridge_info.stp_state = Some((*d).into()); } else if let InfoBridge::Priority(d) = info { bridge_info.priority = Some(*d); } else if let InfoBridge::VlanFiltering(d) = info { bridge_info.vlan_filtering = Some(*d > 0); } else if let InfoBridge::VlanProtocol(d) = info { bridge_info.vlan_protocol = Some((*d).into()); } else if let InfoBridge::GroupFwdMask(d) = info { bridge_info.group_fwd_mask = Some(*d); } else if let InfoBridge::RootId(bridge_id) = info { bridge_info.root_id = Some(parse_bridge_id(bridge_id)?); } else if let InfoBridge::BridgeId(bridge_id) = info { bridge_info.bridge_id = Some(parse_bridge_id(bridge_id)?); } else if let InfoBridge::RootPort(d) = info { bridge_info.root_port = Some(*d); } else if let InfoBridge::RootPathCost(d) = info { bridge_info.root_path_cost = Some(*d); } else if let InfoBridge::TopologyChange(d) = info { bridge_info.topology_change = Some(*d > 0); } else if let InfoBridge::TopologyChangeDetected(d) = info { bridge_info.topology_change_detected = Some(*d > 0); } else if let InfoBridge::HelloTimer(d) = info { bridge_info.hello_timer = Some(*d); } else if let InfoBridge::TcnTimer(d) = info { bridge_info.tcn_timer = Some(*d); } else if let InfoBridge::TopologyChangeTimer(d) = info { bridge_info.topology_change_timer = Some(*d); } else if let InfoBridge::GcTimer(d) = info { bridge_info.gc_timer = Some(*d); } else if let InfoBridge::GroupAddr(d) = info { bridge_info.group_addr = Some(parse_as_mac(ETH_ALEN, d)?); // InfoBridge::FdbFlush is only used for changing bridge } else if let InfoBridge::MulticastRouter(d) = info { bridge_info.multicast_router = Some((*d).into()); } else if let InfoBridge::MulticastSnooping(d) = info { bridge_info.multicast_snooping = Some((*d) > 0); } else if let InfoBridge::MulticastQueryUseIfaddr(d) = info { bridge_info.multicast_query_use_ifaddr = Some((*d) > 0); } else if let InfoBridge::MulticastQuerier(d) = info { bridge_info.multicast_querier = Some((*d) > 0); } else if let InfoBridge::MulticastHashElasticity(d) = info { bridge_info.multicast_hash_elasticity = Some(*d); } else if let InfoBridge::MulticastHashMax(d) = info { bridge_info.multicast_hash_max = Some(*d); } else if let InfoBridge::MulticastLastMemberCount(d) = info { bridge_info.multicast_last_member_count = Some(*d); } else if let InfoBridge::MulticastStartupQueryCount(d) = info { bridge_info.multicast_startup_query_count = Some(*d); } else if let InfoBridge::MulticastLastMemberInterval(d) = info { bridge_info.multicast_last_member_interval = Some(*d); } else if let InfoBridge::MulticastMembershipInterval(d) = info { bridge_info.multicast_membership_interval = Some(*d); } else if let InfoBridge::MulticastQuerierInterval(d) = info { bridge_info.multicast_querier_interval = Some(*d); } else if let InfoBridge::MulticastQueryInterval(d) = info { bridge_info.multicast_query_interval = Some(*d); } else if let InfoBridge::MulticastQueryResponseInterval(d) = info { bridge_info.multicast_query_response_interval = Some(*d); } else if let InfoBridge::MulticastStartupQueryInterval(d) = info { bridge_info.multicast_startup_query_interval = Some(*d); } else if let InfoBridge::NfCallIpTables(d) = info { bridge_info.nf_call_iptables = Some(*d > 0); } else if let InfoBridge::NfCallIp6Tables(d) = info { bridge_info.nf_call_ip6tables = Some(*d > 0); } else if let InfoBridge::NfCallArpTables(d) = info { bridge_info.nf_call_arptables = Some(*d > 0); } else if let InfoBridge::VlanDefaultPvid(d) = info { bridge_info.default_pvid = Some(*d); } else if let InfoBridge::VlanStatsEnabled(d) = info { bridge_info.vlan_stats_enabled = Some(*d > 0); } else if let InfoBridge::MulticastStatsEnabled(d) = info { bridge_info.multicast_stats_enabled = Some(*d > 0); } else if let InfoBridge::MulticastIgmpVersion(d) = info { bridge_info.multicast_igmp_version = Some(*d); } else if let InfoBridge::MulticastMldVersion(d) = info { bridge_info.multicast_mld_version = Some(*d); } else if let InfoBridge::VlanStatsPerHost(d) = info { bridge_info.vlan_stats_per_host = Some(*d > 0); } else if let InfoBridge::MultiBoolOpt(d) = info { bridge_info.multi_bool_opt = Some(*d); } else { log::debug!("Unknown NLA {:?}", &info); } } Ok(bridge_info) } pub(crate) fn parse_bridge_id( bridge_id: &BridgeId, ) -> Result { let mac = parse_as_mac(ETH_ALEN, &bridge_id.address) .map_err(|_| { NisporError::invalid_argument( "invalid mac address in bridge_id".into(), ) })? .to_lowercase() .replace(':', ""); Ok(format!("{:04x}.{}", bridge_id.priority, mac)) } nispor-1.2.19/netlink/bridge_vlan.rs000064400000000000000000000061031046102023000155160ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::BridgeVlanEntry; use crate::NisporError; use netlink_packet_route::link::{AfSpecBridge, BridgeVlanInfo}; // VLAN is PVID, ingress untagged; const BRIDGE_VLAN_INFO_PVID: u16 = 1 << 1; // VLAN egresses untagged; const BRIDGE_VLAN_INFO_UNTAGGED: u16 = 1 << 2; // VLAN is start of vlan range; const BRIDGE_VLAN_INFO_RANGE_BEGIN: u16 = 1 << 3; // VLAN is end of vlan range; const BRIDGE_VLAN_INFO_RANGE_END: u16 = 1 << 4; // TODO: Dup with parse_bond_info pub(crate) fn parse_af_spec_bridge_info( nlas: &[AfSpecBridge], ) -> Result>, NisporError> { let mut vlans = Vec::new(); for nla in nlas { if let AfSpecBridge::VlanInfo(nla_vlan_info) = nla { if let Some(v) = parse_vlan_info(nla_vlan_info)? { vlans.push(v); } } } if !vlans.is_empty() { Ok(Some(merge_vlan_range(&vlans))) } else { Ok(None) } } #[derive(Debug, PartialEq, Eq, Clone, Default)] struct KernelBridgeVlanEntry { vid: u16, is_pvid: bool, // is PVID and ingress untagged is_egress_untagged: bool, is_range_start: bool, is_range_end: bool, } fn parse_vlan_info( nla: &BridgeVlanInfo, ) -> Result, NisporError> { let mut entry = KernelBridgeVlanEntry { vid: nla.vid, ..Default::default() }; entry.is_pvid = (nla.flags & BRIDGE_VLAN_INFO_PVID) > 0; entry.is_egress_untagged = (nla.flags & BRIDGE_VLAN_INFO_UNTAGGED) > 0; entry.is_range_start = (nla.flags & BRIDGE_VLAN_INFO_RANGE_BEGIN) > 0; entry.is_range_end = (nla.flags & BRIDGE_VLAN_INFO_RANGE_END) > 0; Ok(Some(entry)) } fn merge_vlan_range( kernel_vlans: &[KernelBridgeVlanEntry], ) -> Vec { let mut vlans = Vec::new(); let mut vlan_start = None; for k_vlan in kernel_vlans { match (k_vlan.is_range_start, k_vlan.is_range_end) { (true, false) => { vlan_start = Some(k_vlan.vid); continue; } (false, true) => { if let Some(start) = vlan_start { vlans.push(BridgeVlanEntry { vid: None, vid_range: Some((start, k_vlan.vid)), is_pvid: k_vlan.is_pvid, is_egress_untagged: k_vlan.is_egress_untagged, }) } else { log::warn!( "Invalid kernel bridge vlan information: \ missing start VLAN for {}", k_vlan.vid ); } vlan_start = None; } (false, false) | (true, true) => { vlans.push(BridgeVlanEntry { vid: Some(k_vlan.vid), vid_range: None, is_pvid: k_vlan.is_pvid, is_egress_untagged: k_vlan.is_egress_untagged, }); vlan_start = None; } }; } vlans } nispor-1.2.19/netlink/ip.rs000064400000000000000000000106671046102023000136640ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::net::IpAddr; use crate::{ Iface, Ipv4AddrInfo, Ipv4Info, Ipv6AddrFlag, Ipv6AddrInfo, Ipv6Info, NisporError, }; use netlink_packet_route::address::{AddressAttribute, AddressMessage}; pub(crate) fn fill_ip_addr( iface_states: &mut HashMap, nl_msg: &AddressMessage, ) -> Result<(), NisporError> { match nl_msg.header.family { netlink_packet_route::AddressFamily::Inet => { let (iface_index, addr) = parse_ipv4_nlas(nl_msg)?; if let Some(i) = get_iface_name_by_index(iface_states, iface_index) { let iface_name = i.to_string(); if let Some(iface) = iface_states.get_mut(iface_name.as_str()) { if iface.ipv4.is_none() { iface.ipv4 = Some(Ipv4Info::default()); } if let Some(ipv4_info) = iface.ipv4.as_mut() { ipv4_info.addresses.push(addr); } } } } netlink_packet_route::AddressFamily::Inet6 => { let (iface_index, addr) = parse_ipv6_nlas(nl_msg)?; if let Some(i) = get_iface_name_by_index(iface_states, iface_index) { let iface_name = i.to_string(); if let Some(iface) = iface_states.get_mut(iface_name.as_str()) { if iface.ipv6.is_none() { iface.ipv6 = Some(Ipv6Info::default()); } if let Some(ipv6_info) = iface.ipv6.as_mut() { ipv6_info.addresses.push(addr); } } } } _ => { log::warn!( "unknown address family {} {:?}", u8::from(nl_msg.header.family), nl_msg ); } }; Ok(()) } // TODO: remove the dupcode between parse_ipv4_nlas() and parse_ipv6_nlas() fn parse_ipv4_nlas( nl_msg: &AddressMessage, ) -> Result<(u32, Ipv4AddrInfo), NisporError> { let iface_index = nl_msg.header.index; let mut addr = Ipv4AddrInfo { prefix_len: nl_msg.header.prefix_len, ..Default::default() }; let mut peer = String::new(); for nla in &nl_msg.attributes { if let AddressAttribute::Local(v) = nla { addr.address = v.to_string(); } else if let AddressAttribute::Address(v) = nla { peer = v.to_string(); } else if let AddressAttribute::CacheInfo(v) = nla { addr.preferred_lft = left_time_to_string(v.ifa_preferred); addr.valid_lft = left_time_to_string(v.ifa_valid); } } if peer != addr.address { addr.peer = Some(peer) } Ok((iface_index, addr)) } fn parse_ipv6_nlas( nl_msg: &AddressMessage, ) -> Result<(u32, Ipv6AddrInfo), NisporError> { let iface_index = nl_msg.header.index; let mut addr = Ipv6AddrInfo { prefix_len: nl_msg.header.prefix_len, ..Default::default() }; for nla in &nl_msg.attributes { if let AddressAttribute::Local(v) = nla { addr.address = v.to_string(); addr.peer_prefix_len = Some(addr.prefix_len); addr.prefix_len = 128; } } for nla in &nl_msg.attributes { if let AddressAttribute::Address(IpAddr::V6(v)) = nla { if addr.peer_prefix_len.is_some() { addr.peer = Some(*v); } else { addr.address = v.to_string(); } } else if let AddressAttribute::CacheInfo(v) = nla { addr.preferred_lft = left_time_to_string(v.ifa_preferred); addr.valid_lft = left_time_to_string(v.ifa_valid); } else if let AddressAttribute::Flags(flags) = nla { addr.flags = flags .as_slice() .iter() .map(|f| Ipv6AddrFlag::from(*f)) .collect(); } } Ok((iface_index, addr)) } fn left_time_to_string(left_time: u32) -> String { if left_time == u32::MAX { "forever".into() } else { format!("{left_time}sec") } } fn get_iface_name_by_index( iface_states: &HashMap, iface_index: u32, ) -> Option<&str> { for (iface_name, iface) in iface_states.iter() { if iface.index == iface_index { return Some(iface_name.as_str()); } } None } nispor-1.2.19/netlink/mod.rs000064400000000000000000000005011046102023000140150ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod bridge; mod bridge_vlan; mod ip; #[allow(dead_code)] // some nla::parse_xx functions might be unused mod nla; pub(crate) use crate::netlink::bridge::*; pub(crate) use crate::netlink::bridge_vlan::*; pub(crate) use crate::netlink::ip::*; pub(crate) use crate::netlink::nla::*; nispor-1.2.19/netlink/nla.rs000064400000000000000000000052761046102023000140260ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use crate::NisporError; use std::net::Ipv4Addr; use std::net::Ipv6Addr; pub(crate) fn parse_as_u8(data: &[u8]) -> Result { Ok(*data.first().ok_or_else(|| { NisporError::bug("wrong index when parsing as u8".into()) })?) } pub(crate) fn parse_as_u16(data: &[u8]) -> Result { let err_msg = "wrong index when parsing as u16"; Ok(u16::from_ne_bytes([ *data .first() .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(1) .ok_or_else(|| NisporError::bug(err_msg.into()))?, ])) } pub(crate) fn parse_as_u32(data: &[u8]) -> Result { let err_msg = "wrong index when parsing as u32"; Ok(u32::from_ne_bytes([ *data .first() .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(1) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(2) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(3) .ok_or_else(|| NisporError::bug(err_msg.into()))?, ])) } pub(crate) fn parse_as_u64(data: &[u8]) -> Result { let err_msg = "wrong index when parsing as u64"; Ok(u64::from_ne_bytes([ *data .first() .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(1) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(2) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(3) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(4) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(5) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(6) .ok_or_else(|| NisporError::bug(err_msg.into()))?, *data .get(7) .ok_or_else(|| NisporError::bug(err_msg.into()))?, ])) } pub(crate) fn parse_as_ipv4(data: &[u8]) -> Result { let addr_bytes: [u8; 4] = data.try_into().map_err(|_| { NisporError::invalid_argument( "Got invalid IPv4 address u8, the length is not 4".into(), ) })?; Ok(Ipv4Addr::from(addr_bytes)) } pub(crate) fn parse_as_ipv6(data: &[u8]) -> Result { let addr_bytes: [u8; 16] = data.try_into().map_err(|_| { NisporError::invalid_argument( "Got invalid IPv6 address u8, the length is not 16".into(), ) })?; Ok(Ipv6Addr::from(addr_bytes)) } nispor-1.2.19/query/bond.rs000064400000000000000000000612161046102023000136730ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr}; use netlink_packet_route::link::{self, InfoBond, InfoBondPort, InfoData}; use serde::{Deserialize, Serialize}; use super::super::mac::parse_as_mac; use crate::{ControllerType, Iface, IfaceType, NisporError}; const BOND_MODE_ROUNDROBIN: u8 = 0; const BOND_MODE_ACTIVEBACKUP: u8 = 1; const BOND_MODE_XOR: u8 = 2; const BOND_MODE_BROADCAST: u8 = 3; const BOND_MODE_8023AD: u8 = 4; const BOND_MODE_TLB: u8 = 5; const BOND_MODE_ALB: u8 = 6; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BondMode { #[serde(rename = "balance-rr")] BalanceRoundRobin, #[serde(rename = "active-backup")] ActiveBackup, #[serde(rename = "balance-xor")] BalanceXor, #[serde(rename = "broadcast")] Broadcast, #[serde(rename = "802.3ad")] Ieee8021AD, #[serde(rename = "balance-tlb")] BalanceTlb, #[serde(rename = "balance-alb")] BalanceAlb, Other(u8), Unknown, } impl Default for BondMode { fn default() -> Self { Self::BalanceRoundRobin } } impl From for BondMode { fn from(d: u8) -> Self { match d { BOND_MODE_ROUNDROBIN => Self::BalanceRoundRobin, BOND_MODE_ACTIVEBACKUP => Self::ActiveBackup, BOND_MODE_XOR => Self::BalanceXor, BOND_MODE_BROADCAST => Self::Broadcast, BOND_MODE_8023AD => Self::Ieee8021AD, BOND_MODE_TLB => Self::BalanceTlb, BOND_MODE_ALB => Self::BalanceAlb, _ => Self::Other(d), } } } impl From for u8 { fn from(v: BondMode) -> u8 { match v { BondMode::BalanceRoundRobin => BOND_MODE_ROUNDROBIN, BondMode::ActiveBackup => BOND_MODE_ACTIVEBACKUP, BondMode::BalanceXor => BOND_MODE_XOR, BondMode::Broadcast => BOND_MODE_BROADCAST, BondMode::Ieee8021AD => BOND_MODE_8023AD, BondMode::BalanceTlb => BOND_MODE_TLB, BondMode::BalanceAlb => BOND_MODE_ALB, BondMode::Other(d) => d, BondMode::Unknown => { log::warn!( "Treating BondMode::Unknown as \ BondMode::BalanceRoundRobin" ); BOND_MODE_ROUNDROBIN } } } } impl std::fmt::Display for BondMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::BalanceRoundRobin => write!(f, "balance-rr"), Self::ActiveBackup => write!(f, "active-backup"), Self::BalanceXor => write!(f, "balance-xor"), Self::Broadcast => write!(f, "broadcast"), Self::Ieee8021AD => write!(f, "802.3ad"), Self::BalanceTlb => write!(f, "balance-tlb"), Self::BalanceAlb => write!(f, "balance-alb"), Self::Other(u) => write!(f, "{u}"), Self::Unknown => write!(f, "unknown"), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondModeArpAllTargets { Any, All, Other(u32), } const BOND_OPT_ARP_ALL_TARGETS_ANY: u32 = 0; const BOND_OPT_ARP_ALL_TARGETS_ALL: u32 = 1; impl From for BondModeArpAllTargets { fn from(d: u32) -> Self { match d { BOND_OPT_ARP_ALL_TARGETS_ANY => Self::Any, BOND_OPT_ARP_ALL_TARGETS_ALL => Self::All, _ => Self::Other(d), } } } const BOND_ARP_VALIDATE_NONE: u32 = 0; const BOND_ARP_VALIDATE_ACTIVE: u32 = 1 << BOND_STATE_ACTIVE as u32; const BOND_ARP_VALIDATE_BACKUP: u32 = 1 << BOND_STATE_BACKUP as u32; const BOND_ARP_VALIDATE_ALL: u32 = BOND_ARP_VALIDATE_ACTIVE | BOND_ARP_VALIDATE_BACKUP; const BOND_ARP_FILTER: u32 = BOND_ARP_VALIDATE_ALL + 1; const BOND_ARP_FILTER_ACTIVE: u32 = BOND_ARP_VALIDATE_ACTIVE | BOND_ARP_FILTER; const BOND_ARP_FILTER_BACKUP: u32 = BOND_ARP_VALIDATE_BACKUP | BOND_ARP_FILTER; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondArpValidate { None, Active, Backup, All, Filter, #[serde(rename = "filter_active")] FilterActive, #[serde(rename = "filter_backkup")] FilterBackup, Other(u32), } impl From for BondArpValidate { fn from(d: u32) -> Self { match d { BOND_ARP_VALIDATE_NONE => Self::None, BOND_ARP_VALIDATE_ACTIVE => Self::Active, BOND_ARP_VALIDATE_BACKUP => Self::Backup, BOND_ARP_VALIDATE_ALL => Self::All, BOND_ARP_FILTER => Self::Filter, BOND_ARP_FILTER_ACTIVE => Self::FilterActive, BOND_ARP_FILTER_BACKUP => Self::FilterBackup, _ => Self::Other(d), } } } const BOND_PRI_RESELECT_ALWAYS: u8 = 0; const BOND_PRI_RESELECT_BETTER: u8 = 1; const BOND_PRI_RESELECT_FAILURE: u8 = 2; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondPrimaryReselect { Always, Better, Failure, Other(u8), } impl From for BondPrimaryReselect { fn from(d: u8) -> Self { match d { BOND_PRI_RESELECT_ALWAYS => Self::Always, BOND_PRI_RESELECT_BETTER => Self::Better, BOND_PRI_RESELECT_FAILURE => Self::Failure, _ => Self::Other(d), } } } const BOND_FOM_NONE: u8 = 0; const BOND_FOM_ACTIVE: u8 = 1; const BOND_FOM_FOLLOW: u8 = 2; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondFailOverMac { None, Active, Follow, Other(u8), } impl From for BondFailOverMac { fn from(d: u8) -> Self { match d { BOND_FOM_NONE => Self::None, BOND_FOM_ACTIVE => Self::Active, BOND_FOM_FOLLOW => Self::Follow, _ => Self::Other(d), } } } const BOND_XMIT_POLICY_LAYER2: u8 = 0; const BOND_XMIT_POLICY_LAYER34: u8 = 1; const BOND_XMIT_POLICY_LAYER23: u8 = 2; const BOND_XMIT_POLICY_ENCAP23: u8 = 3; const BOND_XMIT_POLICY_ENCAP34: u8 = 4; const BOND_XMIT_POLICY_VLAN_SRCMAC: u8 = 5; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondXmitHashPolicy { #[serde(rename = "layer2")] Layer2, #[serde(rename = "layer3+4")] Layer34, #[serde(rename = "layer2+3")] Layer23, #[serde(rename = "encap2+3")] Encap23, #[serde(rename = "encap3+4")] Encap34, #[serde(rename = "vlan+srcmac")] VlanSrcMac, Other(u8), } impl From for BondXmitHashPolicy { fn from(d: u8) -> Self { match d { BOND_XMIT_POLICY_LAYER2 => Self::Layer2, BOND_XMIT_POLICY_LAYER34 => Self::Layer34, BOND_XMIT_POLICY_LAYER23 => Self::Layer23, BOND_XMIT_POLICY_ENCAP23 => Self::Encap23, BOND_XMIT_POLICY_ENCAP34 => Self::Encap34, BOND_XMIT_POLICY_VLAN_SRCMAC => Self::VlanSrcMac, _ => Self::Other(d), } } } const BOND_ALL_SUBORDINATES_ACTIVE_DROPPED: u8 = 0; const BOND_ALL_SUBORDINATES_ACTIVE_DELIEVERD: u8 = 1; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondAllSubordinatesActive { Dropped, Delivered, Other(u8), } impl From for BondAllSubordinatesActive { fn from(d: u8) -> Self { match d { BOND_ALL_SUBORDINATES_ACTIVE_DROPPED => Self::Dropped, BOND_ALL_SUBORDINATES_ACTIVE_DELIEVERD => Self::Delivered, _ => Self::Other(d), } } } const AD_LACP_SLOW: u8 = 0; const AD_LACP_FAST: u8 = 1; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondLacpRate { Slow, Fast, Other(u8), } impl From for BondLacpRate { fn from(d: u8) -> Self { match d { AD_LACP_SLOW => Self::Slow, AD_LACP_FAST => Self::Fast, _ => Self::Other(d), } } } const BOND_AD_STABLE: u8 = 0; const BOND_AD_BANDWIDTH: u8 = 1; const BOND_AD_COUNT: u8 = 2; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum BondAdSelect { Stable, Bandwidth, Count, Other(u8), } impl From for BondAdSelect { fn from(d: u8) -> Self { match d { BOND_AD_STABLE => Self::Stable, BOND_AD_BANDWIDTH => Self::Bandwidth, BOND_AD_COUNT => Self::Count, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct BondAdInfo { pub aggregator: u16, pub num_ports: u16, pub actor_key: u16, pub partner_key: u16, pub partner_mac: String, } impl From<&[link::BondAdInfo]> for BondAdInfo { fn from(nlas: &[link::BondAdInfo]) -> Self { let mut ret = Self::default(); for nla in nlas { match nla { link::BondAdInfo::Aggregator(v) => ret.aggregator = *v, link::BondAdInfo::NumPorts(v) => ret.num_ports = *v, link::BondAdInfo::ActorKey(v) => ret.actor_key = *v, link::BondAdInfo::PartnerKey(v) => ret.partner_key = *v, link::BondAdInfo::PartnerMac(v) => { match parse_as_mac(v.len(), v) { Ok(m) => ret.partner_mac = m, Err(e) => { log::warn!( "Failed to parse BondAdInfo.parse_as_mac: {}", e ); } } } _ => { log::debug!("Unknown BondAdInfo NLA {:?}", nla); } } } ret } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BondInfo { pub subordinates: Vec, pub mode: BondMode, #[serde(skip_serializing_if = "Option::is_none")] pub miimon: Option, #[serde(skip_serializing_if = "Option::is_none")] pub updelay: Option, #[serde(skip_serializing_if = "Option::is_none")] pub downdelay: Option, #[serde(skip_serializing_if = "Option::is_none")] pub use_carrier: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arp_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arp_ip_target: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arp_all_targets: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arp_validate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub primary: Option, #[serde(skip_serializing_if = "Option::is_none")] pub primary_reselect: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fail_over_mac: Option, #[serde(skip_serializing_if = "Option::is_none")] pub xmit_hash_policy: Option, #[serde(skip_serializing_if = "Option::is_none")] pub resend_igmp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub num_unsol_na: Option, #[serde(skip_serializing_if = "Option::is_none")] pub num_grat_arp: Option, #[serde(skip_serializing_if = "Option::is_none")] pub all_subordinates_active: Option, #[serde(skip_serializing_if = "Option::is_none")] pub min_links: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lp_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub packets_per_subordinate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lacp_rate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_select: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_actor_sys_prio: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_user_port_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_actor_system: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tlb_dynamic_lb: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_notif_delay: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_info: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lacp_active: Option, #[serde(skip_serializing_if = "Option::is_none")] pub arp_missed_max: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ns_ip6_target: Option>, } impl From<&[InfoBond]> for BondInfo { fn from(nlas: &[InfoBond]) -> Self { let mut ret = Self::default(); if let Some(mode) = nlas.iter().find_map(|nla| { if let InfoBond::Mode(v) = nla { Some(v) } else { None } }) { ret.mode = (*mode).into(); } for nla in nlas { match nla { InfoBond::Mode(_) => (), InfoBond::MiiMon(v) => ret.miimon = Some(*v), InfoBond::UpDelay(v) => ret.updelay = Some(*v), InfoBond::DownDelay(v) => ret.downdelay = Some(*v), InfoBond::UseCarrier(v) => ret.use_carrier = Some(*v > 0), InfoBond::ArpInterval(v) => ret.arp_interval = Some(*v), InfoBond::ArpIpTarget(v) => { ret.arp_ip_target = ipv4_addr_array_to_string(v).ok() } InfoBond::ArpValidate(v) => { ret.arp_validate = Some((*v).into()) } InfoBond::ArpAllTargets(v) => { ret.arp_all_targets = Some((*v).into()) } // For all the bond option limitation , please refer to // `bond_opts[BOND_OPT_LAST]` in linux kernel code: // `drivers/net/bonding/bond_options.c` InfoBond::Primary(v) => { if [ BondMode::ActiveBackup, BondMode::BalanceAlb, BondMode::BalanceTlb, ] .contains(&ret.mode) { ret.primary = Some(format!("{v}")); } } InfoBond::PrimaryReselect(v) => { ret.primary_reselect = Some((*v).into()) } InfoBond::FailOverMac(v) => { if ret.mode == BondMode::ActiveBackup { ret.fail_over_mac = Some((*v).into()); } } InfoBond::XmitHashPolicy(v) => { if [ BondMode::BalanceXor, BondMode::Ieee8021AD, BondMode::BalanceTlb, ] .contains(&ret.mode) { ret.xmit_hash_policy = Some((*v).into()); } } InfoBond::ResendIgmp(v) => { if [ BondMode::BalanceRoundRobin, BondMode::ActiveBackup, BondMode::BalanceTlb, BondMode::BalanceAlb, ] .contains(&ret.mode) { ret.resend_igmp = Some(*v); } } InfoBond::NumPeerNotif(v) => { if ret.mode == BondMode::ActiveBackup { ret.num_unsol_na = Some(*v); ret.num_grat_arp = Some(*v); } } InfoBond::AllPortsActive(v) => { ret.all_subordinates_active = Some((*v).into()) } // Kernel code has no limit on this, but document require // 802.3ad mode. Let's follow the kernel code here. InfoBond::MinLinks(v) => ret.min_links = Some(*v), // Kernel code has no limit on this, but document require // balance-tlb and balance-alb modes. Let's follow the kernel // code here. InfoBond::LpInterval(v) => ret.lp_interval = Some(*v), InfoBond::PacketsPerPort(v) => { if ret.mode == BondMode::BalanceRoundRobin { ret.packets_per_subordinate = Some(*v); } } InfoBond::AdLacpRate(v) => { if ret.mode == BondMode::Ieee8021AD { ret.lacp_rate = Some((*v).into()) } } InfoBond::AdSelect(v) => { if ret.mode == BondMode::Ieee8021AD { ret.ad_select = Some((*v).into()); } } InfoBond::AdInfo(v) => ret.ad_info = Some(v.as_slice().into()), InfoBond::AdActorSysPrio(v) => { if ret.mode == BondMode::Ieee8021AD { ret.ad_actor_sys_prio = Some(*v); } } InfoBond::AdUserPortKey(v) => { if ret.mode == BondMode::Ieee8021AD { ret.ad_user_port_key = Some(*v); } } InfoBond::AdActorSystem(v) => { if ret.mode == BondMode::Ieee8021AD { ret.ad_actor_system = parse_as_mac(v.len(), v).ok(); } } InfoBond::TlbDynamicLb(v) => { if [BondMode::BalanceTlb, BondMode::BalanceAlb] .contains(&ret.mode) { ret.tlb_dynamic_lb = Some(*v > 0); } } InfoBond::PeerNotifDelay(v) => ret.peer_notif_delay = Some(*v), InfoBond::AdLacpActive(v) => { if ret.mode == BondMode::Ieee8021AD { ret.lacp_active = Some(*v > 0); } } InfoBond::MissedMax(v) => { if ![ BondMode::Ieee8021AD, BondMode::BalanceTlb, BondMode::BalanceAlb, ] .contains(&ret.mode) { ret.arp_missed_max = Some(*v); } } InfoBond::NsIp6Target(v) => ret.ns_ip6_target = Some(v.clone()), _ => { log::warn!("Unsupported InfoBond: {:?}", nla); } } } ret } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BondSubordinateState { Active, Backup, Other(u8), Unknown, } const BOND_STATE_ACTIVE: u8 = 0; const BOND_STATE_BACKUP: u8 = 1; impl From for BondSubordinateState { fn from(d: u8) -> Self { match d { BOND_STATE_ACTIVE => Self::Active, BOND_STATE_BACKUP => Self::Backup, _ => Self::Other(d), } } } impl Default for BondSubordinateState { fn default() -> Self { Self::Unknown } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BondMiiStatus { LinkUp, LinkFail, LinkDown, LinkBack, Other(u8), Unknown, } const BOND_LINK_UP: u8 = 0; const BOND_LINK_FAIL: u8 = 1; const BOND_LINK_DOWN: u8 = 2; const BOND_LINK_BACK: u8 = 3; impl From for BondMiiStatus { fn from(d: u8) -> Self { match d { BOND_LINK_UP => Self::LinkUp, BOND_LINK_FAIL => Self::LinkFail, BOND_LINK_DOWN => Self::LinkDown, BOND_LINK_BACK => Self::LinkBack, _ => Self::Other(d), } } } impl Default for BondMiiStatus { fn default() -> Self { BondMiiStatus::Unknown } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BondSubordinateInfo { pub subordinate_state: BondSubordinateState, pub mii_status: BondMiiStatus, pub link_failure_count: u32, pub perm_hwaddr: String, pub prio: i32, pub queue_id: u16, #[serde(skip_serializing_if = "Option::is_none")] pub ad_aggregator_id: Option, // 802.3ad port state definitions (43.4.2.2 in the 802.3ad standard) // bit map of LACP_STATE_XXX // TODO: Find a rust way of showing it. #[serde(skip_serializing_if = "Option::is_none")] pub ad_actor_oper_port_state: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ad_partner_oper_port_state: Option, } pub(crate) fn get_bond_info( data: &InfoData, ) -> Result, NisporError> { if let InfoData::Bond(nlas) = data { Ok(Some(nlas.as_slice().into())) } else { Ok(None) } } pub(crate) fn get_bond_subordinate_info( nlas: &[InfoBondPort], ) -> Result { let mut ret = BondSubordinateInfo::default(); for nla in nlas { match nla { InfoBondPort::LinkFailureCount(d) => ret.link_failure_count = *d, InfoBondPort::MiiStatus(d) => ret.mii_status = u8::from(*d).into(), InfoBondPort::PermHwaddr(d) => { ret.perm_hwaddr = parse_as_mac(d.len(), d)?; } InfoBondPort::Prio(d) => ret.prio = *d, InfoBondPort::QueueId(d) => ret.queue_id = *d, InfoBondPort::BondPortState(d) => { ret.subordinate_state = u8::from(*d).into() } _ => { log::info!("Unknown bond port info {:?}", nla); } } } Ok(ret) } pub(crate) fn bond_iface_tidy_up(iface_states: &mut HashMap) { gen_subordinate_list_of_controller(iface_states); primary_index_to_iface_name(iface_states); } fn gen_subordinate_list_of_controller( iface_states: &mut HashMap, ) { let mut controller_subordinates: HashMap> = HashMap::new(); for iface in iface_states.values() { if iface.controller_type == Some(ControllerType::Bond) { if let Some(controller) = &iface.controller { match controller_subordinates.get_mut(controller) { Some(subordinates) => subordinates.push(iface.name.clone()), None => { let new_subordinates: Vec = vec![iface.name.clone()]; controller_subordinates .insert(controller.clone(), new_subordinates); } }; } } } for (controller, subordinates) in controller_subordinates.iter_mut() { if let Some(ref mut controller_iface) = iface_states.get_mut(controller) { if let Some(ref mut bond_info) = controller_iface.bond { subordinates.sort(); bond_info.subordinates.clone_from(subordinates); } } } } fn primary_index_to_iface_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Bond { continue; } if let Some(ref mut bond_info) = iface.bond { if let Some(index) = &bond_info.primary { if let Some(iface_name) = index_to_name.get(index) { bond_info.primary = Some(iface_name.clone()); } } } } } fn ipv4_addr_array_to_string( addrs: &[Ipv4Addr], ) -> Result { let mut rt = String::new(); for i in 0..(addrs.len()) { let addr = &addrs.get(i).ok_or_else(|| { NisporError::bug("wrong index at parsing ipv4 as string".into()) })?; rt.push_str(&addr.to_string()); if i != addrs.len() - 1 { // This is how kernel sysfs is showing rt.push(','); } } Ok(rt) } nispor-1.2.19/query/bridge.rs000064400000000000000000000422031046102023000142000ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{AfSpecBridge, InfoBridgePort, InfoData}; use serde::{Deserialize, Serialize}; use crate::{ netlink::{parse_af_spec_bridge_info, parse_bridge_id, parse_bridge_info}, ControllerType, Iface, IfaceType, NisporError, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BridgeStpState { Disabled, KernelStp, UserStp, Other(u32), Unknown, } const BR_NO_STP: u32 = 0; const BR_KERNEL_STP: u32 = 1; const BR_USER_STP: u32 = 2; impl From for BridgeStpState { fn from(d: u32) -> Self { match d { BR_NO_STP => Self::Disabled, BR_KERNEL_STP => Self::KernelStp, BR_USER_STP => Self::UserStp, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BridgeVlanProtocol { #[serde(rename = "802.1q")] Ieee8021Q, #[serde(rename = "802.1ad")] Ieee8021AD, Other(u16), Unknown, } const ETH_P_8021Q: u16 = 0x8100; const ETH_P_8021AD: u16 = 0x88A8; impl From for BridgeVlanProtocol { fn from(d: u16) -> Self { match d { ETH_P_8021Q => Self::Ieee8021Q, ETH_P_8021AD => Self::Ieee8021AD, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BridgeInfo { pub ports: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub ageing_time: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bridge_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group_fwd_mask: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root_port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub root_path_cost: Option, #[serde(skip_serializing_if = "Option::is_none")] pub topology_change: Option, #[serde(skip_serializing_if = "Option::is_none")] pub topology_change_detected: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tcn_timer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub topology_change_timer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gc_timer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group_addr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub nf_call_iptables: Option, #[serde(skip_serializing_if = "Option::is_none")] pub nf_call_ip6tables: Option, #[serde(skip_serializing_if = "Option::is_none")] pub nf_call_arptables: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlan_filtering: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlan_protocol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub default_pvid: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlan_stats_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlan_stats_per_host: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stp_state: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hello_time: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hello_timer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub forward_delay: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_age: Option, #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multi_bool_opt: Option, // does not avaiable in sysfs yet #[serde(skip_serializing_if = "Option::is_none")] pub multicast_router: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_snooping: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_query_use_ifaddr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_querier: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_stats_enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_hash_elasticity: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_hash_max: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_last_member_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_last_member_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_startup_query_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_membership_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_querier_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_query_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_query_response_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_startup_query_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_igmp_version: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_mld_version: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BridgePortStpState { Disabled, Listening, Learning, Forwarding, Blocking, Other(u8), Unknown, } impl Default for BridgePortStpState { fn default() -> Self { Self::Unknown } } const BR_STATE_DISABLED: u8 = 0; const BR_STATE_LISTENING: u8 = 1; const BR_STATE_LEARNING: u8 = 2; const BR_STATE_FORWARDING: u8 = 3; const BR_STATE_BLOCKING: u8 = 4; impl From for BridgePortStpState { fn from(d: u8) -> Self { match d { BR_STATE_DISABLED => Self::Disabled, BR_STATE_LISTENING => Self::Listening, BR_STATE_LEARNING => Self::Learning, BR_STATE_FORWARDING => Self::Forwarding, BR_STATE_BLOCKING => Self::Blocking, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum BridgePortMulticastRouterType { Disabled, TempQuery, Perm, Temp, Other(u8), Unknown, } impl Default for BridgePortMulticastRouterType { fn default() -> Self { Self::Unknown } } const MDB_RTR_TYPE_DISABLED: u8 = 0; const MDB_RTR_TYPE_TEMP_QUERY: u8 = 1; const MDB_RTR_TYPE_PERM: u8 = 2; const MDB_RTR_TYPE_TEMP: u8 = 3; impl From for BridgePortMulticastRouterType { fn from(d: u8) -> Self { match d { MDB_RTR_TYPE_DISABLED => Self::Disabled, MDB_RTR_TYPE_TEMP_QUERY => Self::TempQuery, MDB_RTR_TYPE_PERM => Self::Perm, MDB_RTR_TYPE_TEMP => Self::Temp, _ => Self::Other(d), } } } impl From for u8 { fn from(value: BridgePortMulticastRouterType) -> u8 { match value { BridgePortMulticastRouterType::Disabled => MDB_RTR_TYPE_DISABLED, BridgePortMulticastRouterType::TempQuery => MDB_RTR_TYPE_TEMP_QUERY, BridgePortMulticastRouterType::Perm => MDB_RTR_TYPE_PERM, BridgePortMulticastRouterType::Temp => MDB_RTR_TYPE_TEMP, BridgePortMulticastRouterType::Other(d) => d, BridgePortMulticastRouterType::Unknown => u8::MAX, } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BridgePortInfo { pub stp_state: BridgePortStpState, pub stp_priority: u16, pub stp_path_cost: u32, pub hairpin_mode: bool, pub bpdu_guard: bool, pub root_block: bool, pub multicast_fast_leave: bool, pub learning: bool, pub unicast_flood: bool, pub proxyarp: bool, pub proxyarp_wifi: bool, pub designated_root: String, pub designated_bridge: String, pub designated_port: u16, pub designated_cost: u16, pub port_id: String, pub port_no: String, pub change_ack: bool, pub config_pending: bool, pub message_age_timer: u64, pub forward_delay_timer: u64, pub hold_timer: u64, pub multicast_router: BridgePortMulticastRouterType, pub multicast_flood: bool, pub multicast_to_unicast: bool, pub vlan_tunnel: bool, pub broadcast_flood: bool, pub group_fwd_mask: u16, pub neigh_suppress: bool, pub isolated: bool, #[serde(skip_serializing_if = "String::is_empty")] pub backup_port: String, #[serde(skip_serializing_if = "Option::is_none")] pub mrp_ring_open: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mrp_in_open: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mcast_eht_hosts_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mcast_eht_hosts_cnt: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlans: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub locked: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mac_authentication_bypass: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_n_groups: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multicast_max_groups: Option, #[serde(skip_serializing_if = "Option::is_none")] pub neigh_vlan_supress: Option, #[serde(skip_serializing_if = "Option::is_none")] pub backup_nexthop_id: Option, } pub(crate) fn get_bridge_info( data: &InfoData, ) -> Result, NisporError> { if let InfoData::Bridge(infos) = data { Ok(Some(parse_bridge_info(infos)?)) } else { Ok(None) } } pub(crate) fn get_bridge_port_info( nlas: &[InfoBridgePort], ) -> Result { let mut ret = BridgePortInfo::default(); for nla in nlas { match nla { InfoBridgePort::State(d) => ret.stp_state = u8::from(*d).into(), InfoBridgePort::Priority(d) => ret.stp_priority = *d, InfoBridgePort::Cost(d) => ret.stp_path_cost = *d, InfoBridgePort::HairpinMode(d) => ret.hairpin_mode = *d, InfoBridgePort::Guard(d) => ret.bpdu_guard = *d, InfoBridgePort::Protect(d) => ret.root_block = *d, InfoBridgePort::FastLeave(d) => ret.multicast_fast_leave = *d, InfoBridgePort::Learning(d) => ret.learning = *d, InfoBridgePort::UnicastFlood(d) => ret.unicast_flood = *d, InfoBridgePort::ProxyARP(d) => ret.proxyarp = *d, InfoBridgePort::ProxyARPWifi(d) => ret.proxyarp_wifi = *d, InfoBridgePort::RootId(d) => { ret.designated_root = parse_bridge_id(d)? } InfoBridgePort::BridgeId(d) => { ret.designated_bridge = parse_bridge_id(d)? } InfoBridgePort::DesignatedPort(d) => ret.designated_port = *d, InfoBridgePort::DesignatedCost(d) => ret.designated_cost = *d, InfoBridgePort::PortId(d) => ret.port_id = format!("0x{:04x}", *d), InfoBridgePort::PortNumber(d) => { ret.port_no = format!("0x{:x}", *d) } InfoBridgePort::TopologyChangeAck(d) => ret.change_ack = *d, InfoBridgePort::ConfigPending(d) => ret.config_pending = *d, InfoBridgePort::MessageAgeTimer(d) => ret.message_age_timer = *d, InfoBridgePort::ForwardDelayTimer(d) => { ret.forward_delay_timer = *d } InfoBridgePort::HoldTimer(d) => ret.hold_timer = *d, InfoBridgePort::Flush => (), InfoBridgePort::MulticastRouter(d) => { ret.multicast_router = u8::from(*d).into() } InfoBridgePort::MulticastFlood(d) => ret.multicast_flood = *d, InfoBridgePort::MulticastToUnicast(d) => { ret.multicast_to_unicast = *d } InfoBridgePort::VlanTunnel(d) => ret.vlan_tunnel = *d, InfoBridgePort::BroadcastFlood(d) => ret.broadcast_flood = *d, InfoBridgePort::GroupFwdMask(d) => ret.group_fwd_mask = *d, InfoBridgePort::NeighSupress(d) => ret.neigh_suppress = *d, InfoBridgePort::Isolated(d) => ret.isolated = *d, InfoBridgePort::BackupPort(d) => ret.backup_port = d.to_string(), InfoBridgePort::MrpRingOpen(d) => ret.mrp_ring_open = Some(*d), InfoBridgePort::MrpInOpen(d) => ret.mrp_in_open = Some(*d), InfoBridgePort::MulticastEhtHostsLimit(d) => { ret.mcast_eht_hosts_limit = Some(*d) } InfoBridgePort::MulticastEhtHostsCnt(d) => { ret.mcast_eht_hosts_cnt = Some(*d) } InfoBridgePort::Locked(d) => ret.locked = Some(*d), InfoBridgePort::Mab(d) => ret.mac_authentication_bypass = Some(*d), InfoBridgePort::MulticastNGroups(d) => { ret.multicast_n_groups = Some(*d) } InfoBridgePort::MulticastMaxGroups(d) => { ret.multicast_max_groups = Some(*d) } InfoBridgePort::NeighVlanSupress(d) => { ret.neigh_vlan_supress = Some(*d) } InfoBridgePort::BackupNextHopId(d) => { ret.backup_nexthop_id = Some(*d) } _ => { log::info!("Unknown bridge port info {:?}", nla); } } } Ok(ret) } pub(crate) fn bridge_iface_tidy_up(iface_states: &mut HashMap) { gen_port_list_of_controller(iface_states); convert_back_port_index_to_name(iface_states); } // TODO: This is duplicate of bond gen_port_list_of_controller() fn gen_port_list_of_controller(iface_states: &mut HashMap) { let mut controller_ports: HashMap> = HashMap::new(); for iface in iface_states.values() { if iface.controller_type == Some(ControllerType::Bridge) { if let Some(controller) = &iface.controller { match controller_ports.get_mut(controller) { Some(ports) => ports.push(iface.name.clone()), None => { let new_ports: Vec = vec![iface.name.clone()]; controller_ports.insert(controller.clone(), new_ports); } }; } } } for (controller, ports) in controller_ports.iter_mut() { if let Some(controller_iface) = iface_states.get_mut(controller) { if let Some(ref mut bridge_info) = controller_iface.bridge { ports.sort(); bridge_info.ports.clone_from(ports); } } } } fn convert_back_port_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.controller_type != Some(ControllerType::Bridge) { continue; } if let Some(ref mut port_info) = iface.bridge_port { let index = &port_info.backup_port; if !index.is_empty() { if let Some(iface_name) = index_to_name.get(index) { port_info.backup_port = iface_name.into(); } } } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct BridgeVlanEntry { #[serde(skip_serializing_if = "Option::is_none")] pub vid: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vid_range: Option<(u16, u16)>, pub is_pvid: bool, // is PVID and ingress untagged pub is_egress_untagged: bool, } pub(crate) fn parse_bridge_vlan_info( iface_state: &mut Iface, nlas: &[AfSpecBridge], ) -> Result<(), NisporError> { if let Some(ref mut port_info) = iface_state.bridge_port { if let Some(cur_vlans) = parse_af_spec_bridge_info(nlas)? { match port_info.vlans.as_mut() { Some(vlans) => vlans.extend(cur_vlans), None => port_info.vlans = Some(cur_vlans), }; } } else if iface_state.iface_type == IfaceType::Bridge { let br_vlan = iface_state.bridge_vlan.get_or_insert(Vec::new()); // It's the VLAN of the bridge itself if let Some(cur_vlans) = parse_af_spec_bridge_info(nlas)? { br_vlan.extend(cur_vlans); } } Ok(()) } nispor-1.2.19/query/ethtool.rs000064400000000000000000000534701046102023000144320ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::{hash_map::Entry, BTreeMap, HashMap}; use ethtool::{ EthtoolAttr, EthtoolCoalesceAttr, EthtoolFeatureAttr, EthtoolFeatureBit, EthtoolHandle, EthtoolHeader, EthtoolLinkModeAttr, EthtoolPauseAttr, EthtoolRingAttr, }; use futures::stream::TryStreamExt; use serde::{Deserialize, Serialize, Serializer}; use crate::NisporError; #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct EthtoolPauseInfo { pub rx: bool, pub tx: bool, pub auto_negotiate: bool, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub struct EthtoolFeatureInfo { #[serde(serialize_with = "ordered_map")] pub fixed: HashMap, #[serde(serialize_with = "ordered_map")] pub changeable: HashMap, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct EthtoolCoalesceInfo { #[serde(skip_serializing_if = "Option::is_none")] pub pkt_rate_high: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pkt_rate_low: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rate_sample_interval: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_max_frames: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_max_frames_high: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_max_frames_irq: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_max_frames_low: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_usecs: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_usecs_high: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_usecs_irq: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_usecs_low: Option, #[serde(skip_serializing_if = "Option::is_none")] pub stats_block_usecs: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_max_frames: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_max_frames_high: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_max_frames_irq: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_max_frames_low: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_usecs: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_usecs_high: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_usecs_irq: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_usecs_low: Option, #[serde(skip_serializing_if = "Option::is_none")] pub use_adaptive_rx: Option, #[serde(skip_serializing_if = "Option::is_none")] pub use_adaptive_tx: Option, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct EthtoolRingInfo { #[serde(skip_serializing_if = "Option::is_none")] pub rx: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_max: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_jumbo: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_jumbo_max: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_mini: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rx_mini_max: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tx_max: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum EthtoolLinkModeDuplex { Half, Full, Unknown, Other(u8), } impl From<ðtool::EthtoolLinkModeDuplex> for EthtoolLinkModeDuplex { fn from(v: ðtool::EthtoolLinkModeDuplex) -> Self { match v { ethtool::EthtoolLinkModeDuplex::Half => Self::Half, ethtool::EthtoolLinkModeDuplex::Full => Self::Full, ethtool::EthtoolLinkModeDuplex::Unknown => Self::Unknown, ethtool::EthtoolLinkModeDuplex::Other(d) => Self::Other(*d), } } } impl Default for EthtoolLinkModeDuplex { fn default() -> Self { EthtoolLinkModeDuplex::Unknown } } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct EthtoolLinkModeInfo { pub auto_negotiate: bool, pub ours: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub peer: Option>, pub speed: u32, pub duplex: EthtoolLinkModeDuplex, #[serde(skip_serializing_if = "Option::is_none")] pub controller_subordinate_cfg: Option, #[serde(skip_serializing_if = "Option::is_none")] pub controller_subordinate_state: Option, #[serde(skip_serializing_if = "Option::is_none")] pub lanes: Option, } #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Default)] #[non_exhaustive] pub struct EthtoolInfo { #[serde(skip_serializing_if = "Option::is_none")] pub pause: Option, #[serde(skip_serializing_if = "Option::is_none")] pub features: Option, #[serde(skip_serializing_if = "Option::is_none")] pub coalesce: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ring: Option, #[serde(skip_serializing_if = "Option::is_none")] pub link_mode: Option, } fn ordered_map( value: &HashMap, serializer: S, ) -> Result where S: Serializer, { let ordered: BTreeMap<_, _> = value.iter().collect(); ordered.serialize(serializer) } pub(crate) async fn get_ethtool_infos( ) -> Result, NisporError> { let mut infos: HashMap = HashMap::new(); let (connection, mut handle, _) = ethtool::new_connection()?; tokio::spawn(connection); let mut pause_infos = dump_pause_infos(&mut handle).await?; let mut feature_infos = dump_feature_infos(&mut handle).await?; let mut coalesce_infos = dump_coalesce_infos(&mut handle).await?; let mut ring_infos = dump_ring_infos(&mut handle).await?; let mut link_mode_infos = dump_link_mode_infos(&mut handle).await?; for (iface_name, pause_info) in pause_infos.drain() { infos.insert( iface_name, EthtoolInfo { pause: Some(pause_info), ..Default::default() }, ); } for (iface_name, feature_info) in feature_infos.drain() { match infos.get_mut(&iface_name) { Some(ref mut info) => { info.features = Some(feature_info); } None => { infos.insert( iface_name, EthtoolInfo { features: Some(feature_info), ..Default::default() }, ); } }; } for (iface_name, coalesce_info) in coalesce_infos.drain() { match infos.get_mut(&iface_name) { Some(ref mut info) => { info.coalesce = Some(coalesce_info); } None => { infos.insert( iface_name, EthtoolInfo { coalesce: Some(coalesce_info), ..Default::default() }, ); } }; } for (iface_name, ring_info) in ring_infos.drain() { match infos.get_mut(&iface_name) { Some(ref mut info) => { info.ring = Some(ring_info); } None => { infos.insert( iface_name, EthtoolInfo { ring: Some(ring_info), ..Default::default() }, ); } }; } for (iface_name, link_mode_info) in link_mode_infos.drain() { match infos.get_mut(&iface_name) { Some(ref mut info) => { info.link_mode = Some(link_mode_info); } None => { infos.insert( iface_name, EthtoolInfo { link_mode: Some(link_mode_info), ..Default::default() }, ); } }; } Ok(infos) } async fn dump_pause_infos( handle: &mut EthtoolHandle, ) -> Result, NisporError> { let mut infos = HashMap::new(); let mut pause_handle = handle.pause().get(None).execute().await; while let Some(genl_msg) = pause_handle.try_next().await? { let ethtool_msg = genl_msg.payload; let mut iface_name = None; let mut pause_info = EthtoolPauseInfo::default(); for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Pause(nla) = nla { if let EthtoolPauseAttr::Header(hdrs) = nla { iface_name = get_iface_name_from_header(hdrs); } else if let EthtoolPauseAttr::AutoNeg(v) = nla { pause_info.auto_negotiate = *v } else if let EthtoolPauseAttr::Rx(v) = nla { pause_info.rx = *v } else if let EthtoolPauseAttr::Tx(v) = nla { pause_info.tx = *v } } } if let Some(i) = iface_name { infos.insert(i, pause_info); } } Ok(infos) } async fn dump_feature_infos( handle: &mut EthtoolHandle, ) -> Result, NisporError> { let mut infos = HashMap::new(); let mut feature_handle = handle.feature().get(None).execute().await; while let Some(genl_msg) = feature_handle.try_next().await? { let ethtool_msg = genl_msg.payload; let mut iface_name = None; let mut fixed_features: HashMap = HashMap::new(); let mut changeable_features: HashMap = HashMap::new(); for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Feature(EthtoolFeatureAttr::NoChange( feature_bits, )) = nla { for EthtoolFeatureBit { name, .. } in feature_bits { fixed_features.insert(name.to_string(), false); } } } for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Feature(EthtoolFeatureAttr::Header(hdrs)) = nla { iface_name = get_iface_name_from_header(hdrs); break; } } for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Feature(EthtoolFeatureAttr::Hw(feature_bits)) = nla { for feature_bit in feature_bits { match feature_bit { EthtoolFeatureBit { index: _, name, value: true, } => { // Dummy interface show `tx-lockless` is // changeable, but EthtoolFeatureAttr::NoChange() says // otherwise. The kernel code // `NETIF_F_NEVER_CHANGE` shows `tx-lockless` // should never been changeable. if let Entry::Occupied(mut e) = fixed_features.entry(name.clone()) { e.insert(false); } else { changeable_features .insert(name.to_string(), false); } } EthtoolFeatureBit { index: _, name, value: false, } => { fixed_features.insert(name.to_string(), false); } } } } } for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Feature(EthtoolFeatureAttr::Active( feature_bits, )) = nla { for feature_bit in feature_bits { if let Entry::Occupied(mut e) = fixed_features.entry(feature_bit.name.clone()) { e.insert(true); } else if changeable_features .contains_key(&feature_bit.name) { changeable_features .insert(feature_bit.name.clone(), true); } } } } if let Some(i) = iface_name { infos.insert( i.to_string(), EthtoolFeatureInfo { fixed: fixed_features, changeable: changeable_features, }, ); } } Ok(infos) } async fn dump_coalesce_infos( handle: &mut EthtoolHandle, ) -> Result, NisporError> { let mut infos = HashMap::new(); let mut coalesce_handle = handle.coalesce().get(None).execute().await; while let Some(genl_msg) = coalesce_handle.try_next().await? { let ethtool_msg = genl_msg.payload; let mut iface_name = None; let mut coalesce_info = EthtoolCoalesceInfo::default(); for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Coalesce(nla) = nla { match nla { EthtoolCoalesceAttr::Header(hdrs) => { iface_name = get_iface_name_from_header(hdrs) } EthtoolCoalesceAttr::RxUsecs(d) => { coalesce_info.rx_usecs = Some(*d) } EthtoolCoalesceAttr::RxMaxFrames(d) => { coalesce_info.rx_max_frames = Some(*d) } EthtoolCoalesceAttr::RxUsecsIrq(d) => { coalesce_info.rx_usecs_irq = Some(*d) } EthtoolCoalesceAttr::RxMaxFramesIrq(d) => { coalesce_info.rx_max_frames_irq = Some(*d) } EthtoolCoalesceAttr::TxUsecs(d) => { coalesce_info.tx_usecs = Some(*d) } EthtoolCoalesceAttr::TxMaxFrames(d) => { coalesce_info.tx_max_frames = Some(*d) } EthtoolCoalesceAttr::TxUsecsIrq(d) => { coalesce_info.tx_usecs_irq = Some(*d) } EthtoolCoalesceAttr::TxMaxFramesIrq(d) => { coalesce_info.tx_max_frames_irq = Some(*d) } EthtoolCoalesceAttr::StatsBlockUsecs(d) => { coalesce_info.stats_block_usecs = Some(*d) } EthtoolCoalesceAttr::UseAdaptiveRx(d) => { coalesce_info.use_adaptive_rx = Some(*d) } EthtoolCoalesceAttr::UseAdaptiveTx(d) => { coalesce_info.use_adaptive_tx = Some(*d) } EthtoolCoalesceAttr::PktRateLow(d) => { coalesce_info.pkt_rate_low = Some(*d) } EthtoolCoalesceAttr::RxUsecsLow(d) => { coalesce_info.rx_usecs_low = Some(*d) } EthtoolCoalesceAttr::RxMaxFramesLow(d) => { coalesce_info.rx_max_frames_low = Some(*d) } EthtoolCoalesceAttr::TxUsecsLow(d) => { coalesce_info.tx_usecs_low = Some(*d) } EthtoolCoalesceAttr::TxMaxFramesLow(d) => { coalesce_info.tx_max_frames_low = Some(*d) } EthtoolCoalesceAttr::PktRateHigh(d) => { coalesce_info.pkt_rate_high = Some(*d) } EthtoolCoalesceAttr::RxUsecsHigh(d) => { coalesce_info.rx_usecs_high = Some(*d) } EthtoolCoalesceAttr::RxMaxFramesHigh(d) => { coalesce_info.rx_max_frames_high = Some(*d) } EthtoolCoalesceAttr::TxUsecsHigh(d) => { coalesce_info.tx_usecs_high = Some(*d) } EthtoolCoalesceAttr::TxMaxFramesHigh(d) => { coalesce_info.tx_max_frames_high = Some(*d) } EthtoolCoalesceAttr::RateSampleInterval(d) => { coalesce_info.rate_sample_interval = Some(*d) } _ => log::warn!( "WARN: Unsupported EthtoolCoalesceAttr {:?}", nla ), } } } if let Some(i) = iface_name { infos.insert(i, coalesce_info); } } Ok(infos) } async fn dump_ring_infos( handle: &mut EthtoolHandle, ) -> Result, NisporError> { let mut infos = HashMap::new(); let mut ring_handle = handle.ring().get(None).execute().await; while let Some(genl_msg) = ring_handle.try_next().await? { let ethtool_msg = genl_msg.payload; let mut iface_name = None; let mut ring_info = EthtoolRingInfo::default(); for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::Ring(nla) = nla { match nla { EthtoolRingAttr::Header(hdrs) => { iface_name = get_iface_name_from_header(hdrs) } EthtoolRingAttr::RxMax(d) => ring_info.rx_max = Some(*d), EthtoolRingAttr::RxMiniMax(d) => { ring_info.rx_mini_max = Some(*d) } EthtoolRingAttr::RxJumboMax(d) => { ring_info.rx_jumbo_max = Some(*d) } EthtoolRingAttr::TxMax(d) => ring_info.tx_max = Some(*d), EthtoolRingAttr::Rx(d) => ring_info.rx = Some(*d), EthtoolRingAttr::RxMini(d) => ring_info.rx_mini = Some(*d), EthtoolRingAttr::RxJumbo(d) => { ring_info.rx_jumbo = Some(*d) } EthtoolRingAttr::Tx(d) => ring_info.tx = Some(*d), _ => log::warn!( "WARN: Unsupported EthtoolRingAttr {:?}", nla ), } } } if let Some(i) = iface_name { infos.insert(i, ring_info); } } Ok(infos) } async fn dump_link_mode_infos( handle: &mut EthtoolHandle, ) -> Result, NisporError> { let mut infos = HashMap::new(); let mut link_mode_handle = handle.link_mode().get(None).execute().await; while let Some(genl_msg) = link_mode_handle.try_next().await? { let ethtool_msg = genl_msg.payload; let mut iface_name = None; let mut link_mode_info = EthtoolLinkModeInfo::default(); for nla in ethtool_msg.nlas.as_slice() { if let EthtoolAttr::LinkMode(nla) = nla { match nla { EthtoolLinkModeAttr::Header(hdrs) => { iface_name = get_iface_name_from_header(hdrs) } EthtoolLinkModeAttr::Autoneg(d) => { link_mode_info.auto_negotiate = *d } EthtoolLinkModeAttr::Ours(d) => { link_mode_info.ours.clone_from(d) } EthtoolLinkModeAttr::Peer(d) => { link_mode_info.peer = Some(d.clone()) } EthtoolLinkModeAttr::Speed(d) => { link_mode_info.speed = if *d == u32::MAX { 0 } else { *d } } EthtoolLinkModeAttr::Duplex(d) => { link_mode_info.duplex = d.into() } EthtoolLinkModeAttr::ControllerSubordinateCfg(d) => { link_mode_info.controller_subordinate_cfg = Some(*d) } EthtoolLinkModeAttr::ControllerSubordinateState(d) => { link_mode_info.controller_subordinate_state = Some(*d) } EthtoolLinkModeAttr::Lanes(d) => { link_mode_info.lanes = Some(*d) } _ => log::warn!( "WARN: Unsupported EthtoolLinkModeAttr {:?}", nla ), } } } if let Some(i) = iface_name { infos.insert(i, link_mode_info); } } Ok(infos) } fn get_iface_name_from_header(hdrs: &[EthtoolHeader]) -> Option { for hdr in hdrs { if let EthtoolHeader::DevName(iface_name) = hdr { return Some(iface_name.to_string()); } } None } nispor-1.2.19/query/hsr.rs000064400000000000000000000065061046102023000135460ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoHsr}; use serde::{Deserialize, Serialize}; use crate::mac::{parse_as_mac, ETH_ALEN}; use crate::{Iface, IfaceType}; const HSR_PROTOCOL_HSR: u8 = 0; const HSR_PROTOCOL_PRP: u8 = 1; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum HsrProtocol { Hsr, Prp, Other(u8), Unknown, } impl Default for HsrProtocol { fn default() -> Self { HsrProtocol::Unknown } } impl From for HsrProtocol { fn from(d: u8) -> Self { match d { HSR_PROTOCOL_HSR => Self::Hsr, HSR_PROTOCOL_PRP => Self::Prp, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct HsrInfo { pub port1: Option, pub port2: Option, pub supervision_addr: String, pub seq_nr: u16, pub multicast_spec: u8, pub version: u8, pub protocol: HsrProtocol, #[serde(skip_serializing)] _port1_ifindex: u32, #[serde(skip_serializing)] _port2_ifindex: u32, } pub(crate) fn get_hsr_info(data: &InfoData) -> Option { if let InfoData::Hsr(infos) = data { let mut hsr_info = HsrInfo::default(); for info in infos { match *info { InfoHsr::Port1(d) => { hsr_info._port1_ifindex = d; } InfoHsr::Port2(d) => { hsr_info._port2_ifindex = d; } InfoHsr::SupervisionAddr(d) => { hsr_info.supervision_addr = parse_as_mac(ETH_ALEN, &d).unwrap_or_default(); } InfoHsr::SeqNr(d) => { hsr_info.seq_nr = d; } InfoHsr::MulticastSpec(d) => { hsr_info.multicast_spec = d; } InfoHsr::Version(d) => { hsr_info.version = d; } InfoHsr::Protocol(d) => { hsr_info.protocol = u8::from(d).into(); } _ => { log::warn!("Unknown HSR info {:?}", info); } } } Some(hsr_info) } else { None } } pub(crate) fn hsr_iface_tidy_up(iface_states: &mut HashMap) { fill_port_iface_names(iface_states); } fn fill_port_iface_names(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(iface.index, iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Hsr { continue; } if let Some(ref mut hsr_info) = iface.hsr { if let Some(port1_iface_name) = index_to_name.get(&hsr_info._port1_ifindex) { hsr_info.port1 = Some(port1_iface_name.to_string()); } if let Some(port2_iface_name) = index_to_name.get(&hsr_info._port2_ifindex) { hsr_info.port2 = Some(port2_iface_name.to_string()); } } } } nispor-1.2.19/query/iface.rs000064400000000000000000000525221046102023000140200ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use std::path::Path; use netlink_packet_route::link::{ self, InfoKind, InfoPortData, InfoPortKind, LinkAttribute, LinkInfo, LinkLayerType, LinkMessage, }; use serde::{Deserialize, Serialize}; use super::{ super::mac::parse_as_mac, bond::{get_bond_info, get_bond_subordinate_info}, bridge::{get_bridge_info, get_bridge_port_info, parse_bridge_vlan_info}, hsr::get_hsr_info, ip::fill_af_spec_inet_info, ipoib::get_ipoib_info, mac_vlan::get_mac_vlan_info, mac_vtap::get_mac_vtap_info, macsec::get_macsec_info, sriov::{get_sriov_info, sriov_is_enabled}, tun::get_tun_info, vlan::get_vlan_info, vrf::{get_vrf_info, get_vrf_subordinate_info}, vxlan::get_vxlan_info, xfrm::get_xfrm_info, }; use crate::{ BondInfo, BondSubordinateInfo, BridgeInfo, BridgePortInfo, BridgeVlanEntry, EthtoolInfo, HsrInfo, IpoibInfo, Ipv4Info, Ipv6Info, MacSecInfo, MacVlanInfo, MacVtapInfo, MptcpAddress, NisporError, SriovInfo, TunInfo, VethInfo, VfInfo, VlanInfo, VrfInfo, VrfSubordinateInfo, VxlanInfo, XfrmInfo, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum IfaceType { Bond, Veth, Bridge, Vlan, Dummy, Vxlan, Loopback, Ethernet, Infiniband, Vrf, Tun, MacVlan, MacVtap, OpenvSwitch, Ipoib, MacSec, Hsr, Unknown, Xfrm, Other(String), } impl Default for IfaceType { fn default() -> Self { IfaceType::Unknown } } impl std::fmt::Display for IfaceType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::Bond => "bond", Self::Veth => "veth", Self::Bridge => "bridge", Self::Vlan => "vlan", Self::Dummy => "dummy", Self::Vxlan => "vxlan", Self::Loopback => "loopback", Self::Ethernet => "ethernet", Self::Infiniband => "infiniband", Self::Vrf => "vrf", Self::Tun => "tun", Self::MacVlan => "macvlan", Self::MacVtap => "macvtap", Self::OpenvSwitch => "openvswitch", Self::Ipoib => "ipoib", Self::MacSec => "macsec", Self::Hsr => "hsr", Self::Unknown => "unknown", Self::Xfrm => "xfrm", Self::Other(s) => s, } ) } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum IfaceState { Up, Dormant, Down, LowerLayerDown, Testing, Absent, // Only for IfaceConf Other(String), #[default] Unknown, } impl From for IfaceState { fn from(d: link::State) -> Self { match d { link::State::Up => Self::Up, link::State::Down => Self::Down, link::State::LowerLayerDown => Self::LowerLayerDown, link::State::Testing => Self::Testing, link::State::Dormant => Self::Dormant, link::State::Unknown => Self::Unknown, _ => { let mut s = format!("{d:?}"); s.make_ascii_lowercase(); Self::Other(s) } } } } impl std::fmt::Display for IfaceState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { Self::Up => "up", Self::Dormant => "dormant", Self::Down => "down", Self::LowerLayerDown => "lower_layer_down", Self::Testing => "testing", Self::Absent => "absent", Self::Other(s) => s.as_str(), Self::Unknown => "unknown", } ) } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum IfaceFlag { AllMulti, AutoMedia, Broadcast, Debug, Dormant, Loopback, LowerUp, Controller, Multicast, NoArp, PoinToPoint, Portsel, Promisc, Running, Subordinate, Up, Other(u32), #[default] Unknown, } impl From for IfaceFlag { fn from(d: link::LinkFlag) -> IfaceFlag { match d { link::LinkFlag::Allmulti => Self::AllMulti, link::LinkFlag::Automedia => Self::AutoMedia, link::LinkFlag::Broadcast => Self::Broadcast, link::LinkFlag::Debug => Self::Debug, link::LinkFlag::Dormant => Self::Dormant, link::LinkFlag::Loopback => Self::Loopback, link::LinkFlag::LowerUp => Self::LowerUp, link::LinkFlag::Controller => Self::Controller, link::LinkFlag::Multicast => Self::Multicast, link::LinkFlag::Noarp => Self::NoArp, link::LinkFlag::Pointopoint => Self::PoinToPoint, link::LinkFlag::Portsel => Self::Portsel, link::LinkFlag::Promisc => Self::Promisc, link::LinkFlag::Running => Self::Running, link::LinkFlag::Port => Self::Subordinate, link::LinkFlag::Up => Self::Up, _ => Self::Other(d.into()), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum ControllerType { Bond, Bridge, Vrf, OpenvSwitch, Other(String), Unknown, } impl From<&str> for ControllerType { fn from(s: &str) -> Self { match s { "bond" => ControllerType::Bond, "bridge" => ControllerType::Bridge, "vrf" => ControllerType::Vrf, "openvswitch" => ControllerType::OpenvSwitch, _ => ControllerType::Other(s.to_string()), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Iface { pub name: String, #[serde(skip_serializing)] pub index: u32, pub iface_type: IfaceType, pub state: IfaceState, pub mtu: i64, #[serde(skip_serializing_if = "Option::is_none")] pub min_mtu: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_mtu: Option, pub flags: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub ipv4: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ipv6: Option, #[serde(skip_serializing_if = "String::is_empty")] pub mac_address: String, #[serde(skip_serializing_if = "String::is_empty")] pub permanent_mac_address: String, #[serde(skip_serializing_if = "Option::is_none")] pub controller: Option, #[serde(skip_serializing_if = "Option::is_none")] pub controller_type: Option, #[serde(skip_serializing_if = "Option::is_none")] pub link_netnsid: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ethtool: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bond: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bond_subordinate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bridge: Option, #[serde(skip_serializing_if = "Option::is_none")] pub bridge_vlan: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub bridge_port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tun: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vlan: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vxlan: Option, #[serde(skip_serializing_if = "Option::is_none")] pub veth: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vrf: Option, #[serde(skip_serializing_if = "Option::is_none")] pub vrf_subordinate: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mac_vlan: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mac_vtap: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sriov: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sriov_vf: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ipoib: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mptcp: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub macsec: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hsr: Option, #[serde(skip_serializing_if = "Option::is_none")] pub xfrm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub driver: Option, } // TODO: impl From Iface to IfaceConf pub(crate) fn parse_nl_msg_to_name_and_index( nl_msg: &LinkMessage, ) -> Option<(String, u32)> { let index = nl_msg.header.index; let name = _get_iface_name(nl_msg); if name.is_empty() { None } else { Some((name, index)) } } pub(crate) fn parse_nl_msg_to_iface( nl_msg: &LinkMessage, ) -> Result, NisporError> { let name = _get_iface_name(nl_msg); if name.is_empty() { return Ok(None); } let driver = _get_iface_driver(name.as_str()); let link_layer_type = match nl_msg.header.link_layer_type { LinkLayerType::Ether => IfaceType::Ethernet, LinkLayerType::Loopback => IfaceType::Loopback, LinkLayerType::Infiniband => IfaceType::Infiniband, _ => IfaceType::Unknown, }; let mut iface_state = Iface { name, driver, iface_type: link_layer_type.clone(), ..Default::default() }; iface_state.index = nl_msg.header.index; let mut link: Option = None; for nla in &nl_msg.attributes { if let LinkAttribute::Mtu(mtu) = nla { iface_state.mtu = *mtu as i64; } else if let LinkAttribute::MinMtu(mtu) = nla { iface_state.min_mtu = if *mtu != 0 { Some(*mtu as i64) } else { None }; } else if let LinkAttribute::MaxMtu(mtu) = nla { iface_state.max_mtu = if *mtu != 0 { Some(*mtu as i64) } else { None }; } else if let LinkAttribute::Address(mac) = nla { iface_state.mac_address = parse_as_mac(mac.len(), mac)?; } else if let LinkAttribute::PermAddress(mac) = nla { iface_state.permanent_mac_address = parse_as_mac(mac.len(), mac)?; } else if let LinkAttribute::OperState(state) = nla { iface_state.state = (*state).into(); } else if let LinkAttribute::Controller(controller) = nla { iface_state.controller = Some(format!("{controller}")); } else if let LinkAttribute::Link(l) = nla { link = Some(*l); } else if let LinkAttribute::LinkInfo(infos) = nla { for info in infos { if let LinkInfo::Kind(t) = info { let iface_type = match t { InfoKind::Bond => IfaceType::Bond, InfoKind::Veth => IfaceType::Veth, InfoKind::Bridge => IfaceType::Bridge, InfoKind::Vlan => IfaceType::Vlan, InfoKind::Vxlan => IfaceType::Vxlan, InfoKind::Dummy => IfaceType::Dummy, InfoKind::Tun => IfaceType::Tun, InfoKind::Vrf => IfaceType::Vrf, InfoKind::MacVlan => IfaceType::MacVlan, InfoKind::MacVtap => IfaceType::MacVtap, InfoKind::Ipoib => IfaceType::Ipoib, InfoKind::MacSec => IfaceType::MacSec, InfoKind::Hsr => IfaceType::Hsr, InfoKind::Xfrm => IfaceType::Xfrm, InfoKind::Other(s) => match s.as_ref() { "openvswitch" => IfaceType::OpenvSwitch, _ => IfaceType::Other(s.to_lowercase()), }, _ => IfaceType::Other( format!("{t:?}").as_str().to_lowercase(), ), }; if let IfaceType::Other(_) = iface_type { /* We did not find an explicit link type. Instead it's * just "Other(_)". If we already determined a link type * above (ethernet or infiniband), keep that one. */ if iface_state.iface_type == IfaceType::Unknown { iface_state.iface_type = iface_type } } else { /* We found a better link type based on the kind. Use it. */ iface_state.iface_type = iface_type } } } for info in infos { if let LinkInfo::Data(d) = info { match iface_state.iface_type { IfaceType::Bond => iface_state.bond = get_bond_info(d)?, IfaceType::Bridge => { iface_state.bridge = get_bridge_info(d)? } IfaceType::Tun => match get_tun_info(d) { Ok(info) => { iface_state.tun = Some(info); } Err(e) => { log::warn!("Error parsing TUN info: {}", e); } }, IfaceType::Vlan => iface_state.vlan = get_vlan_info(d), IfaceType::Vxlan => { iface_state.vxlan = get_vxlan_info(d)? } IfaceType::Vrf => iface_state.vrf = get_vrf_info(d), IfaceType::MacVlan => { iface_state.mac_vlan = get_mac_vlan_info(d)? } IfaceType::MacVtap => { iface_state.mac_vtap = get_mac_vtap_info(d)? } IfaceType::Ipoib => { iface_state.ipoib = get_ipoib_info(d); } IfaceType::MacSec => { iface_state.macsec = get_macsec_info(d); } IfaceType::Hsr => { iface_state.hsr = get_hsr_info(d); } IfaceType::Xfrm => { iface_state.xfrm = get_xfrm_info(d); } _ => log::warn!( "Unhandled IFLA_INFO_DATA for iface type {:?}", iface_state.iface_type ), } } } for info in infos { if let LinkInfo::PortKind(d) = info { match d { InfoPortKind::Bond => { iface_state.controller_type = Some(ControllerType::Bond) } InfoPortKind::Bridge => { iface_state.controller_type = Some(ControllerType::Bridge) } InfoPortKind::Other(s) => { iface_state.controller_type = Some(s.as_str().into()) } _ => { log::info!("Unknown port kind {:?}", info); } } } } if let Some(controller_type) = &iface_state.controller_type { for info in infos { if let LinkInfo::PortData(d) = info { match d { InfoPortData::BondPort(bond_ports) => { iface_state.bond_subordinate = Some( get_bond_subordinate_info(bond_ports)?, ); } InfoPortData::BridgePort(data) => { iface_state.bridge_port = Some(get_bridge_port_info(data)?); } InfoPortData::Other(data) => { match controller_type { ControllerType::Vrf => { iface_state.vrf_subordinate = get_vrf_subordinate_info(data)?; } _ => log::warn!( "Unknown controller type {:?}", controller_type ), } } _ => { log::debug!("Unknown InfoPortData {:?}", d); } } } } } } else if let LinkAttribute::VfInfoList(nlas) = nla { if let Ok(info) = get_sriov_info(&iface_state.name, nlas, &link_layer_type) { if sriov_is_enabled(&iface_state.name) { iface_state.sriov = Some(info); } } } else if let LinkAttribute::NetnsId(id) = nla { iface_state.link_netnsid = Some(*id); } else if let LinkAttribute::AfSpecUnspec(nlas) = nla { fill_af_spec_inet_info(&mut iface_state, nlas); } else { // Place holder for paring more Nla } } if let Some(ref mut vlan_info) = iface_state.vlan { if let Some(base_iface_index) = link { vlan_info.base_iface = format!("{base_iface_index}"); } } if let Some(ref mut ib_info) = iface_state.ipoib { if let Some(base_iface_index) = link { ib_info.base_iface = Some(format!("{base_iface_index}")); } } if let Some(ref mut macsec_info) = iface_state.macsec { if let Some(base_iface_index) = link { macsec_info.base_iface = Some(format!("{base_iface_index}")); } } if let Some(iface_index) = link { match iface_state.iface_type { IfaceType::Veth => { iface_state.veth = Some(VethInfo { peer: format!("{iface_index}"), }) } IfaceType::MacVlan => { if let Some(ref mut mac_vlan_info) = iface_state.mac_vlan { mac_vlan_info.base_iface = format!("{iface_index}"); } } IfaceType::MacVtap => { if let Some(ref mut mac_vtap_info) = iface_state.mac_vtap { mac_vtap_info.base_iface = format!("{iface_index}"); } } _ => (), } } iface_state.flags = nl_msg .header .flags .as_slice() .iter() .map(|f| IfaceFlag::from(*f)) .collect(); Ok(Some(iface_state)) } fn _get_iface_name(nl_msg: &LinkMessage) -> String { for nla in &nl_msg.attributes { if let LinkAttribute::IfName(name) = nla { return name.clone(); } } "".into() } pub(crate) fn fill_bridge_vlan_info( iface_states: &mut HashMap, nl_msg: &LinkMessage, ) -> Result<(), NisporError> { let name = _get_iface_name(nl_msg); if name.is_empty() { return Ok(()); } if let Some(iface_state) = iface_states.get_mut(&name) { for nla in &nl_msg.attributes { if let LinkAttribute::AfSpecBridge(nlas) = nla { parse_bridge_vlan_info(iface_state, nlas)?; } } } Ok(()) } // Currently there is no valid netlink way to get the driver information as the // ETHTOOL_GDRVINFO ioctl command has no netlink equivalent. We use sysfs content // /sys/class/net//device/ and extract the last element from the "driver"-link // (https://docs.kernel.org/admin-guide/sysfs-rules.html) fn _get_iface_driver(if_name: &str) -> Option { let sysfs_path = format!("/sys/class/net/{if_name}/device/driver"); let path = Path::new(&*sysfs_path); let full_path = match path.read_link() { Ok(i) => i, Err(_e) => { return None; } }; let driver = match full_path.file_name() { Some(i) => i, None => { return None; } }; let res = match driver.to_str() { Some(i) => i, None => { log::error!( "Driver for iface {if_name} is not a correct UTF-8 name" ); return None; } }; Some(res.to_string()) } nispor-1.2.19/query/inter_ifaces.rs000064400000000000000000000134311046102023000154000ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use futures::stream::TryStreamExt; use netlink_packet_route::{link::LinkExtentMask, AddressFamily}; use rtnetlink::new_connection; use super::{ super::netlink::fill_ip_addr, bond::bond_iface_tidy_up, bridge::bridge_iface_tidy_up, ethtool::get_ethtool_infos, hsr::hsr_iface_tidy_up, iface::{ fill_bridge_vlan_info, parse_nl_msg_to_iface, parse_nl_msg_to_name_and_index, }, ipoib::ipoib_iface_tidy_up, mac_vlan::mac_vlan_iface_tidy_up, macsec::macsec_iface_tidy_up, sriov::sriov_vf_iface_tidy_up, veth::veth_iface_tidy_up, vlan::vlan_iface_tidy_up, vrf::vrf_iface_tidy_up, vxlan::vxlan_iface_tidy_up, xfrm::xfrm_iface_tidy_up, }; use crate::{EthtoolInfo, Iface, NetStateIfaceFilter, NisporError}; pub(crate) async fn get_ifaces( filter: Option<&NetStateIfaceFilter>, ) -> Result, NisporError> { let mut iface_states: HashMap = HashMap::new(); let (connection, handle, _) = new_connection()?; tokio::spawn(connection); let default_filter = NetStateIfaceFilter::default(); let filter = filter.unwrap_or(&default_filter); let mut link_get_handle = handle.link().get(); if filter.include_sriov_vf_info { link_get_handle = link_get_handle .set_filter_mask(AddressFamily::Unspec, vec![LinkExtentMask::Vf]); } if let Some(iface_name) = filter.iface_name.as_ref() { link_get_handle = link_get_handle.match_name(iface_name.to_string()); } let mut links = link_get_handle.execute(); while let Some(nl_msg) = links.try_next().await? { if let Some(iface_state) = parse_nl_msg_to_iface(&nl_msg)? { iface_states.insert(iface_state.name.clone(), iface_state); } } let iface_index = filter .iface_name .as_ref() .and_then(|name| iface_states.get(name)) .map(|i| i.index); if filter.iface_name.is_some() && iface_index.is_none() { return Err(NisporError::invalid_argument(format!( "Interface {} not found", filter.iface_name.as_ref().unwrap() ))); } if filter.include_ip_address || filter.include_mptcp { let mut addr_get_handle = handle.address().get(); if let Some(iface_index) = iface_index { // rust-rtnetlink is doing filter this at userspace level. // https://github.com/little-dude/netlink/issues/294 addr_get_handle = addr_get_handle.set_link_index_filter(iface_index); } let mut addrs = addr_get_handle.execute(); while let Some(nl_msg) = addrs.try_next().await? { fill_ip_addr(&mut iface_states, &nl_msg)?; } } if filter.include_bridge_vlan { let mut link_get_handle = handle.link().get().set_filter_mask( AddressFamily::Bridge, vec![LinkExtentMask::BrvlanCompressed], ); if let Some(iface_name) = filter.iface_name.as_ref() { link_get_handle = link_get_handle.match_name(iface_name.to_string()); } let mut br_vlan_links = link_get_handle.execute(); while let Some(nl_msg) = br_vlan_links.try_next().await? { fill_bridge_vlan_info(&mut iface_states, &nl_msg)?; } } if filter.include_ethtool { // TODO: Apply interface filter to ethtool dump also match get_ethtool_infos().await { Ok(mut ethtool_infos) => { ifaces_merge_ethool_infos( &mut iface_states, &mut ethtool_infos, ); } Err(e) => { // Ethtool is considered as optional log::warn!("Failed to query ethtool info: {}", e); } }; } tidy_up(&mut iface_states); Ok(iface_states) } fn tidy_up(iface_states: &mut HashMap) { controller_iface_index_to_name(iface_states); bond_iface_tidy_up(iface_states); bridge_iface_tidy_up(iface_states); vlan_iface_tidy_up(iface_states); vxlan_iface_tidy_up(iface_states); veth_iface_tidy_up(iface_states); vrf_iface_tidy_up(iface_states); mac_vlan_iface_tidy_up(iface_states); macsec_iface_tidy_up(iface_states); hsr_iface_tidy_up(iface_states); ipoib_iface_tidy_up(iface_states); sriov_vf_iface_tidy_up(iface_states); xfrm_iface_tidy_up(iface_states); } fn controller_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if let Some(controller) = &iface.controller { if let Some(name) = index_to_name.get(controller) { iface.controller = Some(name.to_string()); } } } } fn ifaces_merge_ethool_infos( iface_states: &mut HashMap, ethtool_infos: &mut HashMap, ) { for iface in iface_states.values_mut() { if let Some(ethtool_info) = ethtool_infos.remove(&iface.name) { iface.ethtool = Some(ethtool_info) } } } pub(crate) async fn get_iface_name2index( ) -> Result, NisporError> { let mut name2index: HashMap = HashMap::new(); let (connection, handle, _) = new_connection()?; tokio::spawn(connection); let mut links = handle.link().get().execute(); while let Some(nl_msg) = links.try_next().await? { if let Some((iface_name, iface_index)) = parse_nl_msg_to_name_and_index(&nl_msg) { name2index.insert(iface_name, iface_index); } } Ok(name2index) } nispor-1.2.19/query/ip.rs000064400000000000000000000125621046102023000133610ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::net::{IpAddr, Ipv6Addr}; use std::str::FromStr; use netlink_packet_route::address; use netlink_packet_route::link::{AfSpecInet6, AfSpecUnspec}; use serde::{Deserialize, Serialize}; use crate::{Iface, NisporError}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Ipv4Info { pub addresses: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Ipv4AddrInfo { pub address: String, pub prefix_len: u8, #[serde(skip_serializing_if = "Option::is_none")] pub peer: Option, // The renaming seonds for this address be valid pub valid_lft: String, // The renaming seonds for this address be preferred pub preferred_lft: String, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Ipv6Info { pub addresses: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Ipv6AddrInfo { pub address: String, pub prefix_len: u8, // The renaming seonds for this address be valid pub valid_lft: String, // The renaming seonds for this address be preferred pub preferred_lft: String, /// IPv6 Address Flags pub flags: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub peer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub peer_prefix_len: Option, } pub(crate) fn parse_ip_addr_str( ip_addr_str: &str, ) -> Result { IpAddr::from_str(ip_addr_str).map_err(|e| { let e = NisporError::invalid_argument(format!( "Invalid IP address {ip_addr_str}: {e}" )); log::error!("{}", e); e }) } pub(crate) fn parse_ip_net_addr_str( ip_net_str: &str, ) -> Result<(IpAddr, u8), NisporError> { let splits: Vec<&str> = ip_net_str.split('/').collect(); if splits.len() > 2 || splits.is_empty() { let e = NisporError::invalid_argument(format!( "Invalid IP network address {ip_net_str}", )); log::error!("{}", e); return Err(e); } let addr_str = splits[0]; let prefix_len = if let Some(prefix_len_str) = splits.get(1) { prefix_len_str.parse::().map_err(|e| { let e = NisporError::invalid_argument(format!( "Invalid IP network prefix {ip_net_str}: {e}" )); log::error!("{}", e); e })? } else if is_ipv6_addr(addr_str) { 128 } else { 32 }; Ok((parse_ip_addr_str(addr_str)?, prefix_len)) } pub(crate) fn fill_af_spec_inet_info(iface: &mut Iface, nlas: &[AfSpecUnspec]) { for nla in nlas { if let AfSpecUnspec::Inet6(nlas) = nla { for nla in nlas { if let AfSpecInet6::Token(addr) = nla { // Kernel set all zero as default value if *addr != Ipv6Addr::UNSPECIFIED { if iface.ipv6.is_none() { iface.ipv6 = Some(Ipv6Info::default()); } if let Some(ipv6_info) = iface.ipv6.as_mut() { ipv6_info.token = Some(ipv6_token_to_string(*addr)); } } } } } } } // The Ipv6Addr::to_string() will convert // ::fac1 to ::0.0.250.193 // Which is no ideal in this case // To workaround that, we set leading 64 bites to '2001:db8::', and // then trip it out from string. fn ipv6_token_to_string(addr: Ipv6Addr) -> String { let mut segments = addr.segments(); segments[0] = 0x2001; segments[1] = 0xdb8; Ipv6Addr::from(segments).to_string()["2001:db8".len()..].to_string() } #[derive(Debug, PartialEq, Eq, Clone)] pub enum IpFamily { Ipv4, Ipv6, } pub(crate) fn is_ipv6_addr(addr: &str) -> bool { addr.contains(':') } #[derive(Clone, Eq, PartialEq, Debug, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum Ipv6AddrFlag { Secondary, Nodad, Optimistic, Dadfailed, Homeaddress, Deprecated, Tentative, Permanent, Managetempaddr, Noprefixroute, Mcautojoin, StablePrivacy, Other(u32), } impl From for Ipv6AddrFlag { fn from(d: address::AddressFlag) -> Self { match d { address::AddressFlag::Secondary => Self::Secondary, address::AddressFlag::Nodad => Self::Nodad, address::AddressFlag::Optimistic => Self::Optimistic, address::AddressFlag::Dadfailed => Self::Dadfailed, address::AddressFlag::Homeaddress => Self::Homeaddress, address::AddressFlag::Deprecated => Self::Deprecated, address::AddressFlag::Tentative => Self::Tentative, address::AddressFlag::Permanent => Self::Permanent, address::AddressFlag::Managetempaddr => Self::Managetempaddr, address::AddressFlag::Noprefixroute => Self::Noprefixroute, address::AddressFlag::Mcautojoin => Self::Mcautojoin, address::AddressFlag::StablePrivacy => Self::StablePrivacy, _ => Self::Other(d.into()), } } } nispor-1.2.19/query/ipoib.rs000064400000000000000000000051011046102023000140420ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoIpoib}; use serde::{Deserialize, Serialize}; use crate::{Iface, IfaceType}; const IPOIB_MODE_DATAGRAM: u16 = 0; const IPOIB_MODE_CONNECTED: u16 = 1; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum IpoibMode { /* using unreliable datagram QPs */ Datagram, /* using connected QPs */ Connected, Other(u16), Unknown, } impl Default for IpoibMode { fn default() -> Self { IpoibMode::Unknown } } impl From for IpoibMode { fn from(d: u16) -> Self { match d { IPOIB_MODE_DATAGRAM => Self::Datagram, IPOIB_MODE_CONNECTED => Self::Connected, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct IpoibInfo { pub pkey: u16, pub mode: IpoibMode, pub umcast: bool, #[serde(skip_serializing_if = "Option::is_none")] pub base_iface: Option, } pub(crate) fn get_ipoib_info(data: &InfoData) -> Option { if let InfoData::Ipoib(infos) = data { let mut ipoib_info = IpoibInfo::default(); for info in infos { if let InfoIpoib::Pkey(d) = *info { ipoib_info.pkey = d; } else if let InfoIpoib::Mode(d) = *info { ipoib_info.mode = d.into(); } else if let InfoIpoib::UmCast(d) = *info { ipoib_info.umcast = d > 0; } else { log::debug!("Unknown IPoIB info {:?}", info) } } Some(ipoib_info) } else { None } } pub(crate) fn ipoib_iface_tidy_up(iface_states: &mut HashMap) { convert_base_iface_index_to_name(iface_states); } fn convert_base_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Ipoib { continue; } if let Some(ref mut ib_info) = iface.ipoib { if let Some(base_iface_name) = &ib_info .base_iface .as_ref() .and_then(|i| index_to_name.get(i)) { ib_info.base_iface = Some(base_iface_name.to_string()); } } } } nispor-1.2.19/query/mac_vlan.rs000064400000000000000000000106171046102023000145300ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoMacVlan, InfoMacVtap}; use serde::{Deserialize, Serialize}; use crate::{ mac::{parse_as_mac, ETH_ALEN}, Iface, IfaceType, NisporError, }; const MACVLAN_MODE_PRIVATE: u32 = 1; const MACVLAN_MODE_VEPA: u32 = 2; const MACVLAN_MODE_BRIDGE: u32 = 4; const MACVLAN_MODE_PASSTHRU: u32 = 8; const MACVLAN_MODE_SOURCE: u32 = 16; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum MacVlanMode { /* don't talk to other macvlans */ Private, /* talk to other ports through ext bridge */ Vepa, /* talk to bridge ports directly */ Bridge, /* take over the underlying device */ #[serde(rename = "passthru")] PassThrough, /* use source MAC address list to assign */ Source, Other(u32), Unknown, } impl Default for MacVlanMode { fn default() -> Self { MacVlanMode::Unknown } } impl From for MacVlanMode { fn from(d: u32) -> Self { match d { MACVLAN_MODE_PRIVATE => Self::Private, MACVLAN_MODE_VEPA => Self::Vepa, MACVLAN_MODE_BRIDGE => Self::Bridge, MACVLAN_MODE_PASSTHRU => Self::PassThrough, MACVLAN_MODE_SOURCE => Self::Source, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct MacVlanInfo { pub base_iface: String, pub mode: MacVlanMode, pub flags: u16, #[serde(skip_serializing_if = "Option::is_none")] pub allowed_mac_addresses: Option>, } pub(crate) fn get_mac_vlan_info( data: &InfoData, ) -> Result, NisporError> { let mut macv_info = MacVlanInfo::default(); if let InfoData::MacVlan(infos) = data { for info in infos { if let InfoMacVlan::Mode(d) = *info { macv_info.mode = d.into(); } else if let InfoMacVlan::Flags(d) = *info { macv_info.flags = d; } else if let InfoMacVlan::MacAddrData(d) = info { let mut addrs = Vec::new(); for macvlan in d { if let InfoMacVlan::MacAddr(mac_d) = macvlan { addrs.push(parse_as_mac(ETH_ALEN, mac_d)?); } } macv_info.allowed_mac_addresses = Some(addrs); } else { log::debug!("Unknown MAC VLAN info {:?}", info) } } Ok(Some(macv_info)) } else if let InfoData::MacVtap(infos) = data { for info in infos { if let InfoMacVtap::Mode(d) = *info { macv_info.mode = d.into(); } else if let InfoMacVtap::Flags(d) = *info { macv_info.flags = d; } else if let InfoMacVtap::MacAddrData(d) = info { let mut addrs = Vec::new(); for macvtap in d { if let InfoMacVtap::MacAddr(mac_d) = macvtap { addrs.push(parse_as_mac(ETH_ALEN, mac_d)?); } } macv_info.allowed_mac_addresses = Some(addrs); } else { log::debug!("Unknown MAC VTAP info {:?}", info) } } Ok(Some(macv_info)) } else { Ok(None) } } pub(crate) fn mac_vlan_iface_tidy_up( iface_states: &mut HashMap, ) { convert_base_iface_index_to_name(iface_states); } fn convert_base_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::MacVlan && iface.iface_type != IfaceType::MacVtap { continue; } if let Some(ref mut info) = iface.mac_vlan { if let Some(base_iface_name) = index_to_name.get(&info.base_iface) { info.base_iface.clone_from(base_iface_name); } } else if let Some(ref mut info) = iface.mac_vtap { if let Some(base_iface_name) = index_to_name.get(&info.base_iface) { info.base_iface.clone_from(base_iface_name); } } } } nispor-1.2.19/query/mac_vtap.rs000064400000000000000000000040351046102023000145370ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use netlink_packet_route::link::InfoData; use serde::{Deserialize, Serialize}; use super::mac_vlan::get_mac_vlan_info; use crate::{MacVlanInfo, MacVlanMode, NisporError}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum MacVtapMode { /* don't talk to other macvlans */ Private, /* talk to other ports through ext bridge */ Vepa, /* talk to bridge ports directly */ Bridge, /* take over the underlying device */ #[serde(rename = "passthru")] PassThrough, /* use source MAC address list to assign */ Source, Other(u32), Unknown, } impl Default for MacVtapMode { fn default() -> Self { MacVtapMode::Unknown } } impl From for MacVtapMode { fn from(d: MacVlanMode) -> Self { match d { MacVlanMode::Private => Self::Private, MacVlanMode::Vepa => Self::Vepa, MacVlanMode::Bridge => Self::Bridge, MacVlanMode::PassThrough => Self::PassThrough, MacVlanMode::Source => Self::Source, MacVlanMode::Unknown => Self::Unknown, MacVlanMode::Other(u32) => Self::Other(u32), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct MacVtapInfo { pub base_iface: String, pub mode: MacVtapMode, pub flags: u16, #[serde(skip_serializing_if = "Option::is_none")] pub allowed_mac_addresses: Option>, } impl From for MacVtapInfo { fn from(d: MacVlanInfo) -> Self { Self { base_iface: d.base_iface, mode: MacVtapMode::from(d.mode), flags: d.flags, allowed_mac_addresses: d.allowed_mac_addresses, } } } pub(crate) fn get_mac_vtap_info( data: &InfoData, ) -> Result, NisporError> { if let Some(info) = get_mac_vlan_info(data)? { Ok(Some(info.into())) } else { Ok(None) } } nispor-1.2.19/query/macsec.rs000064400000000000000000000137261046102023000142070ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoMacSec}; use serde::{Deserialize, Serialize}; use crate::{Iface, IfaceType}; const MACSEC_VALIDATE_DISABLED: u8 = 0; const MACSEC_VALIDATE_CHECK: u8 = 1; const MACSEC_VALIDATE_STRICT: u8 = 2; const MACSEC_OFFLOAD_OFF: u8 = 0; const MACSEC_OFFLOAD_PHY: u8 = 1; const MACSEC_OFFLOAD_MAC: u8 = 2; const MACSEC_CIPHER_ID_GCM_AES_128: u64 = 0x0080C20001000001; const MACSEC_CIPHER_ID_GCM_AES_256: u64 = 0x0080C20001000002; const MACSEC_CIPHER_ID_GCM_AES_XPN_128: u64 = 0x0080C20001000003; const MACSEC_CIPHER_ID_GCM_AES_XPN_256: u64 = 0x0080C20001000004; const MACSEC_DEFAULT_CIPHER_ID: u64 = 0x0080020001000001; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum MacSecValidate { Disabled, Check, Strict, Other(u8), Unknown, } impl Default for MacSecValidate { fn default() -> Self { MacSecValidate::Unknown } } impl From for MacSecValidate { fn from(d: u8) -> Self { match d { MACSEC_VALIDATE_DISABLED => Self::Disabled, MACSEC_VALIDATE_CHECK => Self::Check, MACSEC_VALIDATE_STRICT => Self::Strict, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub enum MacSecOffload { Off, Phy, Mac, Other(u8), Unknown, } impl Default for MacSecOffload { fn default() -> Self { MacSecOffload::Unknown } } impl From for MacSecOffload { fn from(d: u8) -> Self { match d { MACSEC_OFFLOAD_OFF => Self::Off, MACSEC_OFFLOAD_PHY => Self::Phy, MACSEC_OFFLOAD_MAC => Self::Mac, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub enum MacSecCipherId { GcmAes128, GcmAes256, GcmAesXpn128, GcmAesXpn256, Other(u64), } impl Default for MacSecCipherId { fn default() -> Self { MacSecCipherId::GcmAes128 } } impl From for MacSecCipherId { fn from(d: u64) -> Self { match d { MACSEC_DEFAULT_CIPHER_ID => Self::GcmAes128, MACSEC_CIPHER_ID_GCM_AES_128 => Self::GcmAes128, MACSEC_CIPHER_ID_GCM_AES_256 => Self::GcmAes256, MACSEC_CIPHER_ID_GCM_AES_XPN_128 => Self::GcmAesXpn128, MACSEC_CIPHER_ID_GCM_AES_XPN_256 => Self::GcmAesXpn256, _ => Self::Other(d), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct MacSecInfo { pub sci: u64, pub port: u16, pub icv_len: u8, pub cipher: MacSecCipherId, pub window: u32, pub encoding_sa: u8, pub encrypt: bool, pub protect: bool, pub send_sci: bool, pub end_station: bool, pub scb: bool, pub replay_protect: bool, pub validate: MacSecValidate, pub offload: MacSecOffload, #[serde(skip_serializing_if = "Option::is_none")] pub base_iface: Option, } pub(crate) fn get_macsec_info(data: &InfoData) -> Option { if let InfoData::MacSec(infos) = data { let mut macsec_info = MacSecInfo::default(); for info in infos { match *info { InfoMacSec::Sci(d) => { macsec_info.sci = d; } InfoMacSec::Port(d) => { macsec_info.port = d; } InfoMacSec::IcvLen(d) => { macsec_info.icv_len = d; } InfoMacSec::CipherSuite(d) => { macsec_info.cipher = u64::from(d).into(); } InfoMacSec::Window(d) => { macsec_info.window = d; } InfoMacSec::EncodingSa(d) => { macsec_info.encoding_sa = d; } InfoMacSec::Encrypt(d) => { macsec_info.encrypt = d > 0; } InfoMacSec::Protect(d) => { macsec_info.protect = d > 0; } InfoMacSec::IncSci(d) => { macsec_info.send_sci = d > 0; } InfoMacSec::Es(d) => { macsec_info.end_station = d > 0; } InfoMacSec::Scb(d) => { macsec_info.scb = d > 0; } InfoMacSec::ReplayProtect(d) => { macsec_info.replay_protect = d > 0; } InfoMacSec::Validation(d) => { macsec_info.validate = u8::from(d).into(); } InfoMacSec::Offload(d) => { macsec_info.offload = u8::from(d).into(); } _ => { log::debug!("Unknown MACsec info {:?}", info) } } } Some(macsec_info) } else { None } } pub(crate) fn macsec_iface_tidy_up(iface_states: &mut HashMap) { convert_base_iface_index_to_name(iface_states); } fn convert_base_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::MacSec { continue; } if let Some(ref mut macsec_info) = iface.macsec { if let Some(base_iface_name) = &macsec_info .base_iface .as_ref() .and_then(|i| index_to_name.get(i)) { macsec_info.base_iface = Some(base_iface_name.to_string()); } } } } nispor-1.2.19/query/mod.rs000064400000000000000000000044141046102023000135250ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 mod bond; mod bridge; mod hsr; mod ip; mod mptcp; mod xfrm; // Disable `needless_pass_by_ref_mut` check due to upstream issue: // https://github.com/rust-netlink/ethtool/issues/12 #[allow(clippy::needless_pass_by_ref_mut)] mod ethtool; mod iface; mod inter_ifaces; mod ipoib; mod mac_vlan; mod mac_vtap; mod macsec; mod route; mod route_rule; mod sriov; mod tun; mod veth; mod vlan; mod vrf; mod vxlan; pub use self::bond::{ BondAdInfo, BondAdSelect, BondAllSubordinatesActive, BondArpValidate, BondFailOverMac, BondInfo, BondLacpRate, BondMiiStatus, BondMode, BondModeArpAllTargets, BondPrimaryReselect, BondSubordinateInfo, BondSubordinateState, BondXmitHashPolicy, }; pub use self::bridge::{ BridgeInfo, BridgePortInfo, BridgePortMulticastRouterType, BridgePortStpState, BridgeStpState, BridgeVlanEntry, BridgeVlanProtocol, }; pub use self::ethtool::{ EthtoolCoalesceInfo, EthtoolFeatureInfo, EthtoolInfo, EthtoolLinkModeDuplex, EthtoolLinkModeInfo, EthtoolPauseInfo, EthtoolRingInfo, }; pub use self::hsr::{HsrInfo, HsrProtocol}; pub use self::iface::{ ControllerType, Iface, IfaceFlag, IfaceState, IfaceType, }; pub use self::ip::{ IpFamily, Ipv4AddrInfo, Ipv4Info, Ipv6AddrFlag, Ipv6AddrInfo, Ipv6Info, }; pub use self::ipoib::{IpoibInfo, IpoibMode}; pub use self::mac_vlan::{MacVlanInfo, MacVlanMode}; pub use self::mac_vtap::{MacVtapInfo, MacVtapMode}; pub use self::macsec::{ MacSecCipherId, MacSecInfo, MacSecOffload, MacSecValidate, }; pub use self::mptcp::{Mptcp, MptcpAddress, MptcpAddressFlag}; pub use self::route::{ AddressFamily, MultipathRoute, MultipathRouteFlags, Route, RouteProtocol, RouteScope, RouteType, }; pub use self::route_rule::{RouteRule, RuleAction}; pub use self::sriov::{SriovInfo, VfInfo, VfLinkState, VfState}; pub use self::tun::{TunInfo, TunMode}; pub use self::veth::VethInfo; pub use self::vlan::{VlanInfo, VlanProtocol}; pub use self::vrf::{VrfInfo, VrfSubordinateInfo}; pub use self::vxlan::VxlanInfo; pub use self::xfrm::XfrmInfo; pub(crate) use self::{ inter_ifaces::{get_iface_name2index, get_ifaces}, ip::{is_ipv6_addr, parse_ip_addr_str, parse_ip_net_addr_str}, mptcp::{get_mptcp, merge_mptcp_info}, route::get_routes, route_rule::get_route_rules, }; nispor-1.2.19/query/mptcp.rs000064400000000000000000000170431046102023000140730ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io::Read; use std::net::IpAddr; use futures::stream::TryStreamExt; use mptcp_pm::{ MptcpPathManagerAddressAttr, MptcpPathManagerAddressAttrFlag, MptcpPathManagerAttr, MptcpPathManagerLimitsAttr, MptcpPathManagerMessage, }; use serde::{Deserialize, Serialize}; use crate::{Iface, NisporError}; const MPTCP_SYSCTL_PATH: &str = "/proc/sys/net/mptcp/enabled"; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Mptcp { pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub add_addr_accepted_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub subflows_limit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub addresses: Option>, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[non_exhaustive] #[serde(rename_all = "snake_case")] pub enum MptcpAddressFlag { Signal, Subflow, Backup, Fullmesh, Implicit, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub struct MptcpAddress { pub address: IpAddr, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, #[serde(skip_serializing_if = "Option::is_none")] pub flags: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub iface: Option, #[serde(skip)] pub iface_index: Option, } pub(crate) async fn get_mptcp() -> Result { let mut ret = Mptcp { enabled: is_mptcp_enabled(), ..Default::default() }; if !ret.enabled { return Ok(ret); } let (connection, handle, _) = mptcp_pm::new_connection()?; tokio::spawn(connection); let mut limits_handle = handle.limits().get().execute().await; while let Some(genl_msg) = limits_handle.try_next().await? { let mptcp_msg = genl_msg.payload; for nla in mptcp_msg.nlas.as_slice() { match nla { MptcpPathManagerAttr::Limits( MptcpPathManagerLimitsAttr::RcvAddAddrs(d), ) => { ret.add_addr_accepted_limit = Some(*d); } MptcpPathManagerAttr::Limits( MptcpPathManagerLimitsAttr::Subflows(d), ) => { ret.subflows_limit = Some(*d); } _ => { log::info!("Unsupported MPTCP netlink attribute {:?}", nla) } } } } let mut address_handle = handle.address().get().execute().await; let mut addresses = Vec::new(); while let Some(genl_msg) = address_handle.try_next().await? { if let Some(addr) = mptcp_msg_to_nispor(&genl_msg.payload) { addresses.push(addr); } } ret.addresses = Some(addresses); Ok(ret) } fn is_mptcp_enabled() -> bool { if let Ok(mut fd) = std::fs::File::open(MPTCP_SYSCTL_PATH) { let mut content = [0u8; 1]; if fd.read_exact(&mut content).is_err() { false } else { content[0] == b'1' } } else { false } } // * Place address with interface to Iface // * Replace index to interface name pub(crate) fn merge_mptcp_info( iface_states: &mut HashMap, mptcp: &mut Mptcp, ) { let mut addr_index: HashMap> = HashMap::new(); let mut iface_index_map: HashMap = HashMap::new(); for iface in iface_states.values() { iface_index_map.insert(iface.index, iface.name.clone()); } if let Some(addrs) = mptcp.addresses.as_mut() { for addr in addrs { if let Some(iface_index) = addr.iface_index.as_ref() { if let Ok(i) = u32::try_from(*iface_index) { if let Some(iface_name) = iface_index_map.get(&i) { addr.iface = Some(iface_name.to_string()); match addr_index.entry(iface_name.to_string()) { Entry::Occupied(o) => { o.into_mut().push(addr.clone()); } Entry::Vacant(v) => { v.insert(vec![addr.clone()]); } }; } else { addr.iface = Some(iface_index.to_string()); } } else { log::error!("BUG: Got invalid iface index in {:?}", addr); } } } } for iface in iface_states.values_mut() { if let Some(addrs) = addr_index.remove(iface.name.as_str()) { iface.mptcp = Some(addrs); } } } fn mptcp_msg_to_nispor( mptcp_msg: &MptcpPathManagerMessage, ) -> Option { let mut address = None; for nla in mptcp_msg.nlas.as_slice() { if let MptcpPathManagerAttr::Address( MptcpPathManagerAddressAttr::Addr4(ip), ) = nla { address = Some(IpAddr::V4(*ip)); break; } else if let MptcpPathManagerAttr::Address( MptcpPathManagerAddressAttr::Addr6(ip), ) = nla { address = Some(IpAddr::V6(*ip)); break; } } if let Some(address) = address { let mut ret = MptcpAddress { address, id: None, port: None, flags: None, iface: None, iface_index: None, }; for nla in mptcp_msg.nlas.as_slice() { if let MptcpPathManagerAttr::Address(addr_attr) = nla { match addr_attr { MptcpPathManagerAddressAttr::Flags(flags) => { ret.flags = Some(mptcp_flags_to_nispor(flags)); } MptcpPathManagerAddressAttr::IfIndex(i) => { ret.iface_index = Some(*i); } MptcpPathManagerAddressAttr::Port(i) => { if *i != 0 { ret.port = Some(*i); } } MptcpPathManagerAddressAttr::Id(i) => { ret.id = Some(*i); } _ => (), } } } Some(ret) } else { None } } fn mptcp_flags_to_nispor( flags: &[MptcpPathManagerAddressAttrFlag], ) -> Vec { let mut ret = Vec::new(); for flag in flags { if let Some(f) = match flag { MptcpPathManagerAddressAttrFlag::Signal => { Some(MptcpAddressFlag::Signal) } MptcpPathManagerAddressAttrFlag::Subflow => { Some(MptcpAddressFlag::Subflow) } MptcpPathManagerAddressAttrFlag::Backup => { Some(MptcpAddressFlag::Backup) } MptcpPathManagerAddressAttrFlag::Fullmesh => { Some(MptcpAddressFlag::Fullmesh) } MptcpPathManagerAddressAttrFlag::Implicit => { Some(MptcpAddressFlag::Implicit) } _ => { log::info!("Unsupported address flag {:?}", flag); None } } { ret.push(f); } } ret } nispor-1.2.19/query/route.rs000064400000000000000000000653011046102023000141060ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use futures::stream::TryStreamExt; use netlink_packet_route::route::{ self as rt, RouteAddress, RouteAttribute, RouteMessage, RouteMetric, RouteVia, }; use netlink_sys::AsyncSocket; use rtnetlink::{new_connection, IpVersion}; use serde::{Deserialize, Serialize}; use super::super::filter::{apply_kernel_route_filter, should_drop_by_filter}; use crate::{NetStateRouteFilter, NisporError}; const USER_HZ: u32 = 100; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct Route { pub address_family: AddressFamily, pub tos: u8, pub table: u32, pub protocol: RouteProtocol, pub scope: RouteScope, pub route_type: RouteType, pub flags: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub dst: Option, #[serde(skip_serializing_if = "Option::is_none")] pub oif: Option, #[serde(skip_serializing_if = "Option::is_none")] pub iif: Option, #[serde(skip_serializing_if = "Option::is_none")] pub prefered_src: Option, #[serde(skip_serializing_if = "Option::is_none")] pub src: Option, #[serde(skip_serializing_if = "Option::is_none")] pub realm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gateway: Option, #[serde(skip_serializing_if = "Option::is_none")] pub via: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mark: Option, #[serde(skip_serializing_if = "Option::is_none")] pub uid: Option, // Below are RTAX_* of RTA_METRICS #[serde(skip_serializing_if = "Option::is_none")] pub lock: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mtu: Option, #[serde(skip_serializing_if = "Option::is_none")] pub window: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rtt: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rttvar: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ssthresh: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cwnd: Option, #[serde(skip_serializing_if = "Option::is_none")] pub advmss: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reordering: Option, #[serde(skip_serializing_if = "Option::is_none")] pub hoplimit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub initcwnd: Option, #[serde(skip_serializing_if = "Option::is_none")] pub features: Option, #[serde(skip_serializing_if = "Option::is_none")] pub rto_min: Option, #[serde(skip_serializing_if = "Option::is_none")] pub initrwnd: Option, #[serde(skip_serializing_if = "Option::is_none")] pub quickack: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cc_algo: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fastopen_no_cookie: Option, // Below are RTM_CACHEINFO #[serde(skip_serializing_if = "Option::is_none")] pub cache_clntref: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_last_use: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_expires: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_error: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_used: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_ts: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cache_ts_age: Option, // Below are IPv6 only #[serde(skip_serializing_if = "Option::is_none")] pub metric: Option, #[serde(skip_serializing_if = "Option::is_none")] pub preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub multipath: Option>, // Missing support of RTA_NH_ID } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum AddressFamily { IPv4, IPv6, Other(u8), #[default] Unknown, } impl From for AddressFamily { fn from(d: netlink_packet_route::AddressFamily) -> Self { match d { netlink_packet_route::AddressFamily::Inet => AddressFamily::IPv4, netlink_packet_route::AddressFamily::Inet6 => AddressFamily::IPv6, _ => Self::Other(u8::from(d)), } } } impl From for netlink_packet_route::AddressFamily { fn from(v: AddressFamily) -> Self { match v { AddressFamily::IPv4 => netlink_packet_route::AddressFamily::Inet, AddressFamily::IPv6 => netlink_packet_route::AddressFamily::Inet6, AddressFamily::Other(d) => d.into(), AddressFamily::Unknown => { netlink_packet_route::AddressFamily::Unspec } } } } #[derive( Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, )] #[serde(rename_all = "lowercase")] pub enum RouteProtocol { #[default] Unspec, #[serde(rename = "icmp_redirect")] IcmpRedirect, Kernel, Boot, Static, Gated, Ra, #[serde(rename = "merit_mrt")] Mrt, Zebra, Bird, #[serde(rename = "decnet_routing_daemon")] DnRouted, Xorp, #[serde(rename = "netsukuku")] Ntk, Dhcp, #[serde(rename = "multicast_daemon")] Mrouted, #[serde(rename = "keepalived_daemon")] KeepAlived, Babel, Bgp, Isis, Ospf, Rip, Eigrp, Unknown, Other(u8), } impl From for RouteProtocol { fn from(d: rt::RouteProtocol) -> Self { match d { rt::RouteProtocol::Unspec => RouteProtocol::Unspec, rt::RouteProtocol::IcmpRedirect => RouteProtocol::IcmpRedirect, rt::RouteProtocol::Kernel => RouteProtocol::Kernel, rt::RouteProtocol::Boot => RouteProtocol::Boot, rt::RouteProtocol::Static => RouteProtocol::Static, rt::RouteProtocol::Gated => RouteProtocol::Gated, rt::RouteProtocol::Ra => RouteProtocol::Ra, rt::RouteProtocol::Mrt => RouteProtocol::Mrt, rt::RouteProtocol::Zebra => RouteProtocol::Zebra, rt::RouteProtocol::Bird => RouteProtocol::Bird, rt::RouteProtocol::DnRouted => RouteProtocol::DnRouted, rt::RouteProtocol::Xorp => RouteProtocol::Xorp, rt::RouteProtocol::Ntk => RouteProtocol::Ntk, rt::RouteProtocol::Dhcp => RouteProtocol::Dhcp, rt::RouteProtocol::Mrouted => RouteProtocol::Mrouted, rt::RouteProtocol::KeepAlived => RouteProtocol::KeepAlived, rt::RouteProtocol::Babel => RouteProtocol::Babel, rt::RouteProtocol::Bgp => RouteProtocol::Bgp, rt::RouteProtocol::Isis => RouteProtocol::Isis, rt::RouteProtocol::Ospf => RouteProtocol::Ospf, rt::RouteProtocol::Rip => RouteProtocol::Rip, rt::RouteProtocol::Eigrp => RouteProtocol::Eigrp, _ => RouteProtocol::Other(d.into()), } } } impl From for rt::RouteProtocol { fn from(v: RouteProtocol) -> Self { match v { RouteProtocol::Unspec => rt::RouteProtocol::Unspec, RouteProtocol::IcmpRedirect => rt::RouteProtocol::IcmpRedirect, RouteProtocol::Kernel => rt::RouteProtocol::Kernel, RouteProtocol::Boot => rt::RouteProtocol::Boot, RouteProtocol::Static => rt::RouteProtocol::Static, RouteProtocol::Gated => rt::RouteProtocol::Gated, RouteProtocol::Ra => rt::RouteProtocol::Ra, RouteProtocol::Mrt => rt::RouteProtocol::Mrt, RouteProtocol::Zebra => rt::RouteProtocol::Zebra, RouteProtocol::Bird => rt::RouteProtocol::Bird, RouteProtocol::DnRouted => rt::RouteProtocol::DnRouted, RouteProtocol::Xorp => rt::RouteProtocol::Xorp, RouteProtocol::Ntk => rt::RouteProtocol::Ntk, RouteProtocol::Dhcp => rt::RouteProtocol::Dhcp, RouteProtocol::Mrouted => rt::RouteProtocol::Mrouted, RouteProtocol::KeepAlived => rt::RouteProtocol::KeepAlived, RouteProtocol::Babel => rt::RouteProtocol::Babel, RouteProtocol::Bgp => rt::RouteProtocol::Bgp, RouteProtocol::Isis => rt::RouteProtocol::Isis, RouteProtocol::Ospf => rt::RouteProtocol::Ospf, RouteProtocol::Rip => rt::RouteProtocol::Rip, RouteProtocol::Eigrp => rt::RouteProtocol::Eigrp, RouteProtocol::Unknown => rt::RouteProtocol::Unspec, RouteProtocol::Other(d) => d.into(), } } } impl From<&str> for RouteProtocol { fn from(v: &str) -> Self { match v { "icmp_redirect" => RouteProtocol::IcmpRedirect, "kernel" => RouteProtocol::Kernel, "boot" => RouteProtocol::Boot, "static" => RouteProtocol::Static, "gated" => RouteProtocol::Gated, "ra" => RouteProtocol::Ra, "merit_mrt" => RouteProtocol::Mrt, "zebra" => RouteProtocol::Zebra, "bird" => RouteProtocol::Bird, "decnet_routing_daemon" => RouteProtocol::DnRouted, "xorp" => RouteProtocol::Xorp, "netsukuku" => RouteProtocol::Ntk, "Dhcp" => RouteProtocol::Dhcp, "multicast_daemon" => RouteProtocol::Mrouted, "keepalived_daemon" => RouteProtocol::KeepAlived, "babel" => RouteProtocol::Babel, "bgp" => RouteProtocol::Bgp, "isis" => RouteProtocol::Isis, "ospf" => RouteProtocol::Ospf, "rip" => RouteProtocol::Rip, "eigrp" => RouteProtocol::Eigrp, _ => RouteProtocol::Unknown, } } } /* * Kernel Doc for route scope: * Really it is not scope, but sort of distance to the destination. * NOWHERE are reserved for not existing destinations, HOST is our * local addresses, LINK are destinations, located on directly attached * link and UNIVERSE is everywhere in the Universe. * Intermediate values are also possible f.e. interior routes * could be assigned a value between UNIVERSE and LINK. */ #[derive( Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, )] #[serde(rename_all = "lowercase")] pub enum RouteScope { Universe, Site, Link, Host, #[serde(rename = "no_where")] NoWhere, #[default] Unknown, Other(u8), } impl From for RouteScope { fn from(d: rt::RouteScope) -> Self { match d { rt::RouteScope::Universe => RouteScope::Universe, rt::RouteScope::Site => RouteScope::Site, rt::RouteScope::Link => RouteScope::Link, rt::RouteScope::Host => RouteScope::Host, rt::RouteScope::NoWhere => RouteScope::NoWhere, _ => RouteScope::Other(d.into()), } } } impl From for rt::RouteScope { fn from(v: RouteScope) -> rt::RouteScope { match v { RouteScope::Universe => rt::RouteScope::Universe, RouteScope::Site => rt::RouteScope::Site, RouteScope::Link => rt::RouteScope::Link, RouteScope::Host => rt::RouteScope::Host, RouteScope::NoWhere => rt::RouteScope::NoWhere, RouteScope::Unknown => rt::RouteScope::Universe, RouteScope::Other(d) => d.into(), } } } impl std::fmt::Display for RouteScope { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Universe => write!(f, "universe"), Self::Site => write!(f, "site"), Self::Link => write!(f, "link"), Self::Host => write!(f, "host"), Self::NoWhere => write!(f, "no_where"), Self::Unknown => write!(f, "unknown"), Self::Other(s) => write!(f, "{s}"), } } } impl From<&str> for RouteScope { fn from(v: &str) -> Self { match v { "u" | "universe" | "g" | "global" => RouteScope::Universe, "s" | "site" => RouteScope::Site, "l" | "link" => RouteScope::Link, "h" | "host" => RouteScope::Host, "n" | "nowhere" | "no_where" => RouteScope::NoWhere, _ => RouteScope::Unknown, } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "lowercase")] pub enum RouteType { #[default] Unspec, Unicast, Local, Broadcast, Anycast, Multicast, BlackHole, Unreachable, Prohibit, Throw, Nat, ExternalResolve, Unknown, Other(u8), } impl From for RouteType { fn from(d: rt::RouteType) -> Self { match d { rt::RouteType::Unspec => RouteType::Unspec, rt::RouteType::Unicast => RouteType::Unicast, rt::RouteType::Local => RouteType::Local, rt::RouteType::Broadcast => RouteType::Broadcast, rt::RouteType::Anycast => RouteType::Anycast, rt::RouteType::Multicast => RouteType::Multicast, rt::RouteType::BlackHole => RouteType::BlackHole, rt::RouteType::Unreachable => RouteType::Unreachable, rt::RouteType::Prohibit => RouteType::Prohibit, rt::RouteType::Throw => RouteType::Throw, rt::RouteType::Nat => RouteType::Nat, rt::RouteType::ExternalResolve => RouteType::ExternalResolve, _ => RouteType::Other(d.into()), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "lowercase")] #[non_exhaustive] pub struct MultipathRoute { pub via: String, pub iface: String, pub weight: u16, // The kernel is u8, but ip route show it after + 1. pub flags: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "lowercase")] pub enum MultipathRouteFlags { Dead, Pervasive, #[serde(rename = "on_link")] OnLink, Offload, #[serde(rename = "link_down")] LinkDown, Unresolved, Trap, Other(u8), } impl From for MultipathRouteFlags { fn from(d: rt::RouteNextHopFlag) -> Self { match d { rt::RouteNextHopFlag::Dead => Self::Dead, rt::RouteNextHopFlag::Pervasive => Self::Pervasive, rt::RouteNextHopFlag::Onlink => Self::OnLink, rt::RouteNextHopFlag::Offload => Self::Offload, rt::RouteNextHopFlag::Linkdown => Self::LinkDown, rt::RouteNextHopFlag::Unresolved => Self::Unresolved, rt::RouteNextHopFlag::Trap => Self::Trap, _ => Self::Other(u8::from(d)), } } } pub(crate) async fn get_routes( iface_name2index: &HashMap, filter: Option<&NetStateRouteFilter>, ) -> Result, NisporError> { let mut routes = Vec::new(); let mut has_kernel_filter = true; let (mut connection, handle, _) = new_connection()?; let mut ifindex_to_name = HashMap::new(); for (name, index) in iface_name2index.iter() { ifindex_to_name.insert(format!("{index}"), name.to_string()); } if filter.is_some() { if let Err(e) = connection .socket_mut() .socket_mut() .set_netlink_get_strict_chk(true) { log::warn!( "Failed to set kernel space route filter: {e}, \ falling back to user space route filtering which would \ lead to performance penalty" ); has_kernel_filter = false; } } tokio::spawn(connection); for ip_family in [IpVersion::V6, IpVersion::V4] { let mut rt_handle = handle.route().get(ip_family); if let Some(filter) = filter { if has_kernel_filter { apply_kernel_route_filter( &mut rt_handle, filter, iface_name2index, )?; } } let mut links = rt_handle.execute(); while let Some(rt_msg) = links.try_next().await? { let route = get_route(rt_msg, &ifindex_to_name)?; // User space filter is required for RT_SCOPE_UNIVERSE and etc if let Some(filter) = filter { if should_drop_by_filter(&route, filter, has_kernel_filter) { continue; } } routes.push(route); } } Ok(routes) } fn get_route( route_msg: RouteMessage, ifindex_to_name: &HashMap, ) -> Result { let mut rt = Route::default(); let header = &route_msg.header; rt.address_family = header.address_family.into(); let src_prefix_len = header.source_prefix_length; let dst_prefix_len = header.destination_prefix_length; rt.table = header.table.into(); rt.tos = header.tos; rt.protocol = header.protocol.into(); rt.scope = header.scope.into(); rt.flags = header .flags .as_slice() .iter() .map(|f| RouteFlag::from(*f)) .collect(); rt.route_type = header.kind.into(); let _family = &rt.address_family; for nla in &route_msg.attributes { match nla { RouteAttribute::Destination(d) => { rt.dst = Some(format!( "{}/{}", _rt_addr_to_string(d), dst_prefix_len )); } RouteAttribute::Oif(d) => { rt.oif = if let Some(iface_name) = ifindex_to_name.get(&format!("{d}")) { Some(iface_name.clone()) } else { Some(format!("{d}")) } } RouteAttribute::PrefSource(d) => { rt.prefered_src = Some(_rt_addr_to_string(d)); } RouteAttribute::Table(d) => { rt.table = *d; } RouteAttribute::Realm(d) => { rt.realm = Some((*d).into()); } RouteAttribute::Source(d) => { rt.src = Some(format!( "{}/{}", _rt_addr_to_string(d), src_prefix_len )); } RouteAttribute::Gateway(d) => { rt.gateway = Some(_rt_addr_to_string(d)); } RouteAttribute::Via(d) => { if let RouteVia::Inet(a) = d { rt.via = Some(a.to_string()); } else if let RouteVia::Inet6(a) = d { rt.via = Some(a.to_string()); } } RouteAttribute::Metrics(nlas) => { for nla in nlas { match nla { RouteMetric::Lock(d) => { rt.lock = Some(*d); } RouteMetric::Mtu(d) => { rt.mtu = Some(*d); } RouteMetric::Window(d) => { rt.window = Some(*d); } RouteMetric::Rtt(d) => { rt.rtt = Some(*d); } RouteMetric::RttVar(d) => { rt.rttvar = Some(*d); } RouteMetric::SsThresh(d) => { rt.ssthresh = Some(*d); } RouteMetric::Cwnd(d) => { rt.cwnd = Some(*d); } RouteMetric::Advmss(d) => { rt.advmss = Some(*d); } RouteMetric::Reordering(d) => { rt.reordering = Some(*d); } RouteMetric::Hoplimit(d) => { rt.hoplimit = Some(*d); } RouteMetric::InitCwnd(d) => { rt.initcwnd = Some(*d); } RouteMetric::Features(d) => { rt.features = Some(*d); } RouteMetric::RtoMin(d) => { rt.rto_min = Some(*d); } RouteMetric::InitRwnd(d) => { rt.initrwnd = Some(*d); } RouteMetric::QuickAck(d) => { rt.quickack = Some(*d); } RouteMetric::CcAlgo(d) => { rt.cc_algo = Some(*d); } RouteMetric::FastopenNoCookie(d) => { rt.fastopen_no_cookie = Some(*d); } _ => { log::debug!( "Unknown RTA_METRICS message {:?}", nla ); } } } } RouteAttribute::Mark(d) => { rt.mark = Some(*d); } RouteAttribute::Uid(d) => { rt.uid = Some(*d); } RouteAttribute::Iif(d) => { rt.iif = if let Some(iface_name) = ifindex_to_name.get(&format!("{d}")) { Some(iface_name.clone()) } else { Some(format!("{d}")) } } RouteAttribute::CacheInfo(d) => { rt.cache_clntref = Some(d.clntref); rt.cache_last_use = Some(d.last_use); rt.cache_expires = Some(d.expires / USER_HZ); rt.cache_error = Some(d.error); rt.cache_used = Some(d.used); rt.cache_id = Some(d.id); rt.cache_ts = Some(d.ts); rt.cache_ts_age = Some(d.ts_age); } RouteAttribute::Priority(d) => { rt.metric = Some(*d); } RouteAttribute::MultiPath(hops) => { let mut next_hops = Vec::new(); for hop in hops.as_slice() { let mut mp_rt = MultipathRoute::default(); for nla in hop.attributes.iter() { if let RouteAttribute::Gateway(v) = nla { if let RouteAddress::Inet(v) = v { mp_rt.via = v.to_string(); } else if let RouteAddress::Inet6(v) = v { mp_rt.via = v.to_string(); } break; } } let iface_index = hop.interface_index; mp_rt.iface = if let Some(iface_name) = ifindex_to_name.get(&format!("{iface_index}")) { iface_name.clone() } else { format!("{iface_index}") }; mp_rt.flags = hop .flags .as_slice() .iter() .map(|f| MultipathRouteFlags::from(*f)) .collect(); // +1 because ip route does so mp_rt.weight = u16::from(hop.hops) + 1; next_hops.push(mp_rt); } rt.multipath = Some(next_hops); } RouteAttribute::Preference(d) => rt.preference = Some((*d).into()), _ => log::debug!("Unknown NLA message for route {:?}", nla), } } Ok(rt) } fn _rt_addr_to_string(addr: &RouteAddress) -> String { match addr { RouteAddress::Inet(v) => v.to_string(), RouteAddress::Inet6(v) => v.to_string(), _ => { log::debug!("Unknown RouteAddress type {:?}", addr); String::new() } } } #[derive( Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, )] #[non_exhaustive] #[serde(rename_all = "lowercase")] pub enum RoutePreference { Low, #[default] Medium, High, Invalid, Other(u8), } impl From for RoutePreference { fn from(d: rt::RoutePreference) -> Self { match d { rt::RoutePreference::Low => Self::Low, rt::RoutePreference::Medium => Self::Medium, rt::RoutePreference::High => Self::High, rt::RoutePreference::Invalid => Self::Invalid, _ => Self::Other(d.into()), } } } #[derive( Clone, Eq, PartialEq, Debug, Copy, Default, Serialize, Deserialize, )] #[serde(rename_all = "lowercase")] pub struct RouteRealm { pub source: u16, pub destination: u16, } impl From for RouteRealm { fn from(d: rt::RouteRealm) -> Self { Self { source: d.source, destination: d.destination, } } } #[derive(Clone, Eq, PartialEq, Debug, Copy, Serialize, Deserialize)] #[non_exhaustive] #[serde(rename_all = "snake_case")] pub enum RouteFlag { Dead, Pervasive, Onlink, Offload, Linkdown, Unresolved, Trap, Notify, Cloned, Equalize, Prefix, LookupTable, FibMatch, RtOffload, RtTrap, OffloadFailed, Other(u32), } impl From for RouteFlag { fn from(d: rt::RouteFlag) -> Self { match d { rt::RouteFlag::Dead => Self::Dead, rt::RouteFlag::Pervasive => Self::Pervasive, rt::RouteFlag::Onlink => Self::Onlink, rt::RouteFlag::Offload => Self::Offload, rt::RouteFlag::Linkdown => Self::Linkdown, rt::RouteFlag::Unresolved => Self::Unresolved, rt::RouteFlag::Trap => Self::Trap, rt::RouteFlag::Notify => Self::Notify, rt::RouteFlag::Cloned => Self::Cloned, rt::RouteFlag::Equalize => Self::Equalize, rt::RouteFlag::Prefix => Self::Prefix, rt::RouteFlag::LookupTable => Self::LookupTable, rt::RouteFlag::FibMatch => Self::FibMatch, rt::RouteFlag::RtOffload => Self::RtOffload, rt::RouteFlag::RtTrap => Self::RtTrap, rt::RouteFlag::OffloadFailed => Self::OffloadFailed, _ => Self::Other(d.into()), } } } nispor-1.2.19/query/route_rule.rs000064400000000000000000000217261046102023000151400ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use futures::stream::TryStreamExt; use netlink_packet_route::route::RouteHeader; use netlink_packet_route::rule::{self, RuleAttribute, RuleMessage}; use rtnetlink::new_connection; use rtnetlink::IpVersion; use serde::{Deserialize, Serialize}; use crate::{AddressFamily, NisporError, RouteProtocol}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "snake_case")] pub enum RuleAction { #[default] Unspec, /* Pass to fixed table or l3mdev */ Table, /* Jump to another rule */ Goto, /* No operation */ Nop, /* Drop without notification */ Blackhole, /* Drop with ENETUNREACH */ Unreachable, /* Drop with EACCES */ Prohibit, Other(u8), } impl From for RuleAction { fn from(d: rule::RuleAction) -> Self { match d { rule::RuleAction::Unspec => Self::Unspec, rule::RuleAction::ToTable => Self::Table, rule::RuleAction::Goto => Self::Goto, rule::RuleAction::Nop => Self::Nop, rule::RuleAction::Blackhole => Self::Blackhole, rule::RuleAction::Unreachable => Self::Unreachable, rule::RuleAction::Prohibit => Self::Prohibit, _ => Self::Other(d.into()), } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct RouteRule { pub action: RuleAction, pub address_family: AddressFamily, pub flags: u32, pub tos: u8, #[serde(skip_serializing_if = "Option::is_none")] pub table: Option, #[serde(skip_serializing_if = "Option::is_none")] pub dst: Option, #[serde(skip_serializing_if = "Option::is_none")] pub src: Option, #[serde(skip_serializing_if = "Option::is_none")] pub iif: Option, #[serde(skip_serializing_if = "Option::is_none")] pub oif: Option, #[serde(skip_serializing_if = "Option::is_none")] pub goto: Option, #[serde(skip_serializing_if = "Option::is_none")] pub priority: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fw_mark: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fw_mask: Option, #[serde(skip_serializing_if = "Option::is_none")] pub mask: Option, #[serde(skip_serializing_if = "Option::is_none")] pub realm: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tun_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub suppress_ifgroup: Option, #[serde(skip_serializing_if = "Option::is_none")] pub suppress_prefix_len: Option, #[serde(skip_serializing_if = "Option::is_none")] pub protocol: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ip_proto: Option, #[serde(skip_serializing_if = "Option::is_none")] pub src_port_range: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub dst_port_range: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub l3mdev: Option, } pub(crate) async fn get_route_rules() -> Result, NisporError> { let mut rules = Vec::new(); let (connection, handle, _) = new_connection()?; tokio::spawn(connection); let mut links = handle.rule().get(IpVersion::V6).execute(); while let Some(rt_msg) = links.try_next().await? { rules.push(get_rule(rt_msg)?); } let mut links = handle.rule().get(IpVersion::V4).execute(); while let Some(rt_msg) = links.try_next().await? { rules.push(get_rule(rt_msg)?); } Ok(rules) } fn get_rule(rule_msg: RuleMessage) -> Result { let mut rl = RouteRule::default(); let header = &rule_msg.header; rl.address_family = header.family.into(); let src_prefix_len = header.src_len; let dst_prefix_len = header.dst_len; rl.tos = header.tos; rl.action = header.action.into(); if header.table > RouteHeader::RT_TABLE_UNSPEC { rl.table = Some(header.table.into()); } let _family = &rl.address_family; for nla in &rule_msg.attributes { match nla { RuleAttribute::Destination(d) => { rl.dst = Some(format!("{}/{}", d, dst_prefix_len,)); } RuleAttribute::Source(d) => { rl.src = Some(format!("{}/{}", d, src_prefix_len,)); } RuleAttribute::Iifname(d) => { rl.iif = Some(d.clone().to_string()); } RuleAttribute::Oifname(d) => { rl.oif = Some(d.clone().to_string()); } RuleAttribute::Goto(d) => { rl.goto = Some(*d); } RuleAttribute::Priority(d) => { rl.priority = Some(*d); } RuleAttribute::FwMark(d) => { rl.fw_mark = Some(*d); } RuleAttribute::FwMask(d) => { rl.fw_mask = Some(*d); } RuleAttribute::Realm(d) => { rl.realm = Some((*d).into()); } RuleAttribute::TunId(d) => { rl.tun_id = Some(*d); } RuleAttribute::SuppressIfGroup(d) => { if *d != u32::MAX { rl.suppress_ifgroup = Some(*d); } } RuleAttribute::SuppressPrefixLen(d) => { if *d != u32::MAX { rl.suppress_prefix_len = Some(*d); } } RuleAttribute::Table(d) => { if *d > RouteHeader::RT_TABLE_UNSPEC.into() { rl.table = Some(*d); } } RuleAttribute::Protocol(d) => { rl.protocol = Some((*d).into()); } RuleAttribute::IpProtocol(d) => { rl.ip_proto = Some((*d).into()); } RuleAttribute::L3MDev(d) => { rl.l3mdev = Some(*d); } _ => log::debug!("Unknown NLA message for route rule {:?}", nla), } } Ok(rl) } #[derive( Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, )] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum IpProtocol { Hopopts, Icmp, Igmp, Ipip, Tcp, Egp, Pup, Udp, Idp, Tp, Dccp, Ipv6, Rsvp, Gre, Esp, Ah, Mtp, Beetph, Encap, Pim, Comp, L2tp, Sctp, Udplite, Mpls, Ethernet, #[default] Raw, Mptcp, Other(i32), } impl From for IpProtocol { fn from(d: netlink_packet_route::IpProtocol) -> Self { match d { netlink_packet_route::IpProtocol::Hopopts => Self::Hopopts, netlink_packet_route::IpProtocol::Icmp => Self::Icmp, netlink_packet_route::IpProtocol::Igmp => Self::Igmp, netlink_packet_route::IpProtocol::Ipip => Self::Ipip, netlink_packet_route::IpProtocol::Tcp => Self::Tcp, netlink_packet_route::IpProtocol::Egp => Self::Egp, netlink_packet_route::IpProtocol::Pup => Self::Pup, netlink_packet_route::IpProtocol::Udp => Self::Udp, netlink_packet_route::IpProtocol::Idp => Self::Idp, netlink_packet_route::IpProtocol::Tp => Self::Tp, netlink_packet_route::IpProtocol::Dccp => Self::Dccp, netlink_packet_route::IpProtocol::Ipv6 => Self::Ipv6, netlink_packet_route::IpProtocol::Rsvp => Self::Rsvp, netlink_packet_route::IpProtocol::Gre => Self::Gre, netlink_packet_route::IpProtocol::Esp => Self::Esp, netlink_packet_route::IpProtocol::Ah => Self::Ah, netlink_packet_route::IpProtocol::Mtp => Self::Mtp, netlink_packet_route::IpProtocol::Beetph => Self::Beetph, netlink_packet_route::IpProtocol::Encap => Self::Encap, netlink_packet_route::IpProtocol::Pim => Self::Pim, netlink_packet_route::IpProtocol::Comp => Self::Comp, netlink_packet_route::IpProtocol::L2tp => Self::L2tp, netlink_packet_route::IpProtocol::Sctp => Self::Sctp, netlink_packet_route::IpProtocol::Udplite => Self::Udplite, netlink_packet_route::IpProtocol::Mpls => Self::Mpls, netlink_packet_route::IpProtocol::Ethernet => Self::Ethernet, netlink_packet_route::IpProtocol::Raw => Self::Raw, netlink_packet_route::IpProtocol::Mptcp => Self::Mptcp, _ => Self::Other(d.into()), } } } #[derive(Clone, Eq, PartialEq, Debug, Copy, Serialize, Deserialize)] pub struct RouteRealm { pub source: u16, pub destination: u16, } impl From for RouteRealm { fn from(d: netlink_packet_route::route::RouteRealm) -> Self { Self { source: d.source, destination: d.destination, } } } nispor-1.2.19/query/sriov.rs000064400000000000000000000175731046102023000141220ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{self, LinkVfInfo}; use serde::{Deserialize, Serialize}; use crate::{ mac::{parse_as_mac, ETH_ALEN, INFINIBAND_ALEN}, Iface, IfaceType, NisporError, VlanProtocol, }; const MAX_ADDR_LEN: usize = 32; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum VfLinkState { #[default] Auto, Enable, Disable, Other(u32), Unknown, } impl From for VfLinkState { fn from(d: link::VfLinkState) -> Self { match d { link::VfLinkState::Auto => VfLinkState::Auto, link::VfLinkState::Enable => VfLinkState::Enable, link::VfLinkState::Disable => VfLinkState::Disable, _ => VfLinkState::Other(d.into()), } } } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Default)] #[non_exhaustive] pub struct VfState { pub rx_packets: u64, pub tx_packets: u64, pub rx_bytes: u64, pub tx_bytes: u64, pub broadcast: u64, pub multicast: u64, pub rx_dropped: u64, pub tx_dropped: u64, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct SriovInfo { pub vfs: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VfInfo { #[serde(skip_serializing_if = "Option::is_none")] pub iface_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pf_name: Option, pub id: u32, pub mac: String, pub broadcast: String, // 0 disables VLAN filter pub vlan_id: u32, pub qos: u32, pub vlan_proto: VlanProtocol, // Max TX bandwidth in Mbps, 0 disables throttling pub tx_rate: u32, pub spoof_check: bool, pub link_state: VfLinkState, // Min Bandwidth in Mbps pub min_tx_rate: u32, // Max Bandwidth in Mbps pub max_tx_rate: u32, pub query_rss: bool, pub state: VfState, pub trust: bool, #[serde(skip_serializing_if = "Option::is_none")] pub ib_node_guid: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ib_port_guid: Option, } pub(crate) fn get_sriov_info( pf_iface_name: &str, nlas: &[LinkVfInfo], iface_type: &IfaceType, ) -> Result { let mut sriov_info = SriovInfo::default(); let mac_len = match iface_type { IfaceType::Ethernet => ETH_ALEN, IfaceType::Infiniband => INFINIBAND_ALEN, _ => MAX_ADDR_LEN, }; for port_nlas in nlas { let mut vf_info = VfInfo::default(); for port_info in &port_nlas.0 { match port_info { link::VfInfo::Mac(m) => { vf_info.id = m.vf_id; vf_info.iface_name = get_vf_iface_name(pf_iface_name, &vf_info.id); vf_info.pf_name = Some(pf_iface_name.to_string()); vf_info.mac = parse_as_mac(mac_len, &m.mac)?; } link::VfInfo::Vlan(v) => { vf_info.vlan_id = v.vlan_id; vf_info.qos = v.qos; } link::VfInfo::TxRate(v) => vf_info.tx_rate = v.rate, link::VfInfo::SpoofCheck(v) => vf_info.spoof_check = v.enabled, link::VfInfo::LinkState(v) => { vf_info.link_state = v.state.into(); } link::VfInfo::Rate(v) => { vf_info.min_tx_rate = v.min_tx_rate; vf_info.max_tx_rate = v.max_tx_rate; } link::VfInfo::RssQueryEn(v) => vf_info.query_rss = v.enabled, link::VfInfo::Stats(v) => { vf_info.state = parse_vf_stats(v.as_slice())?; } link::VfInfo::Trust(v) => vf_info.trust = v.enabled, link::VfInfo::IbNodeGuid(v) => { vf_info.ib_node_guid = Some(format!("{:X}", v.guid)); } link::VfInfo::IbPortGuid(v) => { vf_info.ib_port_guid = Some(format!("{:X}", v.guid)); } link::VfInfo::VlanList(v) => { if let Some(link::VfVlan::Info(vf_vlan_info)) = v.first() { vf_info.vlan_proto = vf_vlan_info.protocol.into(); } } link::VfInfo::Broadcast(v) => { vf_info.broadcast = parse_as_mac(mac_len, &v.addr)?; } _ => { log::debug!("Unhandled SRIOV NLA {port_info:?}",); } } } sriov_info.vfs.push(vf_info); } Ok(sriov_info) } fn parse_vf_stats(nlas: &[link::VfStats]) -> Result { let mut state = VfState::default(); for nla in nlas { match nla { link::VfStats::RxPackets(d) => state.rx_packets = *d, link::VfStats::TxPackets(d) => state.tx_packets = *d, link::VfStats::RxBytes(d) => state.rx_bytes = *d, link::VfStats::TxBytes(d) => state.tx_bytes = *d, link::VfStats::Broadcast(d) => state.broadcast = *d, link::VfStats::Multicast(d) => state.multicast = *d, link::VfStats::RxDropped(d) => state.rx_dropped = *d, link::VfStats::TxDropped(d) => state.tx_dropped = *d, _ => log::debug!("Unhandled IFLA_VF_STATS {:?}", nla), } } Ok(state) } // Currently there is no valid netlink way to get information as the kernel code // is in at PCI level: drivers/pci/iov.c // We use sysfs content /sys/class/net//devices/virtfn/net/ fn get_vf_iface_name(pf_name: &str, sriov_id: &u32) -> Option { let sysfs_path = format!("/sys/class/net/{pf_name}/device/virtfn{sriov_id}/net/"); read_folder(&sysfs_path).pop() } // SR-IOV can be disabled on BIOS but the VFs netlink attribute will be there. In order to // understand if the SR-IOV can be configured we must look at PCI level. If sriov_numvfs is present // we can assume that the NIC is SR-IOV capable and it is enabled. pub(crate) fn sriov_is_enabled(pf_name: &str) -> bool { let sysfs_path = format!("/sys/class/net/{pf_name}/device/sriov_numvfs"); std::fs::File::open(sysfs_path).is_ok() } fn read_folder(folder_path: &str) -> Vec { let mut folder_contents = Vec::new(); let fd = match std::fs::read_dir(folder_path) { Ok(f) => f, Err(e) => { log::warn!("Failed to read dir {}: {}", folder_path, e); return folder_contents; } }; for entry in fd { let entry = match entry { Ok(e) => e, Err(e) => { log::warn!("Failed to read dir {}: {}", folder_path, e); continue; } }; let path = entry.path(); if let Ok(content) = path.strip_prefix(folder_path) { if let Some(content_str) = content.to_str() { folder_contents.push(content_str.to_string()); } } } folder_contents } // Fill the VfInfo base PF state pub(crate) fn sriov_vf_iface_tidy_up( iface_states: &mut HashMap, ) { let mut vf_info_dict: HashMap = HashMap::new(); for iface in iface_states.values() { if let Some(sriov_conf) = iface.sriov.as_ref() { for vf_info in sriov_conf.vfs.as_slice() { if let Some(vf_name) = vf_info.iface_name.as_ref() { vf_info_dict.insert(vf_name.to_string(), vf_info.clone()); } } } } for (vf_name, vf_info) in vf_info_dict.drain() { if let Some(vf_iface) = iface_states.get_mut(vf_name.as_str()) { vf_iface.sriov_vf = Some(vf_info); } } } nispor-1.2.19/query/tun.rs000064400000000000000000000066121046102023000135560ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use netlink_packet_route::link::InfoData; use netlink_packet_utils::nla::Nla; use serde::{Deserialize, Serialize}; use crate::{ netlink::{parse_as_u32, parse_as_u8}, NisporError, }; const IFF_TUN: u8 = 1; const IFF_TAP: u8 = 2; const IFLA_TUN_OWNER: u16 = 1; const IFLA_TUN_GROUP: u16 = 2; const IFLA_TUN_TYPE: u16 = 3; const IFLA_TUN_PI: u16 = 4; const IFLA_TUN_VNET_HDR: u16 = 5; const IFLA_TUN_PERSIST: u16 = 6; const IFLA_TUN_MULTI_QUEUE: u16 = 7; const IFLA_TUN_NUM_QUEUES: u16 = 8; const IFLA_TUN_NUM_DISABLED_QUEUES: u16 = 9; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct TunInfo { pub mode: TunMode, #[serde(skip_serializing_if = "Option::is_none")] pub owner: Option, #[serde(skip_serializing_if = "Option::is_none")] pub group: Option, pub pi: bool, pub vnet_hdr: bool, pub multi_queue: bool, pub persist: bool, #[serde(skip_serializing_if = "Option::is_none")] pub num_queues: Option, #[serde(skip_serializing_if = "Option::is_none")] pub num_disabled_queues: Option, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum TunMode { Tun, Tap, Unknown, } impl Default for TunMode { fn default() -> Self { TunMode::Unknown } } impl From for TunMode { fn from(d: u8) -> Self { match d { IFF_TUN => TunMode::Tun, IFF_TAP => TunMode::Tap, _ => { log::warn!("Unhandled TUN mode {}", d); TunMode::Unknown } } } } pub(crate) fn get_tun_info(data: &InfoData) -> Result { let mut tun_info = TunInfo::default(); if let InfoData::Tun(nlas) = data { for nla in nlas { let mut payload = vec![0; nla.value_len()]; nla.emit_value(&mut payload); let payload = payload.as_slice(); match nla.kind() { IFLA_TUN_OWNER => { tun_info.owner = Some(parse_as_u32(payload)?); } IFLA_TUN_GROUP => { tun_info.group = Some(parse_as_u32(payload)?); } IFLA_TUN_TYPE => { tun_info.mode = parse_as_u8(payload)?.into(); } IFLA_TUN_PI => { tun_info.pi = parse_as_u8(payload)? > 0; } IFLA_TUN_VNET_HDR => { tun_info.vnet_hdr = parse_as_u8(payload)? > 0; } IFLA_TUN_PERSIST => { tun_info.persist = parse_as_u8(payload)? > 0; } IFLA_TUN_MULTI_QUEUE => { tun_info.multi_queue = parse_as_u8(payload)? > 0; } IFLA_TUN_NUM_QUEUES => { tun_info.num_queues = Some(parse_as_u32(payload)?); } IFLA_TUN_NUM_DISABLED_QUEUES => { tun_info.num_disabled_queues = Some(parse_as_u32(payload)?); } _ => { log::warn!( "Unhandled TUN NLA {} {:?}", nla.kind(), payload ); } } } } Ok(tun_info) } nispor-1.2.19/query/veth.rs000064400000000000000000000023401046102023000137100ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::{Iface, IfaceType}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VethInfo { // Interface name of peer. // Use interface index number when peer interface is in other namespace. pub peer: String, } pub(crate) fn veth_iface_tidy_up(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Veth { continue; } // If the link_netnsid is set, the veth peer is on a different netns // and therefore Nispor should use the ifindex instead. if iface.link_netnsid.is_some() { continue; } if let Some(VethInfo { peer }) = &iface.veth { if let Some(peer_iface_name) = index_to_name.get(peer) { iface.veth = Some(VethInfo { peer: peer_iface_name.clone(), }) } } } } nispor-1.2.19/query/vlan.rs000064400000000000000000000064471046102023000137160ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{self, InfoData, InfoVlan}; use serde::{Deserialize, Serialize}; use crate::{Iface, IfaceType}; #[derive( Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default, )] #[serde(rename_all = "snake_case")] #[non_exhaustive] pub enum VlanProtocol { #[serde(rename = "802.1q")] #[default] Ieee8021Q, #[serde(rename = "802.1ad")] Ieee8021AD, Unknown, } impl From for VlanProtocol { fn from(d: link::VlanProtocol) -> Self { match d { link::VlanProtocol::Ieee8021Q => Self::Ieee8021Q, link::VlanProtocol::Ieee8021Ad => Self::Ieee8021AD, _ => Self::Unknown, } } } const VLAN_FLAG_REORDER_HDR: u32 = 0x1; const VLAN_FLAG_GVRP: u32 = 0x2; const VLAN_FLAG_LOOSE_BINDING: u32 = 0x4; const VLAN_FLAG_MVRP: u32 = 0x8; const VLAN_FLAG_BRIDGE_BINDING: u32 = 0x10; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VlanInfo { pub vlan_id: u16, pub protocol: VlanProtocol, pub base_iface: String, pub is_reorder_hdr: bool, pub is_gvrp: bool, pub is_loose_binding: bool, pub is_mvrp: bool, pub is_bridge_binding: bool, } pub(crate) fn get_vlan_info(data: &InfoData) -> Option { if let InfoData::Vlan(infos) = data { let mut vlan_info = VlanInfo::default(); for info in infos { if let InfoVlan::Id(d) = info { vlan_info.vlan_id = *d; } else if let InfoVlan::Protocol(d) = info { vlan_info.protocol = (*d).into(); } else if let InfoVlan::Flags((flags, _)) = info { // The kernel always set the mask as u32::MAX if *flags & VLAN_FLAG_REORDER_HDR > 0 { vlan_info.is_reorder_hdr = true } if *flags & VLAN_FLAG_GVRP > 0 { vlan_info.is_gvrp = true } if *flags & VLAN_FLAG_LOOSE_BINDING > 0 { vlan_info.is_loose_binding = true } if *flags & VLAN_FLAG_MVRP > 0 { vlan_info.is_mvrp = true } if *flags & VLAN_FLAG_BRIDGE_BINDING > 0 { vlan_info.is_bridge_binding = true } } else { log::debug!("Unknown VLAN info: {:?}", info); } } Some(vlan_info) } else { None } } pub(crate) fn vlan_iface_tidy_up(iface_states: &mut HashMap) { convert_base_iface_index_to_name(iface_states); } fn convert_base_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Vlan { continue; } if let Some(ref mut vlan_info) = iface.vlan { if let Some(base_iface_name) = index_to_name.get(&vlan_info.base_iface) { vlan_info.base_iface.clone_from(base_iface_name); } } } } nispor-1.2.19/query/vrf.rs000064400000000000000000000052461046102023000135470ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoVrf}; use netlink_packet_utils::nla::NlaBuffer; use serde::{Deserialize, Serialize}; use crate::{netlink::parse_as_u32, ControllerType, Iface, NisporError}; const IFLA_VRF_PORT_TABLE: u16 = 1; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VrfInfo { pub table_id: u32, pub subordinates: Vec, } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VrfSubordinateInfo { pub table_id: u32, } pub(crate) fn get_vrf_info(data: &InfoData) -> Option { if let InfoData::Vrf(infos) = data { let mut vrf_info = VrfInfo::default(); for info in infos { if let InfoVrf::TableId(d) = *info { vrf_info.table_id = d; } else { log::debug!("Unknown VRF info {:?}", info) } } Some(vrf_info) } else { None } } pub(crate) fn get_vrf_subordinate_info( data: &[u8], ) -> Result, NisporError> { let nla_buff = NlaBuffer::new(data); if nla_buff.kind() == IFLA_VRF_PORT_TABLE { Ok(Some(VrfSubordinateInfo { table_id: parse_as_u32(nla_buff.value())?, })) } else { Ok(None) } } pub(crate) fn vrf_iface_tidy_up(iface_states: &mut HashMap) { gen_subordinate_list_of_controller(iface_states); } fn gen_subordinate_list_of_controller( iface_states: &mut HashMap, ) { let mut controller_subordinates: HashMap> = HashMap::new(); for iface in iface_states.values() { if iface.controller_type == Some(ControllerType::Vrf) { if let Some(controller) = &iface.controller { match controller_subordinates.get_mut(controller) { Some(subordinates) => subordinates.push(iface.name.clone()), None => { let new_subordinates: Vec = vec![iface.name.clone()]; controller_subordinates .insert(controller.clone(), new_subordinates); } }; } } } for (controller, subordinates) in controller_subordinates.iter_mut() { if let Some(controller_iface) = iface_states.get_mut(controller) { if let Some(ref mut vrf_info) = controller_iface.vrf { subordinates.sort(); vrf_info.subordinates.clone_from(subordinates); } } } } nispor-1.2.19/query/vxlan.rs000064400000000000000000000121231046102023000140720ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoVxlan}; use serde::{Deserialize, Serialize}; use crate::{ netlink::{parse_as_ipv4, parse_as_ipv6}, Iface, IfaceType, NisporError, }; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct VxlanInfo { pub remote: String, pub vxlan_id: u32, pub base_iface: String, pub local: String, pub ttl: u8, pub tos: u8, pub learning: bool, pub ageing: u32, pub max_address: u32, pub src_port_min: u16, pub src_port_max: u16, pub proxy: bool, pub rsc: bool, pub l2miss: bool, pub l3miss: bool, pub dst_port: u16, pub udp_check_sum: bool, pub udp6_zero_check_sum_tx: bool, pub udp6_zero_check_sum_rx: bool, pub remote_check_sum_tx: bool, pub remote_check_sum_rx: bool, pub gbp: bool, pub remote_check_sum_no_partial: bool, pub collect_metadata: bool, pub label: u32, pub gpe: bool, pub ttl_inherit: bool, pub df: u8, } pub(crate) fn get_vxlan_info( data: &InfoData, ) -> Result, NisporError> { if let InfoData::Vxlan(infos) = data { let mut vxlan_info = VxlanInfo::default(); for info in infos { if let InfoVxlan::Id(d) = *info { vxlan_info.vxlan_id = d; } else if let InfoVxlan::Group(d) = info { vxlan_info.remote = parse_as_ipv4(d)?.to_string(); } else if let InfoVxlan::Group6(d) = info { vxlan_info.remote = parse_as_ipv6(d)?.to_string(); } else if let InfoVxlan::Link(d) = *info { vxlan_info.base_iface = format!("{d}"); } else if let InfoVxlan::Local(d) = info { vxlan_info.local = parse_as_ipv4(d)?.to_string(); } else if let InfoVxlan::Local6(d) = info { vxlan_info.local = parse_as_ipv6(d)?.to_string(); } else if let InfoVxlan::Tos(d) = *info { vxlan_info.tos = d; } else if let InfoVxlan::Ttl(d) = *info { vxlan_info.ttl = d; } else if let InfoVxlan::Learning(d) = *info { vxlan_info.learning = d; } else if let InfoVxlan::Label(d) = *info { vxlan_info.label = d; } else if let InfoVxlan::Ageing(d) = *info { vxlan_info.ageing = d; } else if let InfoVxlan::Limit(d) = *info { vxlan_info.max_address = d; } else if let InfoVxlan::PortRange(d) = *info { vxlan_info.src_port_min = d.0; vxlan_info.src_port_max = d.1; } else if let InfoVxlan::Proxy(d) = *info { vxlan_info.proxy = d; } else if let InfoVxlan::Rsc(d) = *info { vxlan_info.rsc = d; } else if let InfoVxlan::L2Miss(d) = *info { vxlan_info.l2miss = d; } else if let InfoVxlan::L3Miss(d) = *info { vxlan_info.l3miss = d; } else if let InfoVxlan::Port(d) = *info { vxlan_info.dst_port = d; } else if let InfoVxlan::UDPCsum(d) = *info { vxlan_info.udp_check_sum = d; } else if let InfoVxlan::UDPZeroCsumTX(d) = *info { vxlan_info.udp6_zero_check_sum_tx = d; } else if let InfoVxlan::UDPZeroCsumRX(d) = *info { vxlan_info.udp6_zero_check_sum_rx = d; } else if let InfoVxlan::RemCsumTX(d) = *info { vxlan_info.remote_check_sum_tx = d; } else if let InfoVxlan::RemCsumRX(d) = *info { vxlan_info.remote_check_sum_rx = d; } else if let InfoVxlan::Gpe(d) = *info { vxlan_info.gpe = d; } else if let InfoVxlan::Gbp(d) = *info { vxlan_info.gbp = d; } else if let InfoVxlan::TtlInherit(d) = *info { vxlan_info.ttl_inherit = d; } else if let InfoVxlan::CollectMetadata(d) = *info { vxlan_info.collect_metadata = d; } else if let InfoVxlan::Df(d) = *info { vxlan_info.df = d; } else { log::debug!("Unknown VXLAN info {:?}", info) } } Ok(Some(vxlan_info)) } else { Ok(None) } } pub(crate) fn vxlan_iface_tidy_up(iface_states: &mut HashMap) { convert_base_iface_index_to_name(iface_states); } fn convert_base_iface_index_to_name(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(format!("{}", iface.index), iface.name.clone()); } for iface in iface_states.values_mut() { if iface.iface_type != IfaceType::Vxlan { continue; } if let Some(ref mut vxlan_info) = iface.vxlan { if let Some(base_iface_name) = index_to_name.get(&vxlan_info.base_iface) { vxlan_info.base_iface.clone_from(base_iface_name); } } } } nispor-1.2.19/query/xfrm.rs000064400000000000000000000032161046102023000137210ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use netlink_packet_route::link::{InfoData, InfoXfrm}; use serde::{Deserialize, Serialize}; use crate::{Iface, IfaceType}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] #[non_exhaustive] pub struct XfrmInfo { pub base_iface: String, pub iface_id: u32, } pub(crate) fn get_xfrm_info(data: &InfoData) -> Option { if let InfoData::Xfrm(infos) = data { let mut xfrm_info = XfrmInfo::default(); for info in infos { match *info { InfoXfrm::Link(i) => { xfrm_info.base_iface = i.to_string(); } InfoXfrm::IfId(d) => { xfrm_info.iface_id = d; } _ => { log::debug!("Unknown XFRM info {:?}", info); } } } Some(xfrm_info) } else { None } } pub(crate) fn xfrm_iface_tidy_up(iface_states: &mut HashMap) { fill_port_iface_names(iface_states); } fn fill_port_iface_names(iface_states: &mut HashMap) { let mut index_to_name = HashMap::new(); for iface in iface_states.values() { index_to_name.insert(iface.index.to_string(), iface.name.clone()); } for iface in iface_states .values_mut() .filter(|i| i.iface_type == IfaceType::Xfrm) { if let Some(xfrm_info) = iface.xfrm.as_mut() { if let Some(base_iface) = index_to_name.get(&xfrm_info.base_iface) { xfrm_info.base_iface = base_iface.to_string(); } } } }