diff --git a/src/firewall/varktables/types.rs b/src/firewall/varktables/types.rs index bd675332b..ac842f092 100644 --- a/src/firewall/varktables/types.rs +++ b/src/firewall/varktables/types.rs @@ -3,7 +3,7 @@ use crate::firewall::varktables::helpers::{ add_chain_unique, append_unique, remove_if_rule_exists, }; use crate::firewall::varktables::types::TeardownPolicy::{Never, OnComplete}; -use crate::network::internal_types::PortForwardConfig; +use crate::network::internal_types::{IsolateOption, PortForwardConfig}; use ipnet::IpNet; use iptables::IPTables; use log::debug; @@ -26,6 +26,7 @@ const MARK: &str = "MARK"; const DNAT: &str = "DNAT"; const NETAVARK_ISOLATION_1: &str = "NETAVARK_ISOLATION_1"; const NETAVARK_ISOLATION_2: &str = "NETAVARK_ISOLATION_2"; +const NETAVARK_ISOLATION_3: &str = "NETAVARK_ISOLATION_3"; const CONTAINER_DN_CHAIN: &str = "NETAVARK-DN-"; @@ -194,7 +195,7 @@ pub fn get_network_chains<'a>( network_hash_name: &'a str, is_ipv6: bool, interface_name: String, - isolation: bool, + isolation: IsolateOption, ) -> Vec> { let mut chains = Vec::new(); let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", network_hash_name); @@ -238,7 +239,31 @@ pub fn get_network_chains<'a>( // used to prepend specific rules let mut ind = 1; - if isolation { + // NETAVARK_ISOLATION_2 + // NETAVARK_ISOLATION_2 chain must always exist, + // because non-isolation creates DROP rule in NETAVARK_ISOLATION_3 + // and NETAVARK_ISOLATION_3 references this as a jump target. + let mut netavark_isolation_chain_2 = VarkChain::new( + conn, + FILTER.to_string(), + NETAVARK_ISOLATION_2.to_string(), + None, + ); + netavark_isolation_chain_2.create = true; + + // NETAVARK_ISOLATION_3 + // NETAVARK_ISOLATION_3 chain must exist when IsolateOption is Never or Strict. + // bacause non-isolation creates DROP rule in NETAVARK_ISOLATION_3. + // and strict isolation references NETAVARK_ISOLATION_3 as a jump target. + let mut netavark_isolation_chain_3 = VarkChain::new( + conn, + FILTER.to_string(), + NETAVARK_ISOLATION_3.to_string(), + None, + ); + netavark_isolation_chain_3.create = true; + + if let IsolateOption::Nomal | IsolateOption::Strict = isolation { debug!("Add extra isolate rules"); // NETAVARK_ISOLATION_1 let mut netavark_isolation_chain_1 = VarkChain::new( @@ -249,15 +274,6 @@ pub fn get_network_chains<'a>( ); netavark_isolation_chain_1.create = true; - // NETAVARK_ISOLATION_2 - let mut netavark_isolation_chain_2 = VarkChain::new( - conn, - FILTER.to_string(), - NETAVARK_ISOLATION_2.to_string(), - None, - ); - netavark_isolation_chain_2.create = true; - // -A FORWARD -j NETAVARK_ISOLATION_1 forward_chain.build_rule(VarkRule { rule: format!("-j {}", NETAVARK_ISOLATION_1), @@ -265,11 +281,17 @@ pub fn get_network_chains<'a>( td_policy: Some(TeardownPolicy::OnComplete), }); - // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j DROP + let netavark_isolation_1_target = if let IsolateOption::Strict = isolation { + // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_3 + NETAVARK_ISOLATION_3 + } else { + // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_2 + NETAVARK_ISOLATION_2 + }; netavark_isolation_chain_1.build_rule(VarkRule { rule: format!( "-i {} ! -o {} -j {}", - interface_name, interface_name, NETAVARK_ISOLATION_2 + interface_name, interface_name, netavark_isolation_1_target ), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), @@ -282,13 +304,40 @@ pub fn get_network_chains<'a>( td_policy: Some(TeardownPolicy::OnComplete), }); + // NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2 + netavark_isolation_chain_3.build_rule(VarkRule { + rule: format!("-j {}", NETAVARK_ISOLATION_2), + position: Some(ind), + td_policy: Some(TeardownPolicy::Never), + }); + ind += 1; // PUSH CHAIN chains.push(netavark_isolation_chain_1); - chains.push(netavark_isolation_chain_2) + } else { + // create DROP rule for non-isolations to enforce strict isolation rules. + + // NETAVARK_ISOLATION_3 -o bridge_name -j DROP + netavark_isolation_chain_3.build_rule(VarkRule { + rule: format!("-o {} -j {}", interface_name, "DROP"), + position: Some(ind), + td_policy: Some(TeardownPolicy::OnComplete), + }); + + // NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2 + netavark_isolation_chain_3.build_rule(VarkRule { + rule: format!("-j {}", NETAVARK_ISOLATION_2), + // position +1 to place this rule under all of NETAVARK_ISOLATION_3 DROP rules. + position: Some(ind + 1), + td_policy: Some(TeardownPolicy::Never), + }); } + // PUSH CHAIN + chains.push(netavark_isolation_chain_2); + chains.push(netavark_isolation_chain_3); + forward_chain.build_rule(VarkRule { rule: format!( "-m comment --comment 'netavark firewall plugin rules' -j {}", diff --git a/src/network/bridge.rs b/src/network/bridge.rs index 20afed04b..f749f42b8 100644 --- a/src/network/bridge.rs +++ b/src/network/bridge.rs @@ -17,13 +17,15 @@ use crate::{ use super::{ constants::{ + ISOLATE_OPTION_FALSE, ISOLATE_OPTION_STRICT, ISOLATE_OPTION_TRUE, NO_CONTAINER_INTERFACE_ERROR, OPTION_ISOLATE, OPTION_METRIC, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, }, core_utils::{self, get_ipam_addresses, join_netns, parse_option, CoreUtils}, driver::{self, DriverInfo}, internal_types::{ - IPAMAddresses, PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, + IPAMAddresses, IsolateOption, PortForwardConfig, SetupNetwork, TearDownNetwork, + TeardownPortForward, }, netlink, types::StatusBlock, @@ -43,7 +45,7 @@ struct InternalData { /// mtu for the network interfaces (0 if default) mtu: u32, /// if this network should be isolated from others - isolate: bool, + isolate: IsolateOption, /// Route metric for any default routes added for the network metric: Option, /// if set, no default gateway will be added @@ -75,8 +77,7 @@ impl driver::NetworkDriver for Bridge<'_> { let ipam = get_ipam_addresses(self.info.per_network_opts, self.info.network)?; let mtu: u32 = parse_option(&self.info.network.options, OPTION_MTU)?.unwrap_or(0); - let isolate: bool = - parse_option(&self.info.network.options, OPTION_ISOLATE)?.unwrap_or(false); + let isolate: IsolateOption = get_isolate_option(&self.info.network.options)?; let metric: u32 = parse_option(&self.info.network.options, OPTION_METRIC)?.unwrap_or(100); let no_default_route: bool = parse_option(&self.info.network.options, OPTION_NO_DEFAULT_ROUTE)?.unwrap_or(false); @@ -303,7 +304,7 @@ impl<'a> Bridge<'a> { &'a self, container_addresses: &Vec, nameservers: &'a Vec, - isolate: bool, + isolate: IsolateOption, ) -> NetavarkResult<(SetupNetwork, PortForwardConfig)> { let id_network_hash = CoreUtils::create_network_hash(&self.info.network.name, MAX_HASH_SIZE); @@ -389,15 +390,11 @@ impl<'a> Bridge<'a> { let (container_addresses_ref, nameservers_ref, isolate) = match &self.data { Some(d) => (&d.ipam.container_addresses, &d.ipam.nameservers, d.isolate), None => { - // options are not yet parsed - let isolate: bool = match parse_option(&self.info.network.options, OPTION_ISOLATE) { - Ok(i) => i.unwrap_or(false), - Err(e) => { - // just log we still try to do as much as possible for cleanup - error!("failed to parse {} option: {}", OPTION_ISOLATE, e); - false - } - }; + let isolate = get_isolate_option(&self.info.network.options).unwrap_or_else(|e| { + // just log we still try to do as much as possible for cleanup + error!("failed to parse {} option: {}", OPTION_ISOLATE, e); + IsolateOption::Never + }); (container_addresses, nameservers) = match get_ipam_addresses(self.info.per_network_opts, self.info.network) { @@ -706,3 +703,14 @@ fn remove_link( } Ok(false) } + +fn get_isolate_option(opts: &Option>) -> NetavarkResult { + let isolate = parse_option(opts, OPTION_ISOLATE)?.unwrap_or(ISOLATE_OPTION_FALSE.to_string()); + // return isolate option value "false" if unknown value or no value passed + Ok(match isolate.as_str() { + ISOLATE_OPTION_STRICT => IsolateOption::Strict, + ISOLATE_OPTION_TRUE => IsolateOption::Nomal, + ISOLATE_OPTION_FALSE => IsolateOption::Never, + _ => IsolateOption::Never, + }) +} diff --git a/src/network/constants.rs b/src/network/constants.rs index 9e5e51ed4..a48a43321 100644 --- a/src/network/constants.rs +++ b/src/network/constants.rs @@ -13,6 +13,9 @@ pub const DRIVER_IPVLAN: &str = "ipvlan"; pub const DRIVER_MACVLAN: &str = "macvlan"; pub const OPTION_ISOLATE: &str = "isolate"; +pub const ISOLATE_OPTION_TRUE: &str = "true"; +pub const ISOLATE_OPTION_FALSE: &str = "false"; +pub const ISOLATE_OPTION_STRICT: &str = "strict"; pub const OPTION_MTU: &str = "mtu"; pub const OPTION_MODE: &str = "mode"; pub const OPTION_METRIC: &str = "metric"; diff --git a/src/network/internal_types.rs b/src/network/internal_types.rs index 0dd2d8841..18df2a109 100644 --- a/src/network/internal_types.rs +++ b/src/network/internal_types.rs @@ -18,7 +18,7 @@ pub struct SetupNetwork { /// hash id for the network pub network_hash_name: String, /// isolation determines whether the network can communicate with others outside of its interface - pub isolation: bool, + pub isolation: IsolateOption, } #[derive(Debug)] @@ -73,3 +73,11 @@ pub struct IPAMAddresses { pub net_addresses: Vec, pub nameservers: Vec, } + +// IsolateOption is used to select isolate option value +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum IsolateOption { + Strict, + Nomal, + Never, +} diff --git a/test/100-bridge-iptables.bats b/test/100-bridge-iptables.bats index b258063b0..4178c3e75 100644 --- a/test/100-bridge-iptables.bats +++ b/test/100-bridge-iptables.bats @@ -630,46 +630,142 @@ EOF } @test "$fw_driver - isolate networks" { + # create container/networks with isolation + + # isolate1: 10.89.0.2/24, fd90::2, isolate=true run_netavark --file ${TESTSDIR}/testfiles/isolate1.json setup $(get_container_netns_path) + # isolate2: 10.89.1.2/24, fd99::2, isolate=true create_container_ns run_netavark --file ${TESTSDIR}/testfiles/isolate2.json setup $(get_container_netns_path 1) + # isolate3: 10.89.2.2/24, fd92::2, isolate=strict + create_container_ns + run_netavark --file ${TESTSDIR}/testfiles/isolate3.json setup $(get_container_netns_path 2) + + # isolate4: 10.89.3.2/24, fd93::2, isolate=strict + create_container_ns + run_netavark --file ${TESTSDIR}/testfiles/isolate4.json setup $(get_container_netns_path 3) + # check iptables NETAVARK_ISOLATION_1 chain run_in_host_netns iptables -S NETAVARK_ISOLATION_1 - assert "${lines[1]}" == "-A NETAVARK_ISOLATION_1 -i isolate2 ! -o isolate2 -j NETAVARK_ISOLATION_2" - assert "${lines[2]}" == "-A NETAVARK_ISOLATION_1 -i isolate1 ! -o isolate1 -j NETAVARK_ISOLATION_2" + assert "${lines[1]}" == "-A NETAVARK_ISOLATION_1 -i isolate4 ! -o isolate4 -j NETAVARK_ISOLATION_3" + assert "${lines[2]}" == "-A NETAVARK_ISOLATION_1 -i isolate3 ! -o isolate3 -j NETAVARK_ISOLATION_3" + assert "${lines[3]}" == "-A NETAVARK_ISOLATION_1 -i isolate2 ! -o isolate2 -j NETAVARK_ISOLATION_2" + assert "${lines[4]}" == "-A NETAVARK_ISOLATION_1 -i isolate1 ! -o isolate1 -j NETAVARK_ISOLATION_2" run_in_host_netns iptables -nvL FORWARD assert "${lines[2]}" =~ "NETAVARK_ISOLATION_1" # check iptables NETAVARK_ISOLATION_2 chain run_in_host_netns iptables -S NETAVARK_ISOLATION_2 - assert "${lines[1]}" == "-A NETAVARK_ISOLATION_2 -o isolate2 -j DROP" - assert "${lines[2]}" == "-A NETAVARK_ISOLATION_2 -o isolate1 -j DROP" + assert "${lines[1]}" == "-A NETAVARK_ISOLATION_2 -o isolate4 -j DROP" + assert "${lines[2]}" == "-A NETAVARK_ISOLATION_2 -o isolate3 -j DROP" + assert "${lines[3]}" == "-A NETAVARK_ISOLATION_2 -o isolate2 -j DROP" + assert "${lines[4]}" == "-A NETAVARK_ISOLATION_2 -o isolate1 -j DROP" + + # check iptables NETAVARK_ISOLATION_3 chain + run_in_host_netns iptables -S NETAVARK_ISOLATION_3 + assert "${lines[1]}" == "-A NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2" # ping our own ip to make sure the ips work and there is no typo run_in_container_netns ping -w 1 -c 1 10.89.0.2 run_in_container_netns 1 ping -w 1 -c 1 10.89.1.2 + run_in_container_netns 2 ping -w 1 -c 1 10.89.2.2 + run_in_container_netns 3 ping -w 1 -c 1 10.89.3.2 # make sure the isolated network cannot reach the other network + + # from network isolate1 to isolate2 expected_rc=1 run_in_container_netns ping -w 1 -c 1 10.89.1.2 + # from network isolate1 to isolate3 + expected_rc=1 run_in_container_netns ping -w 1 -c 1 10.89.2.2 + # from network isolate1 to isolate4 + expected_rc=1 run_in_container_netns ping -w 1 -c 1 10.89.3.2 + + # from network isolate2 to isolate1 expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 10.89.0.2 + # from network isolate2 to isolate3 + expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 10.89.2.2 + # from network isolate2 to isolate4 + expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 10.89.3.2 + + # from network isolate3 to isolate1 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 10.89.0.2 + # from network isolate3 to isolate2 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 10.89.1.2 + # from network isolate3 to isolate4 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 10.89.3.2 + + # from network isolate4 to isolate1 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 10.89.0.2 + # from network isolate4 to isolate2 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 10.89.1.2 + # from network isolate4 to isolate3 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 10.89.2.2 # now the same with ipv6 + run_in_container_netns ping -w 1 -c 1 fd90::2 run_in_container_netns 1 ping -w 1 -c 1 fd99::2 + run_in_container_netns 2 ping -w 1 -c 1 fd92::2 + run_in_container_netns 3 ping -w 1 -c 1 fd93::2 + # from network isolate1 to isolate2 expected_rc=1 run_in_container_netns ping -w 1 -c 1 fd99::2 - expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 fd90::2 - - # create container/network without isolation, this should be able to ping isolated containers + # from network isolate1 to isolate3 + expected_rc=1 run_in_container_netns ping -w 1 -c 1 fd92::2 + # from network isolate1 to isolate4 + expected_rc=1 run_in_container_netns ping -w 1 -c 1 fd93::2 + # from network isolate2 to isolate1 + expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 fd90::2 + # from network isolate2 to isolate3 + expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 fd92::2 + # from network isolate2 to isolate4 + expected_rc=1 run_in_container_netns 1 ping -w 1 -c 1 fd93::2 + + # from network isolate3 to isolate1 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 fd90::2 + # from network isolate3 to isolate2 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 fd99::2 + # from network isolate3 to isolate4 + expected_rc=1 run_in_container_netns 2 ping -w 1 -c 1 fd93::2 + + # from network isolate4 to isolate1 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 fd90::2 + # from network isolate4 to isolate2 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 fd99::2 + # from network isolate4 to isolate3 + expected_rc=1 run_in_container_netns 3 ping -w 1 -c 1 fd92::2 + + # create container/network without isolation + + # podman: 10.88.0.2/16 create_container_ns - run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path 2) - - run_in_container_netns 2 ping -w 1 -c 1 10.88.0.2 - + run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json setup $(get_container_netns_path 4) + + # check iptables NETAVARK_ISOLATION_3 chain + run_in_host_netns iptables -S NETAVARK_ISOLATION_3 + assert "${lines[1]}" == "-A NETAVARK_ISOLATION_3 -o podman0 -j DROP" + assert "${lines[2]}" == "-A NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2" + + # this should be able to ping non-strict isolated containers + # from network podman to isolate1 + run_in_container_netns 4 ping -w 1 -c 1 10.89.0.2 + # from network podman to isolate2 + run_in_container_netns 4 ping -w 1 -c 1 10.89.1.2 + + # and should NOT be able to ping strict isolated containers + # from network podman to isolate3 + expected_rc=1 run_in_container_netns 4 ping -w 1 -c 1 10.89.2.2 + # from network podman to isolate4 + expected_rc=1 run_in_container_netns 4 ping -w 1 -c 1 10.89.3.2 + + # teardown all networks + run_netavark --file ${TESTSDIR}/testfiles/simplebridge.json teardown $(get_container_netns_path 4) + run_netavark --file ${TESTSDIR}/testfiles/isolate4.json teardown $(get_container_netns_path 3) + run_netavark --file ${TESTSDIR}/testfiles/isolate3.json teardown $(get_container_netns_path 2) run_netavark --file ${TESTSDIR}/testfiles/isolate2.json teardown $(get_container_netns_path 1) run_netavark --file ${TESTSDIR}/testfiles/isolate1.json teardown $(get_container_netns_path) @@ -678,6 +774,8 @@ EOF assert "${lines[2]}" == "" "NETAVARK_ISOLATION_1 chain should be empty" run_in_host_netns iptables -nvL NETAVARK_ISOLATION_2 assert "${lines[2]}" == "" "NETAVARK_ISOLATION_2 chain should be empty" + run_in_host_netns iptables -S NETAVARK_ISOLATION_3 + assert "${lines[1]}" == "-A NETAVARK_ISOLATION_3 -j NETAVARK_ISOLATION_2" } @test "$fw_driver - test read only /proc" { diff --git a/test/testfiles/isolate3.json b/test/testfiles/isolate3.json new file mode 100644 index 000000000..72d8adf3f --- /dev/null +++ b/test/testfiles/isolate3.json @@ -0,0 +1,37 @@ +{ + "container_id": "01f0b94d5f4c1", + "container_name": "isolatedcontainer1", + "networks": { + "isolate3": { + "interface_name": "eth3", + "static_ips": [ + "10.89.2.2", + "fd92::2" + ] + } + }, + "network_info": { + "isolate3": { + "dns_enabled": false, + "driver": "bridge", + "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", + "internal": false, + "ipv6_enabled": false, + "name": "isolate3", + "network_interface": "isolate3", + "subnets": [ + { + "gateway": "10.89.2.1", + "subnet": "10.89.2.0/24" + }, + { + "subnet": "fd92::/64", + "gateway": "fd92::1" + } + ], + "options": { + "isolate": "strict" + } + } + } +} diff --git a/test/testfiles/isolate4.json b/test/testfiles/isolate4.json new file mode 100644 index 000000000..293ca5d52 --- /dev/null +++ b/test/testfiles/isolate4.json @@ -0,0 +1,37 @@ +{ + "container_id": "01f0b94d5f4c1", + "container_name": "isolatedcontainer1", + "networks": { + "isolate4": { + "interface_name": "eth4", + "static_ips": [ + "10.89.3.2", + "fd93::2" + ] + } + }, + "network_info": { + "isolate4": { + "dns_enabled": false, + "driver": "bridge", + "id": "f3ac3c903b76f20e8a122b1eb2ba393ab2519ba516626be5b490b66dc96b54b7", + "internal": false, + "ipv6_enabled": false, + "name": "isolate4", + "network_interface": "isolate4", + "subnets": [ + { + "gateway": "10.89.3.1", + "subnet": "10.89.3.0/24" + }, + { + "subnet": "fd93::/64", + "gateway": "fd93::1" + } + ], + "options": { + "isolate": "strict" + } + } + } +}