diff --git a/src/firewall/iptables.rs b/src/firewall/iptables.rs index 817ca79ca..b8d238bbf 100644 --- a/src/firewall/iptables.rs +++ b/src/firewall/iptables.rs @@ -3,7 +3,8 @@ use crate::firewall; use crate::firewall::firewalld; use crate::firewall::varktables::types::TeardownPolicy::OnComplete; use crate::firewall::varktables::types::{ - create_network_chains, get_network_chains, get_port_forwarding_chains, TeardownPolicy, + create_network_chains, get_network_chains, get_port_forwarding_chains, NetworkChainConfig, + TeardownPolicy, }; use crate::network::internal_types::{ PortForwardConfig, SetupNetwork, TearDownNetwork, TeardownPortForward, @@ -54,15 +55,16 @@ impl firewall::FirewallDriver for IptablesDriver { conn = &self.conn6; } - let chains = get_network_chains( - conn, + let config = NetworkChainConfig { network, - &network_setup.network_hash_name, - is_ipv6, - network_setup.bridge_name.clone(), - network_setup.isolation, - network_setup.dns_port, - ); + network_hash_name: network_setup.network_hash_name.clone(), + interface_name: network_setup.bridge_name.clone(), + isolation: network_setup.isolation, + dns_port: network_setup.dns_port, + outbound_addr4: network_setup.outbound_addr4, + outbound_addr6: network_setup.outbound_addr6, + }; + let chains = get_network_chains(conn, config); create_network_chains(chains)?; @@ -83,15 +85,16 @@ impl firewall::FirewallDriver for IptablesDriver { if is_ipv6 { conn = &self.conn6; } - let chains = get_network_chains( - conn, + let config = NetworkChainConfig { network, - &tear.config.network_hash_name, - is_ipv6, - tear.config.bridge_name.clone(), - tear.config.isolation, - tear.config.dns_port, - ); + network_hash_name: tear.config.network_hash_name.clone(), + interface_name: tear.config.bridge_name.clone(), + isolation: tear.config.isolation, + dns_port: tear.config.dns_port, + outbound_addr4: tear.config.outbound_addr4, + outbound_addr6: tear.config.outbound_addr6, + }; + let chains = get_network_chains(conn, config); for c in &chains { c.remove_rules(tear.complete_teardown)?; diff --git a/src/firewall/nft.rs b/src/firewall/nft.rs index c797e4159..128239fcd 100644 --- a/src/firewall/nft.rs +++ b/src/firewall/nft.rs @@ -31,6 +31,9 @@ const ISOLATION3CHAIN: &str = "NETAVARK-ISOLATION-3"; const MASK: u32 = 0x2000; +const MULTICAST_NET_V4: &str = "224.0.0.0/4"; +const MULTICAST_NET_V6: &str = "ff00::/8"; + /// The dnat priority for chains /// This (and the below) are based on https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks#Priority_within_hook const DNATPRIO: i32 = -100; @@ -376,18 +379,91 @@ impl firewall::FirewallDriver for Nftables { ]), )); - // Subnet chain: ip daddr != 224.0.0.0/4 masquerade + // Subnet chain: ip daddr != 224.0.0.0/4 snat/masquerade let multicast_address: IpNet = match subnet { - IpNet::V4(_) => "224.0.0.0/4".parse()?, - IpNet::V6(_) => "ff::00/8".parse()?, + IpNet::V4(_) => MULTICAST_NET_V4.parse()?, + IpNet::V6(_) => MULTICAST_NET_V6.parse()?, }; - batch.add(make_rule( - chain.clone(), - Cow::Owned(vec![ - get_subnet_match(&multicast_address, "daddr", stmt::Operator::NEQ), - stmt::Statement::Masquerade(None), - ]), - )); + + // Use appropriate outbound address based on subnet type + match subnet { + IpNet::V4(_) => { + if let Some(addr4) = network_setup.outbound_addr4 { + log::trace!("Creating IPv4 SNAT rule with outbound address {addr4}"); + batch.add(make_rule( + chain.clone(), + Cow::Owned(vec![ + get_subnet_match( + &multicast_address, + "daddr", + stmt::Operator::NEQ, + ), + stmt::Statement::SNAT(Some(stmt::NAT { + addr: Some(expr::Expression::String( + addr4.to_string().into(), + )), + family: Some(stmt::NATFamily::IP), + port: None, + flags: None, + })), + ]), + )); + } else { + log::trace!( + "No IPv4 outbound address set, using default MASQUERADE rule" + ); + batch.add(make_rule( + chain.clone(), + Cow::Owned(vec![ + get_subnet_match( + &multicast_address, + "daddr", + stmt::Operator::NEQ, + ), + stmt::Statement::Masquerade(None), + ]), + )); + } + } + IpNet::V6(_) => { + if let Some(addr6) = network_setup.outbound_addr6 { + log::trace!("Creating IPv6 SNAT rule with outbound address {addr6}"); + batch.add(make_rule( + chain.clone(), + Cow::Owned(vec![ + get_subnet_match( + &multicast_address, + "daddr", + stmt::Operator::NEQ, + ), + stmt::Statement::SNAT(Some(stmt::NAT { + addr: Some(expr::Expression::String( + addr6.to_string().into(), + )), + family: Some(stmt::NATFamily::IP6), + port: None, + flags: None, + })), + ]), + )); + } else { + log::trace!( + "No IPv6 outbound address set, using default MASQUERADE rule" + ); + batch.add(make_rule( + chain.clone(), + Cow::Owned(vec![ + get_subnet_match( + &multicast_address, + "daddr", + stmt::Operator::NEQ, + ), + stmt::Statement::Masquerade(None), + ]), + )); + } + } + } // Next, populate basic chains with forwarding rules // Input chain: ip saddr udp dport 53 accept diff --git a/src/firewall/state.rs b/src/firewall/state.rs index 6165f462c..90b2dac73 100644 --- a/src/firewall/state.rs +++ b/src/firewall/state.rs @@ -272,6 +272,8 @@ mod tests { network_hash_name: "hash".to_string(), isolation: IsolateOption::Never, dns_port: 53, + outbound_addr4: None, + outbound_addr6: None, }; let net_conf_json = r#"{"subnets":["10.0.0.0/24"],"bridge_name":"bridge","network_id":"c2c8a073252874648259997d53b0a1bffa491e21f04bc1bf8609266359931395","network_hash_name":"hash","isolation":"Never","dns_port":53}"#; diff --git a/src/firewall/varktables/types.rs b/src/firewall/varktables/types.rs index 54e1f333c..329df35b4 100644 --- a/src/firewall/varktables/types.rs +++ b/src/firewall/varktables/types.rs @@ -7,7 +7,7 @@ use crate::network::internal_types::{IsolateOption, PortForwardConfig}; use ipnet::IpNet; use iptables::IPTables; use log::debug; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; // Chain names const NAT: &str = "nat"; @@ -66,6 +66,7 @@ impl VarkRule { &self.rule } } + // Varkchain is an iptable chain with extra info pub struct VarkChain<'a> { // name of chain @@ -192,17 +193,24 @@ pub fn create_network_chains(chains: Vec>) -> NetavarkResult<()> { Ok(()) } -pub fn get_network_chains<'a>( - conn: &'a IPTables, - network: IpNet, - network_hash_name: &'a str, - is_ipv6: bool, - interface_name: String, - isolation: IsolateOption, - dns_port: u16, -) -> Vec> { +pub struct NetworkChainConfig { + pub network: IpNet, + pub network_hash_name: String, + pub interface_name: String, + pub isolation: IsolateOption, + pub dns_port: u16, + pub outbound_addr4: Option, + pub outbound_addr6: Option, +} + +pub fn get_network_chains(conn: &IPTables, config: NetworkChainConfig) -> Vec> { let mut chains = Vec::new(); - let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", network_hash_name); + let prefixed_network_hash_name = format!("{}-{}", "NETAVARK", config.network_hash_name); + + let is_ipv6 = match config.network { + IpNet::V4(_) => false, + IpNet::V6(_) => true, + }; // NETAVARK-HASH let mut hashed_network_chain = VarkChain::new( @@ -214,7 +222,7 @@ pub fn get_network_chains<'a>( hashed_network_chain.create = true; hashed_network_chain.build_rule(VarkRule::new( - format!("-d {network} -j {ACCEPT}"), + format!("-d {} -j {}", config.network, ACCEPT), Some(TeardownPolicy::OnComplete), )); @@ -222,17 +230,43 @@ pub fn get_network_chains<'a>( if is_ipv6 { multicast_dest = MULTICAST_NET_V6; } - hashed_network_chain.build_rule(VarkRule::new( - format!("! -d {multicast_dest} -j {MASQUERADE}"), - Some(TeardownPolicy::OnComplete), - )); + + // Use appropriate outbound address based on subnet type + if is_ipv6 { + if let Some(addr6) = config.outbound_addr6 { + log::trace!("Creating IPv6 SNAT rule with outbound address {addr6}"); + hashed_network_chain.build_rule(VarkRule::new( + format!("! -d {multicast_dest} -j SNAT --to-source {addr6}"), + Some(TeardownPolicy::OnComplete), + )); + } else { + log::trace!("No IPv6 outbound address set, using default MASQUERADE rule"); + hashed_network_chain.build_rule(VarkRule::new( + format!("! -d {multicast_dest} -j {MASQUERADE}"), + Some(TeardownPolicy::OnComplete), + )); + } + } else if let Some(addr4) = config.outbound_addr4 { + log::trace!("Creating IPv4 SNAT rule with outbound address {addr4}"); + hashed_network_chain.build_rule(VarkRule::new( + format!("! -d {multicast_dest} -j SNAT --to-source {addr4}"), + Some(TeardownPolicy::OnComplete), + )); + } else { + log::trace!("No IPv4 outbound address set, using default MASQUERADE rule"); + hashed_network_chain.build_rule(VarkRule::new( + format!("! -d {multicast_dest} -j {MASQUERADE}"), + Some(TeardownPolicy::OnComplete), + )); + } + chains.push(hashed_network_chain); // POSTROUTING let mut postrouting_chain = VarkChain::new(conn, NAT.to_string(), POSTROUTING.to_string(), None); postrouting_chain.build_rule(VarkRule::new( - format!("-s {network} -j {prefixed_network_hash_name}"), + format!("-s {} -j {}", config.network, prefixed_network_hash_name), Some(TeardownPolicy::OnComplete), )); chains.push(postrouting_chain); @@ -272,7 +306,7 @@ pub fn get_network_chains<'a>( ); netavark_isolation_chain_3.create = true; - if let IsolateOption::Normal | IsolateOption::Strict = isolation { + if let IsolateOption::Normal | IsolateOption::Strict = config.isolation { debug!("Add extra isolate rules"); // NETAVARK_ISOLATION_1 let mut netavark_isolation_chain_1 = VarkChain::new( @@ -290,7 +324,7 @@ pub fn get_network_chains<'a>( td_policy: Some(TeardownPolicy::OnComplete), }); - let netavark_isolation_1_target = if let IsolateOption::Strict = isolation { + let netavark_isolation_1_target = if let IsolateOption::Strict = config.isolation { // NETAVARK_ISOLATION_1 -i bridge_name ! -o bridge_name -j NETAVARK_ISOLATION_3 NETAVARK_ISOLATION_3 } else { @@ -299,7 +333,8 @@ pub fn get_network_chains<'a>( }; netavark_isolation_chain_1.build_rule(VarkRule { rule: format!( - "-i {interface_name} ! -o {interface_name} -j {netavark_isolation_1_target}" + "-i {} ! -o {} -j {}", + config.interface_name, config.interface_name, netavark_isolation_1_target ), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), @@ -307,7 +342,7 @@ pub fn get_network_chains<'a>( // NETAVARK_ISOLATION_2 -o bridge_name -j DROP netavark_isolation_chain_2.build_rule(VarkRule { - rule: format!("-o {} -j {}", interface_name, "DROP"), + rule: format!("-o {} -j {}", config.interface_name, "DROP"), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); @@ -328,7 +363,7 @@ pub fn get_network_chains<'a>( // NETAVARK_ISOLATION_3 -o bridge_name -j DROP netavark_isolation_chain_3.build_rule(VarkRule { - rule: format!("-o {} -j {}", interface_name, "DROP"), + rule: format!("-o {} -j {}", config.interface_name, "DROP"), position: Some(ind), td_policy: Some(TeardownPolicy::OnComplete), }); @@ -375,7 +410,10 @@ pub fn get_network_chains<'a>( // to gateway when using bridge network with internal dns. for proto in ["udp", "tcp"] { netavark_input_chain.build_rule(VarkRule::new( - format!("-p {proto} -s {network} --dport {dns_port} -j {ACCEPT}"), + format!( + "-p {proto} -s {} --dport {} -j {ACCEPT}", + config.network, config.dns_port + ), Some(TeardownPolicy::OnComplete), )); } @@ -392,14 +430,17 @@ pub fn get_network_chains<'a>( // Create incoming traffic rule // CNI did this by IP address, this is implemented per subnet netavark_forward_chain.build_rule(VarkRule::new( - format!("-d {network} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT"), + format!( + "-d {} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT", + config.network + ), Some(TeardownPolicy::OnComplete), )); // Create outgoing traffic rule // CNI did this by IP address, this is implemented per subnet netavark_forward_chain.build_rule(VarkRule::new( - format!("-s {network} -j ACCEPT"), + format!("-s {} -j ACCEPT", config.network), Some(TeardownPolicy::OnComplete), )); chains.push(netavark_forward_chain); diff --git a/src/network/bridge.rs b/src/network/bridge.rs index 0a4ef72fc..f7652010d 100644 --- a/src/network/bridge.rs +++ b/src/network/bridge.rs @@ -1,4 +1,9 @@ -use std::{collections::HashMap, fs, net::IpAddr, os::fd::BorrowedFd}; +use std::{ + collections::HashMap, + fs, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + os::fd::BorrowedFd, +}; use ipnet::IpNet; use log::{debug, error}; @@ -25,7 +30,8 @@ use super::{ constants::{ ISOLATE_OPTION_FALSE, ISOLATE_OPTION_STRICT, ISOLATE_OPTION_TRUE, NO_CONTAINER_INTERFACE_ERROR, OPTION_HOST_INTERFACE_NAME, OPTION_ISOLATE, OPTION_METRIC, - OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_VLAN, OPTION_VRF, + OPTION_MODE, OPTION_MTU, OPTION_NO_DEFAULT_ROUTE, OPTION_OUTBOUND_ADDR4, + OPTION_OUTBOUND_ADDR6, OPTION_VLAN, OPTION_VRF, }, core_utils::{self, get_ipam_addresses, is_using_systemd, join_netns, parse_option, CoreUtils}, driver::{self, DriverInfo}, @@ -72,6 +78,10 @@ struct InternalData { vrf: Option, /// vlan id of the interface attached to the bridge vlan: Option, + /// outbound IPv4 address for SNAT + outbound_addr4: Option, + /// outbound IPv6 address for SNAT + outbound_addr6: Option, } pub struct Bridge<'a> { @@ -128,6 +138,12 @@ impl driver::NetworkDriver for Bridge<'_> { } } + // Parse outbound address options early to catch errors + let outbound_addr4: Option = + parse_option(&self.info.network.options, OPTION_OUTBOUND_ADDR4)?; + let outbound_addr6: Option = + parse_option(&self.info.network.options, OPTION_OUTBOUND_ADDR6)?; + self.data = Some(InternalData { bridge_interface_name: bridge_name, container_interface_name: self.info.per_network_opts.interface_name.clone(), @@ -141,6 +157,8 @@ impl driver::NetworkDriver for Bridge<'_> { no_default_route, vrf, vlan, + outbound_addr4, + outbound_addr6, }); Ok(()) } @@ -385,6 +403,8 @@ impl<'a> Bridge<'a> { nameservers: &'a Vec, isolate: IsolateOption, bridge_name: String, + outbound_addr4: Option, + outbound_addr6: Option, ) -> NetavarkResult<(SetupNetwork, PortForwardConfig<'a>)> { let id_network_hash = CoreUtils::create_network_hash(&self.info.network.name, MAX_HASH_SIZE); @@ -400,6 +420,8 @@ impl<'a> Bridge<'a> { network_hash_name: id_network_hash.clone(), isolation: isolate, dns_port: self.info.dns_port, + outbound_addr4, + outbound_addr6, }; let mut has_ipv4 = false; @@ -451,6 +473,8 @@ impl<'a> Bridge<'a> { &data.ipam.nameservers, data.isolate, data.bridge_interface_name.clone(), + data.outbound_addr4, + data.outbound_addr6, )?; if !self.info.rootless { @@ -481,33 +505,63 @@ impl<'a> Bridge<'a> { // "borrow later used" problems let (container_addresses, nameservers); - let (container_addresses_ref, nameservers_ref, isolate) = match &self.data { - Some(d) => (&d.ipam.container_addresses, &d.ipam.nameservers, d.isolate), - None => { - 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_ISOLATE} option: {e}"); - IsolateOption::Never - }); - - (container_addresses, nameservers) = - match get_ipam_addresses(self.info.per_network_opts, self.info.network) { - Ok(i) => (i.container_addresses, i.nameservers), - Err(e) => { + let (container_addresses_ref, nameservers_ref, isolate, outbound_addr4, outbound_addr6) = + match &self.data { + Some(d) => ( + &d.ipam.container_addresses, + &d.ipam.nameservers, + d.isolate, + d.outbound_addr4, + d.outbound_addr6, + ), + None => { + 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 ipam options: {e}"); - (Vec::new(), Vec::new()) - } - }; - (&container_addresses, &nameservers, isolate) - } - }; + error!("failed to parse {OPTION_ISOLATE} option: {e}"); + IsolateOption::Never + }); + + // Parse outbound addresses for teardown case + let outbound_addr4 = + parse_option::(&self.info.network.options, OPTION_OUTBOUND_ADDR4) + .unwrap_or_else(|e| { + error!("failed to parse {OPTION_OUTBOUND_ADDR4} option: {e}"); + None + }); + let outbound_addr6 = + parse_option::(&self.info.network.options, OPTION_OUTBOUND_ADDR6) + .unwrap_or_else(|e| { + error!("failed to parse {OPTION_OUTBOUND_ADDR6} option: {e}"); + None + }); + + (container_addresses, nameservers) = + match get_ipam_addresses(self.info.per_network_opts, self.info.network) { + Ok(i) => (i.container_addresses, i.nameservers), + Err(e) => { + // just log we still try to do as much as possible for cleanup + error!("failed to parse ipam options: {e}"); + (Vec::new(), Vec::new()) + } + }; + ( + &container_addresses, + &nameservers, + isolate, + outbound_addr4, + outbound_addr6, + ) + } + }; let (sn, spf) = self.get_firewall_conf( container_addresses_ref, nameservers_ref, isolate, bridge_name, + outbound_addr4, + outbound_addr6, )?; let tn = TearDownNetwork { diff --git a/src/network/constants.rs b/src/network/constants.rs index bf0324ca0..c6db6310a 100644 --- a/src/network/constants.rs +++ b/src/network/constants.rs @@ -24,6 +24,8 @@ pub const OPTION_BCLIM: &str = "bclim"; pub const OPTION_VRF: &str = "vrf"; pub const OPTION_VLAN: &str = "vlan"; pub const OPTION_HOST_INTERFACE_NAME: &str = "host_interface_name"; +pub const OPTION_OUTBOUND_ADDR4: &str = "outbound_addr4"; +pub const OPTION_OUTBOUND_ADDR6: &str = "outbound_addr6"; /// 100 is the default metric for most Linux networking tools. pub const DEFAULT_METRIC: u32 = 100; diff --git a/src/network/internal_types.rs b/src/network/internal_types.rs index 84d4322b8..47b389447 100644 --- a/src/network/internal_types.rs +++ b/src/network/internal_types.rs @@ -1,6 +1,6 @@ use super::netlink; use crate::network::types; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Teardown contains options for tearing down behind a container #[derive(Debug)] @@ -26,6 +26,14 @@ pub struct SetupNetwork { pub isolation: IsolateOption, /// port used for the dns server pub dns_port: u16, + /// outbound IPv4 address for SNAT + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub outbound_addr4: Option, + /// outbound IPv6 address for SNAT + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub outbound_addr6: Option, } #[derive(Debug)] diff --git a/test/100-bridge-iptables.bats b/test/100-bridge-iptables.bats index 68b245e38..0b7639ba1 100644 --- a/test/100-bridge-iptables.bats +++ b/test/100-bridge-iptables.bats @@ -1094,6 +1094,32 @@ function check_simple_bridge_iptables() { assert "${#lines[@]}" = 4 "too many NETAVARK_FORWARD rules" } +@test "$fw_driver - bridge with outbound addr4" { + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr4.json setup $(get_container_netns_path) + + # Check that the iptables rules were created with SNAT + run_in_host_netns iptables -t nat -S NETAVARK-F11DC6A6D09CF + assert "${lines[2]}" == "-A NETAVARK-F11DC6A6D09CF ! -d 224.0.0.0/4 -j SNAT --to-source 100.1.100.1" + + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr4.json teardown $(get_container_netns_path) + + # Check that the chain is removed + expected_rc=1 run_in_host_netns iptables -t nat -nvL NETAVARK-F11DC6A6D09CF +} + +@test "$fw_driver - bridge with outbound addr6" { + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr6.json setup $(get_container_netns_path) + + # Check that the ip6tables rules were created with SNAT + run_in_host_netns ip6tables -t nat -S NETAVARK-F11DC6A6D09CF + assert "${lines[2]}" == "-A NETAVARK-F11DC6A6D09CF ! -d ff00::/8 -j SNAT --to-source fd20:100:200::1" + + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr6.json teardown $(get_container_netns_path) + + # Check that the chain is removed + expected_rc=1 run_in_host_netns ip6tables -t nat -nvL NETAVARK-F11DC6A6D09CF +} + @test "$fw_driver - aardvark-dns error cleanup" { expected_rc=1 run_netavark -a /usr/bin/false --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json setup $(get_container_netns_path) assert_json ".error" "error while applying dns entries: aardvark-dns exited unexpectedly without error message" "aardvark-dns error" diff --git a/test/250-bridge-nftables.bats b/test/250-bridge-nftables.bats index 7d8f8a054..7f55631af 100644 --- a/test/250-bridge-nftables.bats +++ b/test/250-bridge-nftables.bats @@ -1052,6 +1052,32 @@ function check_simple_bridge_nftables() { assert "$output" == $'table inet netavark {\n\tchain NETAVARK-HOSTPORT-DNAT {\n\t}\n}' "NETAVARK-HOSTPORT-DNAT chain must be empty" } +@test "$fw_driver - bridge with outbound addr4" { + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr4.json setup $(get_container_netns_path) + + # Check that the nftables rules were created with SNAT + run_in_host_netns nft list chain inet netavark nv_2f259bab_10_89_0_0_nm24 + assert "${lines[3]}" =~ "ip daddr != 224.0.0.0/4 snat ip to 100.1.100.1" + + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr4.json teardown $(get_container_netns_path) + + # Check that the chain is removed + expected_rc=1 run_in_host_netns nft list chain inet netavark nv_2f259bab_10_89_0_0_nm24 +} + +@test "$fw_driver - bridge with outbound addr6" { + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr6.json setup $(get_container_netns_path) + + # Check that the nftables rules were created with SNAT for IPv6 + run_in_host_netns nft list chain inet netavark nv_2f259bab_fd10-88-a--_nm64 + assert "${lines[3]}" =~ "ip6 daddr != ff00::/8 snat ip6 to fd20:100:200::1" + + run_netavark --file ${TESTSDIR}/testfiles/bridge-outbound-addr6.json teardown $(get_container_netns_path) + + # Check that the chain is removed + expected_rc=1 run_in_host_netns nft list chain inet netavark nv_2f259bab_fd10-88-a--_nm64 +} + @test "$fw_driver - aardvark-dns error cleanup" { expected_rc=1 run_netavark -a /usr/bin/false --file ${TESTSDIR}/testfiles/dualstack-bridge-custom-dns-server.json setup $(get_container_netns_path) assert_json ".error" "error while applying dns entries: aardvark-dns exited unexpectedly without error message" "aardvark-dns error" diff --git a/test/testfiles/bridge-outbound-addr4.json b/test/testfiles/bridge-outbound-addr4.json new file mode 100644 index 000000000..f9d868cac --- /dev/null +++ b/test/testfiles/bridge-outbound-addr4.json @@ -0,0 +1,32 @@ +{ + "container_id": "someID", + "container_name": "someName", + "networks": { + "podman1": { + "interface_name": "eth0", + "static_ips": [ + "10.89.0.2" + ] + } + }, + "network_info": { + "podman1": { + "name": "podman1", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a9fc6", + "driver": "bridge", + "network_interface": "podman1", + "subnets": [ + { + "gateway": "10.89.0.1", + "subnet": "10.89.0.0/24" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "options": { + "outbound_addr4": "100.1.100.1" + } + } + } +} diff --git a/test/testfiles/bridge-outbound-addr6.json b/test/testfiles/bridge-outbound-addr6.json new file mode 100644 index 000000000..64943dae5 --- /dev/null +++ b/test/testfiles/bridge-outbound-addr6.json @@ -0,0 +1,32 @@ +{ + "container_id": "someID", + "container_name": "someName", + "networks": { + "podman1": { + "static_ips": [ + "fd10:88:a::2" + ], + "interface_name": "eth0" + } + }, + "network_info": { + "podman1": { + "name": "podman1", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a9fc6", + "driver": "bridge", + "network_interface": "podman1", + "subnets": [ + { + "gateway": "fd10:88:a::1", + "subnet": "fd10:88:a::/64" + } + ], + "ipv6_enabled": true, + "internal": false, + "dns_enabled": false, + "options": { + "outbound_addr6": "fd20:100:200::1" + } + } + } +}