diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..5608611 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +runner = 'sudo' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4c002a..52448e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,8 +22,16 @@ jobs: - name: Install necessary dependencies run: | sudo apt update - sudo apt install "linux-modules-extra-$(uname -r)" + sudo apt install -y "linux-modules-extra-$(uname -r)" sudo modprobe vrf + - name: Install iproute2 lastest git build + run: | + sudo apt-get -y remove iproute2 + git clone https://git.kernel.org/pub/scm/network/iproute2/iproute2.git + cd iproute2 + ./configure --prefix=/usr + make -j5 + sudo make install - name: Install Rust Stable run: | diff --git a/Cargo.toml b/Cargo.toml index 0c28be8..951a771 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ tokio = { version = "1.30", features = ["rt", "net", "time", "macros"] } [dev-dependencies] pretty_assertions = "1.4.1" -ctor = "0.5.0" [patch.crates-io.rtnetlink] git = "https://github.com/rust-netlink/rtnetlink" diff --git a/Makefile b/Makefile index 5482d48..cc5d8f2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ check: cargo build; env CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="sudo" \ - cargo test -- --test-threads=1 --show-output $(WHAT) ; + cargo test -- --show-output; diff --git a/src/color.rs b/src/color.rs index 862f330..27a969c 100644 --- a/src/color.rs +++ b/src/color.rs @@ -82,7 +82,8 @@ impl CliColor { false } } else { - false + // iproute assume dark color if unknown + true } }) } diff --git a/src/ip/link/ifaces/bridge.rs b/src/ip/link/ifaces/bridge.rs index b793f99..9414740 100644 --- a/src/ip/link/ifaces/bridge.rs +++ b/src/ip/link/ifaces/bridge.rs @@ -2,17 +2,11 @@ use iproute_rs::mac_to_string; use rtnetlink::packet_route::link::{ - BridgePortState, InfoBridge, InfoBridgePort, VlanProtocol, + BridgeBooleanOptionFlags as BoolOptFlags, BridgePortState, InfoBridge, + InfoBridgePort, VlanProtocol, }; use serde::Serialize; -// Additional bridge constants not yet in netlink-packet-route -const IFLA_BR_FDB_N_LEARNED: u16 = 48; -const IFLA_BR_FDB_MAX_LEARNED: u16 = 49; -const IFLA_BR_NO_LL_LEARN: u16 = 51; -const IFLA_BR_VLAN_MCAST_SNOOPING: u16 = 52; -const IFLA_BR_MST_ENABLED: u16 = 53; - #[derive(Serialize)] pub(crate) struct CliLinkInfoDataBridge { forward_delay: u32, @@ -48,9 +42,16 @@ pub(crate) struct CliLinkInfoDataBridge { #[serde(skip_serializing_if = "String::is_empty")] group_addr: String, mcast_snooping: u8, - no_linklocal_learn: u8, - mcast_vlan_snooping: u8, - mst_enabled: u8, + #[serde(skip_serializing_if = "Option::is_none")] + no_linklocal_learn: Option, + #[serde(skip_serializing_if = "Option::is_none")] + mcast_vlan_snooping: Option, + #[serde(skip_serializing_if = "Option::is_none")] + mst_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + mdb_offload_fail_notification: Option, + #[serde(skip_serializing_if = "Option::is_none")] + fdb_local_vlan_0: Option, mcast_router: u8, mcast_query_use_ifaddr: u8, mcast_querier: u8, @@ -124,9 +125,11 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge { let mut mcast_mld_version = None; let mut fdb_n_learned = None; let mut fdb_max_learned = None; - let mut no_linklocal_learn = 0; - let mut mcast_vlan_snooping = 0; - let mut mst_enabled = 0; + let mut no_linklocal_learn = None; + let mut mcast_vlan_snooping = None; + let mut mst_enabled = None; + let mut mdb_offload_fail_notification = None; + let mut fdb_local_vlan_0 = None; for nla in info { match nla { @@ -223,35 +226,41 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge { InfoBridge::MulticastMldVersion(v) => { mcast_mld_version = Some(*v) } - InfoBridge::Other(nla) => { - use rtnetlink::packet_core::Nla; - match nla.kind() { - IFLA_BR_FDB_N_LEARNED => { - let mut val = [0u8; 4]; - nla.emit_value(&mut val); - fdb_n_learned = Some(u32::from_ne_bytes(val)); - } - IFLA_BR_FDB_MAX_LEARNED => { - let mut val = [0u8; 4]; - nla.emit_value(&mut val); - fdb_max_learned = Some(u32::from_ne_bytes(val)); - } - IFLA_BR_NO_LL_LEARN => { - let mut val = [0u8; 1]; - nla.emit_value(&mut val); - no_linklocal_learn = val[0]; - } - IFLA_BR_VLAN_MCAST_SNOOPING => { - let mut val = [0u8; 1]; - nla.emit_value(&mut val); - mcast_vlan_snooping = val[0]; - } - IFLA_BR_MST_ENABLED => { - let mut val = [0u8; 1]; - nla.emit_value(&mut val); - mst_enabled = val[0]; - } - _ => (), + InfoBridge::FdbNLearned(v) => fdb_n_learned = Some(*v), + InfoBridge::FdbMaxLearned(v) => fdb_max_learned = Some(*v), + InfoBridge::MultiBoolOpt(opts) => { + if opts.mask.contains(BoolOptFlags::NoLinkLocalLearn) { + no_linklocal_learn = Some( + opts.value + .contains(BoolOptFlags::NoLinkLocalLearn) + .into(), + ); + } + if opts.mask.contains(BoolOptFlags::VlanMulticastSnooping) { + mcast_vlan_snooping = Some( + opts.value + .contains(BoolOptFlags::VlanMulticastSnooping) + .into(), + ); + } + if opts.mask.contains(BoolOptFlags::MstEnable) { + mst_enabled = Some( + opts.value.contains(BoolOptFlags::MstEnable).into(), + ); + } + if opts.mask.contains(BoolOptFlags::MdbOffloadFailNotif) { + mdb_offload_fail_notification = Some( + opts.value + .contains(BoolOptFlags::MdbOffloadFailNotif) + .into(), + ); + } + if opts.mask.contains(BoolOptFlags::FdbLocalVlan0) { + fdb_local_vlan_0 = Some( + opts.value + .contains(BoolOptFlags::FdbLocalVlan0) + .into(), + ); } } _ => (), @@ -290,6 +299,8 @@ impl From<&[InfoBridge]> for CliLinkInfoDataBridge { no_linklocal_learn, mcast_vlan_snooping, mst_enabled, + mdb_offload_fail_notification, + fdb_local_vlan_0, mcast_router, mcast_query_use_ifaddr, mcast_querier, @@ -368,9 +379,21 @@ impl std::fmt::Display for CliLinkInfoDataBridge { write!(f, "group_address {} ", self.group_addr)?; } write!(f, "mcast_snooping {} ", self.mcast_snooping)?; - write!(f, "no_linklocal_learn {} ", self.no_linklocal_learn)?; - write!(f, "mcast_vlan_snooping {} ", self.mcast_vlan_snooping)?; - write!(f, "mst_enabled {} ", self.mst_enabled)?; + if let Some(v) = self.no_linklocal_learn { + write!(f, "no_linklocal_learn {v} ")?; + } + if let Some(v) = self.mcast_vlan_snooping { + write!(f, "mcast_vlan_snooping {v} ")?; + } + if let Some(v) = self.mst_enabled { + write!(f, "mst_enabled {v} ")?; + } + if let Some(v) = self.mdb_offload_fail_notification { + write!(f, "mdb_offload_fail_notification {v} ")?; + } + if let Some(v) = self.fdb_local_vlan_0 { + write!(f, "fdb_local_vlan_0 {v} ")?; + } write!(f, "mcast_router {} ", self.mcast_router)?; write!(f, "mcast_query_use_ifaddr {} ", self.mcast_query_use_ifaddr)?; write!(f, "mcast_querier {} ", self.mcast_querier)?; diff --git a/src/ip/link/show.rs b/src/ip/link/show.rs index be029c0..22163ec 100644 --- a/src/ip/link/show.rs +++ b/src/ip/link/show.rs @@ -134,11 +134,7 @@ pub(crate) async fn handle_show( tokio::spawn(connection); - let mut link_get_handle = handle.link().get(); - - if let Some(iface_name) = opts.first() { - link_get_handle = link_get_handle.match_name(iface_name.to_string()); - } + let link_get_handle = handle.link().get(); let mut links = link_get_handle.execute(); let mut ifaces: Vec = Vec::new(); @@ -150,6 +146,13 @@ pub(crate) async fn handle_show( resolve_controller_and_link_names(&mut ifaces); resolve_netns_names(&mut ifaces).await?; + // In order to resolved interface index to interface name and netns name, + // we cannot use kernel side interface filter, but need to dump everything, + // then filter here + if let Some(iface_name) = opts.first() { + ifaces.retain(|i| i.ifname.as_str() == *iface_name); + } + Ok(ifaces) } diff --git a/src/ip/link/tests/link.rs b/src/ip/link/tests/bridge.rs similarity index 51% rename from src/ip/link/tests/link.rs rename to src/ip/link/tests/bridge.rs index 4642186..34b82f3 100644 --- a/src/ip/link/tests/link.rs +++ b/src/ip/link/tests/bridge.rs @@ -1,15 +1,6 @@ // SPDX-License-Identifier: MIT -use crate::tests::{exec_cmd, get_ip_cli_path}; - -const TEST_NETNS: &str = "iproute-rs-test"; - -/// Execute a command inside the test network namespace -fn exec_in_netns(args: &[&str]) -> String { - let mut full_args = vec!["ip", "netns", "exec", TEST_NETNS]; - full_args.extend_from_slice(args); - exec_cmd(&full_args) -} +use crate::tests::{exec_cmd, ip_rs_exec_cmd}; /// Normalize timer values in output to avoid test flakiness /// Timer values can vary slightly between consecutive calls due to kernel @@ -104,98 +95,108 @@ fn normalize_timers_json(output: &str) -> String { result } -#[cfg(test)] -#[ctor::ctor] -fn setup() { - println!("setup network namespace and interfaces for tests"); - - // Create network namespace (delete first if it exists) - let netns_list = exec_cmd(&["ip", "netns", "list"]); - if netns_list.contains(TEST_NETNS) { - exec_cmd(&["ip", "netns", "del", TEST_NETNS]); - } - exec_cmd(&["ip", "netns", "add", TEST_NETNS]); - - // Add vlan over dummy interface - exec_in_netns(&["ip", "link", "add", "dummy0", "type", "dummy"]); - exec_in_netns(&[ - "ip", - "link", - "property", - "add", - "dev", - "dummy0", - "altname", - "dmmy-zero", - ]); - exec_in_netns(&["ip", "link", "add", "br0", "type", "bridge"]); - exec_in_netns(&[ - "ip", "link", "add", "link", "dummy0", "name", "dummy0.1", "type", - "vlan", "id", "1", - ]); - exec_in_netns(&["ip", "link", "set", "dev", "dummy0.1", "master", "br0"]); +#[test] +fn test_link_detailed_show_bridge() { + let br_name = "test-br0"; + let dummy_name = "test-dummy0"; - exec_in_netns(&["ip", "link", "set", "dummy0", "up"]); - exec_in_netns(&["ip", "link", "set", "dummy0.1", "up"]); - exec_in_netns(&["ip", "link", "set", "br0", "up"]); -} + with_bridge_iface(br_name, dummy_name, || { + let expected_output = exec_cmd(&["ip", "-d", "link", "show", br_name]); -#[cfg(test)] -#[ctor::dtor] -fn teardown() { - println!("teardown network namespace for tests"); + let our_output = ip_rs_exec_cmd(&["-d", "link", "show", br_name]); - // Delete network namespace - exec_cmd(&["ip", "netns", "del", TEST_NETNS]); + pretty_assertions::assert_eq!( + normalize_timers(&expected_output), + normalize_timers(&our_output) + ); + }) } #[test] -fn test_link_show() { - let cli_path = get_ip_cli_path(); - - let expected_output = exec_in_netns(&["ip", "link", "show"]); - - let our_output = exec_in_netns(&[cli_path.as_str(), "link", "show"]); - - pretty_assertions::assert_eq!(expected_output, our_output); +fn test_link_detailed_show_json_bridge() { + let br_name = "test-br1"; + let dummy_name = "test-dummy1"; + with_bridge_iface(br_name, dummy_name, || { + let expected_output = + exec_cmd(&["ip", "-d", "-j", "link", "show", br_name]); + + let our_output = ip_rs_exec_cmd(&["-d", "-j", "link", "show", br_name]); + + pretty_assertions::assert_eq!( + normalize_timers_json(&expected_output), + normalize_timers_json(&our_output) + ); + }) } #[test] -fn test_link_detailed_show() { - let cli_path = get_ip_cli_path(); +fn test_link_detailed_show_bridge_port() { + let br_name = "test-br2"; + let dummy_name = "test-dummy2"; - let expected_output = exec_in_netns(&["ip", "-d", "link", "show"]); + with_bridge_iface(br_name, dummy_name, || { + let expected_output = + exec_cmd(&["ip", "-d", "link", "show", dummy_name]); - let our_output = exec_in_netns(&[cli_path.as_str(), "-d", "link", "show"]); + let our_output = ip_rs_exec_cmd(&["-d", "link", "show", dummy_name]); - pretty_assertions::assert_eq!( - normalize_timers(&expected_output), - normalize_timers(&our_output) - ); + pretty_assertions::assert_eq!( + normalize_timers(&expected_output), + normalize_timers(&our_output) + ); + }) } #[test] -fn test_link_show_json() { - let cli_path = get_ip_cli_path(); - - let expected_output = exec_in_netns(&["ip", "-j", "link", "show"]); - - let our_output = exec_in_netns(&[cli_path.as_str(), "-j", "link", "show"]); - - pretty_assertions::assert_eq!(expected_output, our_output); +fn test_link_detailed_show_json_bridge_port() { + let br_name = "test-br3"; + let dummy_name = "test-dummy3"; + with_bridge_iface(br_name, dummy_name, || { + let expected_output = + exec_cmd(&["ip", "-d", "-j", "link", "show", dummy_name]); + + let our_output = + ip_rs_exec_cmd(&["-d", "-j", "link", "show", dummy_name]); + + pretty_assertions::assert_eq!( + normalize_timers_json(&expected_output), + normalize_timers_json(&our_output) + ); + }) } -#[test] -fn test_link_detailed_show_json() { - let cli_path = get_ip_cli_path(); +/// Since all test cases are running simultaneously, please make sure `br_name` +/// and `dummy_name` are unique among tests. +fn with_bridge_iface(br_name: &str, dummy_name: &str, test: T) +where + T: FnOnce() + std::panic::UnwindSafe, +{ + // create bridge using dummy interface + exec_cmd(&["ip", "link", "add", dummy_name, "type", "dummy"]); + exec_cmd(&[ + "ip", + "link", + "add", + br_name, + "type", + "bridge", + "stp_state", + "0", + ]); + exec_cmd(&["ip", "link", "set", "dev", dummy_name, "master", br_name]); + + exec_cmd(&["ip", "link", "set", dummy_name, "up"]); + exec_cmd(&["ip", "link", "set", br_name, "up"]); - let expected_output = exec_in_netns(&["ip", "-d", "-j", "link", "show"]); + // Wait 1 second for bridge ID to be stable + std::thread::sleep(std::time::Duration::from_secs(1)); - let our_output = - exec_in_netns(&[cli_path.as_str(), "-d", "-j", "link", "show"]); + let result = std::panic::catch_unwind(|| { + test(); + }); - pretty_assertions::assert_eq!( - normalize_timers_json(&expected_output), - normalize_timers_json(&our_output) - ); + // clean up + exec_cmd(&["ip", "link", "del", dummy_name]); + exec_cmd(&["ip", "link", "del", br_name]); + assert!(result.is_ok()) } diff --git a/src/ip/link/tests/color.rs b/src/ip/link/tests/color.rs index 0b104e8..0e1a1e4 100644 --- a/src/ip/link/tests/color.rs +++ b/src/ip/link/tests/color.rs @@ -1,17 +1,14 @@ // SPDX-License-Identifier: MIT -use crate::tests::{exec_cmd, get_ip_cli_path}; +use crate::tests::{exec_cmd, ip_rs_exec_cmd}; const COLOR_CLEAR: &str = "\x1b[0m"; #[test] fn test_ip_link_show_color_always() { - let cli_path = get_ip_cli_path(); + let expected_output = exec_cmd(&["ip", "-c=always", "link", "show", "lo"]); - let expected_output = exec_cmd(&["ip", "-c=always", "link", "show"]); - - let our_output = - exec_cmd(&[cli_path.as_str(), "-c=always", "link", "show"]); + let our_output = ip_rs_exec_cmd(&["-c=always", "link", "show", "lo"]); assert!(our_output.contains(COLOR_CLEAR)); @@ -20,11 +17,9 @@ fn test_ip_link_show_color_always() { #[test] fn test_ip_link_show_color_auto_without_terminal() { - let cli_path = get_ip_cli_path(); - - let expected_output = exec_cmd(&["ip", "-c=auto", "link", "show"]); + let expected_output = exec_cmd(&["ip", "-c=auto", "link", "show", "lo"]); - let our_output = exec_cmd(&[cli_path.as_str(), "-c=auto", "link", "show"]); + let our_output = ip_rs_exec_cmd(&["-c=auto", "link", "show", "lo"]); assert!(!our_output.contains(COLOR_CLEAR)); @@ -33,11 +28,9 @@ fn test_ip_link_show_color_auto_without_terminal() { #[test] fn test_ip_link_show_color_never() { - let cli_path = get_ip_cli_path(); - - let expected_output = exec_cmd(&["ip", "-c=never", "link", "show"]); + let expected_output = exec_cmd(&["ip", "-c=never", "link", "show", "lo"]); - let our_output = exec_cmd(&[cli_path.as_str(), "-c=never", "link", "show"]); + let our_output = ip_rs_exec_cmd(&["-c=never", "link", "show", "lo"]); assert!(!our_output.contains(COLOR_CLEAR)); diff --git a/src/ip/link/tests/loopback.rs b/src/ip/link/tests/loopback.rs new file mode 100644 index 0000000..a873df0 --- /dev/null +++ b/src/ip/link/tests/loopback.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +use crate::tests::{exec_cmd, ip_rs_exec_cmd}; + +#[test] +fn test_link_show_lo() { + let expected_output = exec_cmd(&["ip", "link", "show", "lo"]); + + let our_output = ip_rs_exec_cmd(&["link", "show", "lo"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +} + +#[test] +fn test_link_show_lo_json() { + let expected_output = exec_cmd(&["ip", "-j", "link", "show", "lo"]); + + let our_output = ip_rs_exec_cmd(&["-j", "link", "show", "lo"]); + + pretty_assertions::assert_eq!(expected_output, our_output); +} diff --git a/src/ip/link/tests/mod.rs b/src/ip/link/tests/mod.rs index 700ff08..b161c21 100644 --- a/src/ip/link/tests/mod.rs +++ b/src/ip/link/tests/mod.rs @@ -1,7 +1,5 @@ // SPDX-License-Identifier: MIT -#[cfg(test)] +mod bridge; mod color; - -#[cfg(test)] -mod link; +mod loopback; diff --git a/src/ip/tests/cmd.rs b/src/ip/tests/cmd.rs index 57a2031..7632fdf 100644 --- a/src/ip/tests/cmd.rs +++ b/src/ip/tests/cmd.rs @@ -4,9 +4,7 @@ pub(crate) fn exec_cmd(args: &[&str]) -> String { let output = std::process::Command::new(args[0]) .args(&args[1..]) .output() - .unwrap_or_else(|e| { - panic!("failed to execute file command {args:?}: {e}") - }); + .unwrap_or_else(|e| panic!("failed to execute command {args:?}: {e}")); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -14,5 +12,30 @@ pub(crate) fn exec_cmd(args: &[&str]) -> String { } String::from_utf8(output.stdout) - .expect("Failed to convert file command output to String") + .expect("Failed to convert command output to String") +} + +pub(crate) fn ip_rs_exec_cmd(args: &[&str]) -> String { + let mut cur_exec_path = + std::env::current_exe().expect("No current exec path"); + + cur_exec_path.pop(); + cur_exec_path.pop(); + + let output = std::process::Command::new( + cur_exec_path.join("ip").to_str().expect("Not UTF-8 string"), + ) + .args(args) + .output() + .unwrap_or_else(|e| { + panic!("failed to execute ip-rs command {args:?}: {e}") + }); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("Command failed: {args:?}\nstderr: {stderr}"); + } + + String::from_utf8(output.stdout) + .expect("Failed to convert command output to String") } diff --git a/src/ip/tests/mod.rs b/src/ip/tests/mod.rs index 2cd1253..7f6035d 100644 --- a/src/ip/tests/mod.rs +++ b/src/ip/tests/mod.rs @@ -1,9 +1,5 @@ // SPDX-License-Identifier: MIT mod cmd; -mod path; -#[cfg(test)] -pub(crate) use self::cmd::exec_cmd; -#[cfg(test)] -pub(crate) use self::path::get_ip_cli_path; +pub(crate) use self::cmd::{exec_cmd, ip_rs_exec_cmd}; diff --git a/src/ip/tests/path.rs b/src/ip/tests/path.rs deleted file mode 100644 index 819ce14..0000000 --- a/src/ip/tests/path.rs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT - -pub(crate) fn get_ip_cli_path() -> String { - let mut cur_exec_path = - std::env::current_exe().expect("No current exec path"); - - cur_exec_path.pop(); - cur_exec_path.pop(); - - cur_exec_path - .join("ip") - .to_str() - .expect("Not UTF-8 string") - .to_string() -}