From d477ee726d8ad85bcb938079553c347ca973c8d6 Mon Sep 17 00:00:00 2001 From: moz-sec Date: Mon, 11 Nov 2024 07:24:44 +0000 Subject: [PATCH 1/4] test: add relative_network_cgroups test Signed-off-by: moz-sec --- tests/contest/contest/src/main.rs | 2 + .../contest/contest/src/tests/cgroups/mod.rs | 1 + .../src/tests/cgroups/relative_network.rs | 94 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 tests/contest/contest/src/tests/cgroups/relative_network.rs diff --git a/tests/contest/contest/src/main.rs b/tests/contest/contest/src/main.rs index e0d3a4a02..d7344e64c 100644 --- a/tests/contest/contest/src/main.rs +++ b/tests/contest/contest/src/main.rs @@ -103,6 +103,7 @@ fn main() -> Result<()> { let cgroup_v1_memory = cgroups::memory::get_test_group(); let cgroup_v1_network = cgroups::network::get_test_group(); let cgroup_v1_blkio = cgroups::blkio::get_test_group(); + let cgroup_v1_relative_network = cgroups::relative_network::get_test_group(); let seccomp = get_seccomp_test(); let seccomp_notify = get_seccomp_notify_test(); let ro_paths = get_ro_paths_test(); @@ -128,6 +129,7 @@ fn main() -> Result<()> { tm.add_test_group(Box::new(cgroup_v1_memory)); tm.add_test_group(Box::new(cgroup_v1_network)); tm.add_test_group(Box::new(cgroup_v1_blkio)); + tm.add_test_group(Box::new(cgroup_v1_relative_network)); tm.add_test_group(Box::new(seccomp)); tm.add_test_group(Box::new(seccomp_notify)); tm.add_test_group(Box::new(ro_paths)); diff --git a/tests/contest/contest/src/tests/cgroups/mod.rs b/tests/contest/contest/src/tests/cgroups/mod.rs index 51f467d5c..b8cea1d4c 100644 --- a/tests/contest/contest/src/tests/cgroups/mod.rs +++ b/tests/contest/contest/src/tests/cgroups/mod.rs @@ -9,6 +9,7 @@ pub mod cpu; pub mod memory; pub mod network; pub mod pids; +pub mod relative_network; pub fn cleanup_v1() -> Result<()> { for subsystem in list_subsystem_mount_points()? { diff --git a/tests/contest/contest/src/tests/cgroups/relative_network.rs b/tests/contest/contest/src/tests/cgroups/relative_network.rs new file mode 100644 index 000000000..202a4f9c7 --- /dev/null +++ b/tests/contest/contest/src/tests/cgroups/relative_network.rs @@ -0,0 +1,94 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use oci_spec::runtime::{ + LinuxBuilder, LinuxInterfacePriorityBuilder, LinuxNetworkBuilder, LinuxResourcesBuilder, Spec, + SpecBuilder, +}; +use pnet_datalink::interfaces; +use test_framework::{test_result, ConditionalTest, TestGroup, TestResult}; + +use crate::utils::test_outside_container; +use crate::utils::test_utils::check_container_created; + +fn create_spec(cgroup_name: &str, class_id: u32, prio: u32, if_name: &str) -> Result { + // Create the Linux Spec + let linux_spec = LinuxBuilder::default() + .cgroups_path(Path::new("testdir/runtime-test/container").join(cgroup_name)) + .resources( + LinuxResourcesBuilder::default() + .network( + LinuxNetworkBuilder::default() + .class_id(class_id) + .priorities(vec![LinuxInterfacePriorityBuilder::default() + .name(if_name) + .priority(prio) + .build() + .context("failed to build network interface priority spec")?]) + .build() + .context("failed to build network spec")?, + ) + .build() + .context("failed to build resource spec")?, + ) + .build() + .context("failed to build linux spec")?; + + // Create the top level Spec + let spec = SpecBuilder::default() + .linux(linux_spec) + .build() + .context("failed to build spec")?; + + Ok(spec) +} + +// Gets the loopback interface if it exists +fn get_loopback_interface() -> Option { + let interfaces = interfaces(); + let lo_if_name = interfaces.first().map(|iface| &iface.name)?; + + Some(lo_if_name.to_string()) +} + +fn test_relative_network_cgroups() -> TestResult { + let cgroup_name = "test_relative_network_cgroups"; + + let id = 255; + let prio = 10; + let if_name = "lo"; + let spec = test_result!(create_spec(cgroup_name, id, prio, if_name)); + + test_outside_container(spec, &|data| { + test_result!(check_container_created(&data)); + TestResult::Passed + }) +} + +fn can_run() -> bool { + // Ensure the expected network interfaces exist on the system running the test + let iface_exists = get_loopback_interface().is_some(); + + // This is kind of annoying, network controller can be at a number of mount points + let cgroup_paths_exists = (Path::new("/sys/fs/cgroup/net_cls/net_cls.classid").exists() + && Path::new("/sys/fs/cgroup/net_prio/net_prio.ifpriomap").exists()) + || (Path::new("/sys/fs/cgroup/net_cls,net_prio/net_cls.classid").exists() + && Path::new("/sys/fs/cgroup/net_cls,net_prio/net_prio.ifpriomap").exists()) + || (Path::new("/sys/fs/cgroup/net_prio,net_cls/net_cls.classid").exists() + && Path::new("/sys/fs/cgroup/net_prio,net_cl/net_prio.ifpriomap").exists()); + + iface_exists && cgroup_paths_exists +} + +pub fn get_test_group() -> TestGroup { + let mut test_group = TestGroup::new("cgroup_v1_relative_network"); + let linux_cgroups_network = ConditionalTest::new( + "test_linux_cgroups_relative_network", + Box::new(can_run), + Box::new(test_relative_network_cgroups), + ); + + test_group.add(vec![Box::new(linux_cgroups_network)]); + + test_group +} From 4255ed92eff27f988432dbc2cbadcf02ca2b6eca Mon Sep 17 00:00:00 2001 From: moz-sec Date: Mon, 25 Nov 2024 14:49:50 +0900 Subject: [PATCH 2/4] test: validate network cgroup resources Signed-off-by: moz-sec --- .../contest/src/tests/cgroups/relative_network.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/contest/contest/src/tests/cgroups/relative_network.rs b/tests/contest/contest/src/tests/cgroups/relative_network.rs index 202a4f9c7..7062e5901 100644 --- a/tests/contest/contest/src/tests/cgroups/relative_network.rs +++ b/tests/contest/contest/src/tests/cgroups/relative_network.rs @@ -59,10 +59,15 @@ fn test_relative_network_cgroups() -> TestResult { let if_name = "lo"; let spec = test_result!(create_spec(cgroup_name, id, prio, if_name)); - test_outside_container(spec, &|data| { + let test_result = test_outside_container(spec, &|data| { test_result!(check_container_created(&data)); TestResult::Passed - }) + }); + if let TestResult::Failed(_) = test_result { + return test_result; + } + + TestResult::Passed } fn can_run() -> bool { From 6280f34f9a5fd2beddaebcbc1a118ed959a032c1 Mon Sep 17 00:00:00 2001 From: moz-sec Date: Mon, 2 Dec 2024 12:10:26 +0900 Subject: [PATCH 3/4] test: add validate_network() to validate net_cls.classid and net_prio.ifpriomap Signed-off-by: moz-sec --- .../src/tests/cgroups/relative_network.rs | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/contest/contest/src/tests/cgroups/relative_network.rs b/tests/contest/contest/src/tests/cgroups/relative_network.rs index 7062e5901..c3e749f3b 100644 --- a/tests/contest/contest/src/tests/cgroups/relative_network.rs +++ b/tests/contest/contest/src/tests/cgroups/relative_network.rs @@ -1,6 +1,7 @@ -use std::path::Path; +use std::fs; +use std::path::{Path, PathBuf}; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use oci_spec::runtime::{ LinuxBuilder, LinuxInterfacePriorityBuilder, LinuxNetworkBuilder, LinuxResourcesBuilder, Spec, SpecBuilder, @@ -9,7 +10,7 @@ use pnet_datalink::interfaces; use test_framework::{test_result, ConditionalTest, TestGroup, TestResult}; use crate::utils::test_outside_container; -use crate::utils::test_utils::check_container_created; +use crate::utils::test_utils::{check_container_created, CGROUP_ROOT}; fn create_spec(cgroup_name: &str, class_id: u32, prio: u32, if_name: &str) -> Result { // Create the Linux Spec @@ -59,8 +60,9 @@ fn test_relative_network_cgroups() -> TestResult { let if_name = "lo"; let spec = test_result!(create_spec(cgroup_name, id, prio, if_name)); - let test_result = test_outside_container(spec, &|data| { + let test_result = test_outside_container(spec.clone(), &|data| { test_result!(check_container_created(&data)); + test_result!(validate_network(cgroup_name, &spec)); TestResult::Passed }); if let TestResult::Failed(_) = test_result { @@ -70,6 +72,52 @@ fn test_relative_network_cgroups() -> TestResult { TestResult::Passed } +/// validates the Network structure parsed from /sys/fs/cgroup/net_cls,net_prio with the spec +fn validate_network(cgroup_name: &str, spec: &Spec) -> Result<()> { + let cgroup_path = PathBuf::from(CGROUP_ROOT) + .join("net_cls,net_prio/runtime-test") + .join(cgroup_name); + + let resources = spec.linux().as_ref().unwrap().resources().as_ref().unwrap(); + let spec_network = resources.network().as_ref().unwrap(); + + // Validate net_cls.classid + let classid_path = cgroup_path.join("net_cls.classid"); + let classid_content = fs::read_to_string(&classid_path) + .with_context(|| format!("failed to read {:?}", classid_path))?; + let expected_classid = spec_network.class_id().unwrap(); + let actual_classid: u32 = classid_content + .trim() + .parse() + .with_context(|| format!("could not parse {:?}", classid_content.trim()))?; + if expected_classid != actual_classid { + bail!( + "expected {:?} to contain a classid of {}, but the classid was {}", + classid_path, + expected_classid, + actual_classid + ); + } + + // Validate net_prio.ifpriomap + let ifpriomap_path = cgroup_path.join("net_prio.ifpriomap"); + let ifpriomap_content = fs::read_to_string(&ifpriomap_path) + .with_context(|| format!("failed to read {:?}", ifpriomap_path))?; + let expected_priorities = spec_network.priorities().as_ref().unwrap(); + for priority in expected_priorities { + let expected_entry = format!("{} {}", priority.name(), priority.priority()); + if !ifpriomap_content.contains(&expected_entry) { + bail!( + "expected {:?} to contain an entry '{}', but it was not found", + ifpriomap_path, + expected_entry + ); + } + } + + Ok(()) +} + fn can_run() -> bool { // Ensure the expected network interfaces exist on the system running the test let iface_exists = get_loopback_interface().is_some(); From ca7ab44933730c0591270e1dd50862db5dc38514 Mon Sep 17 00:00:00 2001 From: moz-sec Date: Mon, 16 Dec 2024 16:14:33 +0900 Subject: [PATCH 4/4] fix: support different mount points in network cgroup Signed-off-by: moz-sec --- .../src/tests/cgroups/relative_network.rs | 79 ++++++++++++++----- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/tests/contest/contest/src/tests/cgroups/relative_network.rs b/tests/contest/contest/src/tests/cgroups/relative_network.rs index c3e749f3b..cf6887de1 100644 --- a/tests/contest/contest/src/tests/cgroups/relative_network.rs +++ b/tests/contest/contest/src/tests/cgroups/relative_network.rs @@ -60,31 +60,59 @@ fn test_relative_network_cgroups() -> TestResult { let if_name = "lo"; let spec = test_result!(create_spec(cgroup_name, id, prio, if_name)); - let test_result = test_outside_container(spec.clone(), &|data| { + test_outside_container(spec.clone(), &|data| { test_result!(check_container_created(&data)); test_result!(validate_network(cgroup_name, &spec)); TestResult::Passed - }); - if let TestResult::Failed(_) = test_result { - return test_result; - } - - TestResult::Passed + }) } /// validates the Network structure parsed from /sys/fs/cgroup/net_cls,net_prio with the spec fn validate_network(cgroup_name: &str, spec: &Spec) -> Result<()> { - let cgroup_path = PathBuf::from(CGROUP_ROOT) - .join("net_cls,net_prio/runtime-test") - .join(cgroup_name); + let (net_cls_path, net_prio_path) = if Path::new("/sys/fs/cgroup/net_cls/net_cls.classid") + .exists() + && Path::new("/sys/fs/cgroup/net_prio/net_prio.ifpriomap").exists() + { + ( + net_cls_path(PathBuf::from(CGROUP_ROOT).join("net_cls"), cgroup_name), + net_prio_path(PathBuf::from(CGROUP_ROOT).join("net_prio"), cgroup_name), + ) + } else if Path::new("/sys/fs/cgroup/net_cls,net_prio/net_cls.classid").exists() + && Path::new("/sys/fs/cgroup/net_cls,net_prio/net_prio.ifpriomap").exists() + { + ( + net_cls_path( + PathBuf::from(CGROUP_ROOT).join("net_cls,net_prio"), + cgroup_name, + ), + net_prio_path( + PathBuf::from(CGROUP_ROOT).join("net_cls,net_prio"), + cgroup_name, + ), + ) + } else if Path::new("/sys/fs/cgroup/net_prio,net_cls/net_cls.classid").exists() + && Path::new("/sys/fs/cgroup/net_prio,net_cls/net_prio.ifpriomap").exists() + { + ( + net_cls_path( + PathBuf::from(CGROUP_ROOT).join("net_prio,net_cls"), + cgroup_name, + ), + net_prio_path( + PathBuf::from(CGROUP_ROOT).join("net_prio,net_cls"), + cgroup_name, + ), + ) + } else { + return Err(anyhow::anyhow!("Required cgroup paths do not exist")); + }; let resources = spec.linux().as_ref().unwrap().resources().as_ref().unwrap(); let spec_network = resources.network().as_ref().unwrap(); // Validate net_cls.classid - let classid_path = cgroup_path.join("net_cls.classid"); - let classid_content = fs::read_to_string(&classid_path) - .with_context(|| format!("failed to read {:?}", classid_path))?; + let classid_content = fs::read_to_string(&net_cls_path) + .with_context(|| format!("failed to read {:?}", net_cls_path))?; let expected_classid = spec_network.class_id().unwrap(); let actual_classid: u32 = classid_content .trim() @@ -93,23 +121,22 @@ fn validate_network(cgroup_name: &str, spec: &Spec) -> Result<()> { if expected_classid != actual_classid { bail!( "expected {:?} to contain a classid of {}, but the classid was {}", - classid_path, + net_cls_path, expected_classid, actual_classid ); } // Validate net_prio.ifpriomap - let ifpriomap_path = cgroup_path.join("net_prio.ifpriomap"); - let ifpriomap_content = fs::read_to_string(&ifpriomap_path) - .with_context(|| format!("failed to read {:?}", ifpriomap_path))?; + let ifpriomap_content = fs::read_to_string(&net_prio_path) + .with_context(|| format!("failed to read {:?}", net_prio_path))?; let expected_priorities = spec_network.priorities().as_ref().unwrap(); for priority in expected_priorities { let expected_entry = format!("{} {}", priority.name(), priority.priority()); if !ifpriomap_content.contains(&expected_entry) { bail!( "expected {:?} to contain an entry '{}', but it was not found", - ifpriomap_path, + net_prio_path, expected_entry ); } @@ -118,6 +145,20 @@ fn validate_network(cgroup_name: &str, spec: &Spec) -> Result<()> { Ok(()) } +fn net_cls_path(base_path: PathBuf, cgroup_name: &str) -> PathBuf { + base_path + .join("testdir/runtime-test/container") + .join(cgroup_name) + .join("net_cls.classid") +} + +fn net_prio_path(base_path: PathBuf, cgroup_name: &str) -> PathBuf { + base_path + .join("testdir/runtime-test/container") + .join(cgroup_name) + .join("net_prio.ifpriomap") +} + fn can_run() -> bool { // Ensure the expected network interfaces exist on the system running the test let iface_exists = get_loopback_interface().is_some(); @@ -128,7 +169,7 @@ fn can_run() -> bool { || (Path::new("/sys/fs/cgroup/net_cls,net_prio/net_cls.classid").exists() && Path::new("/sys/fs/cgroup/net_cls,net_prio/net_prio.ifpriomap").exists()) || (Path::new("/sys/fs/cgroup/net_prio,net_cls/net_cls.classid").exists() - && Path::new("/sys/fs/cgroup/net_prio,net_cl/net_prio.ifpriomap").exists()); + && Path::new("/sys/fs/cgroup/net_prio,net_cls/net_prio.ifpriomap").exists()); iface_exists && cgroup_paths_exists }