From 49b6f1d2c158669175b3351de6eee55ebd793141 Mon Sep 17 00:00:00 2001 From: Shivang K Raghuvanshi Date: Tue, 18 Nov 2025 18:11:56 +0530 Subject: [PATCH] firewall: flush stale UDP conntrack entries on port_forward setup/teardown Add a new netlink_netfilter module to interact with the kernel's conntrack table using the netlink_packet_netfilter crate. This module allows dumping and deleting conntrack entries. All firewall drivers now call the new flush_udp_conntrack() function during port forwarding setup and teardown. When a container with a UDP port mapping is started, stale conntrack entries can prevent traffic from reaching the new container instance. This change proactively deletes these stale entries for the mapped UDP ports, ensuring that new connections are not dropped by the kernel. Added an integration test for the same and unit tests for dump_conntrack and del_conntrack. Fixes: #1045 Signed-off-by: Shivang K Raghuvanshi --- Cargo.lock | 46 +++ Cargo.toml | 1 + src/network/bridge.rs | 24 +- src/network/mod.rs | 1 + src/network/netlink_netfilter.rs | 278 ++++++++++++++++++ src/test/netlink.rs | 171 ++++++++++- test/700-udp-traffic-flush.bats | 107 +++++++ .../bridge-udp-stale-conntrack-range.json | 32 ++ .../testfiles/bridge-udp-stale-conntrack.json | 32 ++ 9 files changed, 689 insertions(+), 3 deletions(-) create mode 100644 src/network/netlink_netfilter.rs create mode 100644 test/700-udp-traffic-flush.bats create mode 100644 test/testfiles/bridge-udp-stale-conntrack-range.json create mode 100644 test/testfiles/bridge-udp-stale-conntrack.json diff --git a/Cargo.lock b/Cargo.lock index aecc5f616..162287f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,6 +402,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -433,6 +439,19 @@ dependencies = [ "typenum", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -1066,6 +1085,7 @@ dependencies = [ "log", "mozim", "netlink-packet-core", + "netlink-packet-netfilter", "netlink-packet-route", "netlink-sys", "nftables", @@ -1095,6 +1115,17 @@ dependencies = [ "paste", ] +[[package]] +name = "netlink-packet-netfilter" +version = "0.2.0" +source = "git+https://github.com/shivkr6/netlink-packet-netfilter.git?branch=conntrack-new#41c8cb88fefb99981f80534b4ac353a383e367bc" +dependencies = [ + "bitflags", + "derive_more", + "libc", + "netlink-packet-core", +] + [[package]] name = "netlink-packet-route" version = "0.25.1" @@ -1526,6 +1557,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.2" @@ -1576,6 +1616,12 @@ dependencies = [ "syn", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 07a944263..651efa697 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ nix = { version = "0.30.1", features = ["net", "sched", "signal", "socket", "use rand = "0.9.2" sha2 = "0.10.9" netlink-packet-route = "0.25.1" +netlink-packet-netfilter = { git = "https://github.com/shivkr6/netlink-packet-netfilter.git", branch = "conntrack-new" } netlink-packet-core = "0.8.1" netlink-sys = "0.8.7" nftables = "0.6.3" diff --git a/src/network/bridge.rs b/src/network/bridge.rs index 34990c0cc..a395f3649 100644 --- a/src/network/bridge.rs +++ b/src/network/bridge.rs @@ -5,7 +5,6 @@ use std::{ os::fd::BorrowedFd, }; -use crate::dns::aardvark::SafeString; use crate::network::core_utils::get_default_route_interface; use crate::network::dhcp::{dhcp_teardown, get_dhcp_lease}; use crate::network::netlink::Socket; @@ -22,6 +21,7 @@ use crate::{ }, network::{constants, sysctl::disable_ipv6_autoconf, types}, }; +use crate::{dns::aardvark::SafeString, network::netlink_netfilter::flush_udp_conntrack}; use ipnet::IpNet; use log::{debug, error}; use netlink_packet_route::address::{AddressAttribute, AddressScope}; @@ -542,7 +542,18 @@ impl<'a> Bridge<'a> { self.info.firewall.setup_network(sn, &system_dbus)?; + let port_mappings = spf.port_mappings; + let container_ip_v4 = spf.container_ip_v4; + let container_ip_v6 = spf.container_ip_v6; + self.info.firewall.setup_port_forward(spf, &system_dbus)?; + + if let Some(port_mappings) = port_mappings { + // Flush stale UDP conntrack entries to prevent dropped packets. + // See the function's doc comment for more details. + flush_udp_conntrack(port_mappings, container_ip_v4, container_ip_v6)?; + } + Ok(()) } @@ -639,7 +650,18 @@ impl<'a> Bridge<'a> { complete_teardown, }; + let port_mappings = tpf.config.port_mappings; + let container_ip_v4 = tpf.config.container_ip_v4; + let container_ip_v6 = tpf.config.container_ip_v6; + self.info.firewall.teardown_port_forward(tpf)?; + + if let Some(port_mappings) = port_mappings { + // Flush stale UDP conntrack entries to prevent dropped packets. + // See the function's doc comment for more details. + flush_udp_conntrack(port_mappings, container_ip_v4, container_ip_v6)?; + } + Ok(()) } } diff --git a/src/network/mod.rs b/src/network/mod.rs index ed79a9f8c..dd96f65fd 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -18,6 +18,7 @@ pub mod driver; pub mod internal_types; pub mod netlink; +pub mod netlink_netfilter; pub mod netlink_route; pub mod plugin; diff --git a/src/network/netlink_netfilter.rs b/src/network/netlink_netfilter.rs new file mode 100644 index 000000000..c3d8ad7b2 --- /dev/null +++ b/src/network/netlink_netfilter.rs @@ -0,0 +1,278 @@ +use netlink_packet_netfilter::{ + conntrack::{ConntrackAttribute, IPTuple, ProtoTuple, Protocol, Tuple}, + NetfilterMessageInner, +}; +use std::{collections::HashSet, net::IpAddr, num::NonZeroI32}; + +use crate::{ + error::{ErrorWrap, NetavarkError, NetavarkResult}, + network::{ + netlink::{expect_netlink_result, function, NetlinkFamily, Socket}, + types::PortMapping, + }, +}; +use netlink_packet_core::{NLM_F_ACK, NLM_F_DUMP}; +use netlink_packet_netfilter::{ + conntrack::ConntrackMessage, NetfilterHeader, NetfilterMessage, ProtoFamily, +}; +use netlink_sys::protocols::NETLINK_NETFILTER; + +pub struct NetlinkNetfilter; + +impl NetlinkFamily for NetlinkNetfilter { + const PROTOCOL: isize = NETLINK_NETFILTER; + type Message = NetfilterMessage; +} + +// Represents the 5-tuple for a single direction of a connection flow. +#[derive(Clone)] +pub struct FlowTuple { + pub src_ip: IpAddr, + pub dst_ip: IpAddr, + pub src_port: u16, + pub dst_port: u16, + pub protocol: Protocol, +} +impl FlowTuple { + pub fn is_ipv6(&self) -> bool { + self.src_ip.is_ipv6() && self.dst_ip.is_ipv6() + } +} + +// Represents a conntrack entry, detailing the original and reply flows. +#[derive(Clone)] +pub struct ConntrackFlow { + pub origin: Option, + pub reply: Option, +} + +// Converts a `ConntrackFlow` into a vector of `ConntrackAttribute` +// attributes suitable for a netlink message payload. +impl ConntrackFlow { + fn to_attributes(self: ConntrackFlow) -> Vec { + fn to_tuple_attributes(flow_tuple: &FlowTuple) -> Vec { + let ip_attributes = vec![ + IPTuple::SourceAddress(flow_tuple.src_ip), + IPTuple::DestinationAddress(flow_tuple.dst_ip), + ]; + + let proto_attributes = vec![ + ProtoTuple::Protocol(flow_tuple.protocol), + ProtoTuple::SourcePort(flow_tuple.src_port), + ProtoTuple::DestinationPort(flow_tuple.dst_port), + ]; + vec![Tuple::Ip(ip_attributes), Tuple::Proto(proto_attributes)] + } + + let mut attributes = Vec::new(); + + if let Some(ref origin_flow) = self.origin { + let origin_attributes = to_tuple_attributes(origin_flow); + if !origin_attributes.is_empty() { + attributes.push(ConntrackAttribute::CtaTupleOrig(origin_attributes)); + } + } + + if let Some(ref reply_flow) = self.reply { + let reply_attributes = to_tuple_attributes(reply_flow); + if !reply_attributes.is_empty() { + attributes.push(ConntrackAttribute::CtaTupleReply(reply_attributes)); + } + } + + attributes + } + fn is_ipv6(&self) -> NetavarkResult { + match (&self.origin, &self.reply) { + (Some(origin), Some(reply)) => { + let origin_is_v6 = origin.is_ipv6(); + + if origin_is_v6 != reply.is_ipv6() { + Err(NetavarkError::Message( + "Mismatched IP families in conntrack flow".to_string(), + )) + } else { + Ok(origin_is_v6) + } + } + + (Some(origin), None) => Ok(origin.is_ipv6()), + + (None, Some(reply)) => Ok(reply.is_ipv6()), + + (None, None) => Err(NetavarkError::Message( + "There needs to be atleast one FlowTuple in a conntrack flow".to_string(), + )), + } + } +} + +impl Socket { + pub fn dump_conntrack(&mut self) -> NetavarkResult> { + let msg: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(ProtoFamily::ProtoUnspec, 0, 0), + ConntrackMessage::Get(vec![]), + ); + + let result = self.make_netlink_request(msg, NLM_F_DUMP)?; + + Ok(result) + } + + pub fn del_conntrack(&mut self, flow: ConntrackFlow) -> NetavarkResult<()> { + let is_v6 = flow.is_ipv6()?; + let proto_family = if is_v6 { + ProtoFamily::ProtoIPv6 + } else { + ProtoFamily::ProtoIPv4 + }; + let msg: NetfilterMessage = NetfilterMessage::new( + NetfilterHeader::new(proto_family, 0, 0), + ConntrackMessage::Delete(flow.to_attributes()), + ); + + let result = self.make_netlink_request(msg, NLM_F_ACK)?; + expect_netlink_result!(result, 0); + + Ok(()) + } +} + +/// This function addresses an issue where UDP traffic is dropped due to stale conntrack entries. +/// +/// To solve this, we proactively flush any conntrack entries associated with the mapped UDP +/// host ports before the network is fully set up. This ensures the kernel creates a fresh, +/// correct entry for the new service instance. +/// +/// Fixes: https://github.com/containers/netavark/issues/1045 +pub fn flush_udp_conntrack( + port_mappings: &[PortMapping], + container_ipv4: Option, + container_ipv6: Option, +) -> NetavarkResult<()> { + let mut host_ports_to_flush = HashSet::::new(); + for pm in port_mappings.iter().filter(|pm| pm.protocol == "udp") { + for port in pm.host_port..(pm.host_port + pm.range) { + host_ports_to_flush.insert(port); + } + } + + if host_ports_to_flush.is_empty() && container_ipv4.is_none() && container_ipv6.is_none() { + return Ok(()); + } + + let mut ct_socket = Socket::::new().wrap("conntrack netlink socket")?; + let conntrack_dump = ct_socket.dump_conntrack()?; + + for msg in &conntrack_dump { + if let Some(flow) = parse_ct_new_msg(msg) { + if matches_flow(&flow, &host_ports_to_flush, container_ipv4, container_ipv6) { + match ct_socket.del_conntrack(flow) { + Ok(_) => {} + // We iterate over a dump of entries. Between the time we dump the table + // and the time we attempt the delete, the entry might have naturally + // expired. Treating ENOENT as success ensures we don't fail the setup + // just because the cleanup happened automatically. + Err(NetavarkError::Netlink(e)) if e.code == NonZeroI32::new(-libc::ENOENT) => { + log::debug!("Conntrack entry already deleted, skipping"); + } + Err(e) => return Err(e), + } + } + } + } + + Ok(()) +} + +fn matches_flow( + flow: &ConntrackFlow, + ports: &HashSet, + ipv4: Option, + ipv6: Option, +) -> bool { + if let Some(origin) = &flow.origin { + if origin.protocol == Protocol::Udp && ports.contains(&origin.dst_port) { + return true; + } + } + if let Some(reply) = &flow.reply { + let is_match = |ip| ipv4 == Some(ip) || ipv6 == Some(ip); + if is_match(reply.src_ip) || is_match(reply.dst_ip) { + return true; + } + } + false +} + +/// parses a conntrack new netfilter message into a ConntrackFlow struct +pub fn parse_ct_new_msg(msg: &NetfilterMessage) -> Option { + let attributes = match &msg.inner { + NetfilterMessageInner::Conntrack(ConntrackMessage::New(attr)) => attr, + _ => return None, + }; + + let mut origin_tuples = None; + let mut reply_tuples = None; + + for nla in attributes { + match nla { + ConntrackAttribute::CtaTupleOrig(tuples) => origin_tuples = Some(tuples.as_slice()), + ConntrackAttribute::CtaTupleReply(tuples) => reply_tuples = Some(tuples.as_slice()), + _ => {} + } + + if origin_tuples.is_some() && reply_tuples.is_some() { + break; + } + } + + let origin_flow = parse_tuples_to_flow(origin_tuples?)?; + let reply_flow = parse_tuples_to_flow(reply_tuples?)?; + + Some(ConntrackFlow { + origin: Some(origin_flow), + reply: Some(reply_flow), + }) +} + +fn parse_tuples_to_flow(tuples: &[Tuple]) -> Option { + let mut src_ip = None; + let mut dst_ip = None; + let mut src_port = None; + let mut dst_port = None; + let mut protocol_from_tuple = None; + + for tuple in tuples { + match tuple { + Tuple::Ip(ip_tuples) => { + for ip_tuple in ip_tuples { + match ip_tuple { + IPTuple::SourceAddress(ip) => src_ip = Some(*ip), + IPTuple::DestinationAddress(ip) => dst_ip = Some(*ip), + _ => (), + } + } + } + Tuple::Proto(proto_tuples) => { + for proto_tuple in proto_tuples { + match proto_tuple { + ProtoTuple::Protocol(p) => protocol_from_tuple = Some(*p), + ProtoTuple::SourcePort(p) => src_port = Some(*p), + ProtoTuple::DestinationPort(p) => dst_port = Some(*p), + _ => (), + } + } + } + _ => (), + } + } + + Some(FlowTuple { + src_ip: src_ip?, + dst_ip: dst_ip?, + src_port: src_port?, + dst_port: dst_port?, + protocol: protocol_from_tuple?, + }) +} diff --git a/src/test/netlink.rs b/src/test/netlink.rs index 85907c7dc..d1b66dabe 100644 --- a/src/test/netlink.rs +++ b/src/test/netlink.rs @@ -1,10 +1,14 @@ #[cfg(test)] mod tests { - use std::net::{IpAddr, Ipv4Addr}; - use netavark::network::netlink::Socket; + use netavark::network::netlink_netfilter::{ + parse_ct_new_msg, ConntrackFlow, FlowTuple, NetlinkNetfilter, + }; use netavark::network::netlink_route::{CreateLinkOptions, LinkID, NetlinkRoute, Route}; + use netlink_packet_netfilter::conntrack::Protocol; + use netlink_packet_route::{address, link::InfoKind}; + use std::net::{IpAddr, Ipv4Addr}; macro_rules! test_setup { () => { @@ -225,4 +229,167 @@ mod tests { } } } + + #[test] + fn test_dump_conntrack() { + test_setup!(); + let mut sock = Socket::::new().expect("Socket::new()"); + + let tcp_src_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)); + let tcp_dst_ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)); + let tcp_src_port: u16 = 12345; + let tcp_dst_port: u16 = 80; + + let udp_src_ip = IpAddr::V4(Ipv4Addr::new(172, 16, 30, 5)); + let udp_dst_ip = IpAddr::V4(Ipv4Addr::new(8, 8, 4, 4)); + let udp_src_port: u16 = 49152; + let udp_dst_port: u16 = 53; + + let out = run_command!( + "conntrack", + "-I", + "-p", + "tcp", + "--src", + &tcp_src_ip.to_string(), + "--dst", + &tcp_dst_ip.to_string(), + "--sport", + &tcp_src_port.to_string(), + "--dport", + &tcp_dst_port.to_string(), + "--state", + "SYN_SENT", + "--timeout", + "60" + ); + assert!(out.status.success(), "failed to add TCP conntrack entry"); + + let out = run_command!( + "conntrack", + "-I", + "-p", + "udp", + "--src", + &udp_src_ip.to_string(), + "--dst", + &udp_dst_ip.to_string(), + "--sport", + &udp_src_port.to_string(), + "--dport", + &udp_dst_port.to_string(), + "--timeout", + "30" + ); + assert!(out.status.success(), "failed to add UDP conntrack entry"); + + let msgs = sock.dump_conntrack().expect("dump_conntrack failed"); + assert!(msgs.len() == 2, "Expected two conntrack entries"); + + let mut found_tcp_flow = false; + let mut found_udp_flow = false; + + for msg in &msgs { + if let Some(flow) = parse_ct_new_msg(msg) { + let origin = flow.origin.as_ref().unwrap(); + + match origin.protocol { + Protocol::Tcp if origin.dst_port == tcp_dst_port => { + assert_eq!(origin.src_ip, tcp_src_ip, "TCP origin source IP mismatch"); + assert_eq!( + origin.src_port, tcp_src_port, + "TCP origin source port mismatch" + ); + found_tcp_flow = true; + } + Protocol::Udp if origin.dst_port == udp_dst_port => { + assert_eq!(origin.src_ip, udp_src_ip, "UDP origin source IP mismatch"); + assert_eq!( + origin.src_port, udp_src_port, + "UDP origin source port mismatch" + ); + found_udp_flow = true; + } + _ => (), + } + } + } + + assert!( + found_tcp_flow, + "Did not find the expected TCP conntrack flow in the dump" + ); + assert!( + found_udp_flow, + "Did not find the expected UDP conntrack flow in the dump" + ); + } + + #[test] + fn test_del_conntrack() { + test_setup!(); + let mut sock = Socket::::new().expect("Socket::new()"); + + let src_ip = "192.168.1.100"; + let src_port = "12345"; + let dst_ip = "10.0.0.1"; + let dst_port = "80"; + + let out = run_command!( + "conntrack", + "-I", + "-p", + "tcp", + "--src", + src_ip, + "--dst", + dst_ip, + "--sport", + src_port, + "--dport", + dst_port, + "--state", + "SYN_SENT", + "--timeout", + "60" + ); + eprintln!("{}", String::from_utf8_lossy(&out.stderr)); + assert!( + out.status.success(), + "failed to add conntrack entry via conntrack-tools" + ); + + let conntrack_flow = ConntrackFlow { + origin: Some(FlowTuple { + src_ip: IpAddr::V4(src_ip.parse().unwrap()), + dst_ip: IpAddr::V4(dst_ip.parse().unwrap()), + src_port: src_port.parse().unwrap(), + dst_port: dst_port.parse().unwrap(), + protocol: Protocol::Tcp, + }), + reply: None, + }; + + sock.del_conntrack(conntrack_flow) + .expect("del_conntrack failed"); + + let out = run_command!( + "conntrack", + "-G", + "-p", + "tcp", + "--src", + src_ip, + "--dst", + dst_ip, + "--sport", + src_port, + "--dport", + dst_port + ); + assert!( + !out.status.success(), + "got deleted conntrack entry via conntrack-tools, i.e., deleting unsuccessful" + ); + } } diff --git a/test/700-udp-traffic-flush.bats b/test/700-udp-traffic-flush.bats new file mode 100644 index 000000000..f94a989a4 --- /dev/null +++ b/test/700-udp-traffic-flush.bats @@ -0,0 +1,107 @@ +#!/usr/bin/env bats -*- bats -*- +# +# Regression test for flushing stale conntrack udp entries https://github.com/containers/netavark/issues/1045 +# + +load helpers + +function start_sticky_udp_spammer() { + local target_ip=$1 + local target_port=$2 + local message=$3 + + cat << EOF > "$NETAVARK_TMPDIR/spammer.sh" +#!/bin/bash +# Open UDP connection to target on FD 3. +# Keeps Source Port constant. +exec 3<>/dev/udp/$target_ip/$target_port +while true; do + echo "$message" >&3 + sleep 0.1 +done +EOF + chmod +x "$NETAVARK_TMPDIR/spammer.sh" + run_in_host_netns "$NETAVARK_TMPDIR/spammer.sh" & + SPAMMER_PID=$! +} + +function run_ct_udp_flush_test() { + local config_file=$1 + local target_ip=$2 + local target_port=$3 + local container_port=$4 + + local msg="payload_$$" + + start_sticky_udp_spammer "$target_ip" "$target_port" "$msg" + + # avoid race condition and allow conntrack entry creation + sleep 1 + + run_in_host_netns conntrack -L + assert "$output" =~ "dst=${target_ip}.*dport=${target_port}" "Conntrack flow matches target IP and Port" + + run_netavark --file "$config_file" setup $(get_container_netns_path) + + run_in_host_netns conntrack -L + assert "$output" !~ "dst=${target_ip}.*dport=${target_port}" "Conntrack entry should NOT exist" + + run_in_container_netns timeout 1 sh -c "socat -u UDP4-LISTEN:$container_port STDOUT | head -n 1" + + local container_output="$output" + + kill $SPAMMER_PID + wait $SPAMMER_PID || true + run_netavark --file "$config_file" teardown $(get_container_netns_path) + + assert "$container_output" =~ "$msg" "received proper payload" +} + +@test "nftables: receive udp traffic with pre-existing stale conntrack entry" { + export NETAVARK_FW="nftables" + # Explicitly add a rule to trigger connection tracking. + # This ensures the traffic generated by the spammer creates a conntrack entry that + # Netavark must flush. Without this, the kernel might not track the flow, preventing + # the reproduction of the stale entry issue (causing a false positive test pass). + run_in_host_netns nft -f - << EOF +add table inet bats { + chain output { + type filter hook output priority 0; policy accept; + udp dport 10080 ct state new + } +} +EOF + run_ct_udp_flush_test "${TESTSDIR}/testfiles/bridge-udp-stale-conntrack.json" "127.0.0.1" "10080" "8080" +} + +@test "nftables: receive udp traffic with pre-existing stale conntrack entry (range)" { + export NETAVARK_FW="nftables" + # Explicitly add a rule to trigger connection tracking for the range 10080-10081 + run_in_host_netns nft -f - << EOF +add table inet bats { + chain output { + type filter hook output priority 0; policy accept; + udp dport 10080-10081 ct state new + } +} +EOF + run_ct_udp_flush_test "${TESTSDIR}/testfiles/bridge-udp-stale-conntrack-range.json" "127.0.0.1" "10081" "8081" +} + +@test "firewalld: receive udp traffic with pre-existing stale conntrack entry" { + setup_firewalld + export NETAVARK_FW="firewalld" + # Explicitly add a rule to trigger connection tracking. + run_in_host_netns firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p udp -m state --state NEW -j ACCEPT + run_ct_udp_flush_test "${TESTSDIR}/testfiles/bridge-udp-stale-conntrack.json" "127.0.0.1" "10080" "8080" +} + +@test "firewalld: receive udp traffic with pre-existing stale conntrack entry (range)" { + skip "port forwarding range seems to be broken with firewalld. All the traffic gets redirected to the first container port no matter which host range port you pick" + + setup_firewalld + export NETAVARK_FW="firewalld" + # Explicitly add a rule to trigger connection tracking. + run_in_host_netns firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p udp -m state --state NEW -j ACCEPT + run_ct_udp_flush_test "${TESTSDIR}/testfiles/bridge-udp-stale-conntrack-range.json" "127.0.0.1" "10081" "8081" +} diff --git a/test/testfiles/bridge-udp-stale-conntrack-range.json b/test/testfiles/bridge-udp-stale-conntrack-range.json new file mode 100644 index 000000000..20b1908ca --- /dev/null +++ b/test/testfiles/bridge-udp-stale-conntrack-range.json @@ -0,0 +1,32 @@ +{ + "container_id": "udp_traffic_container", + "container_name": "udp_traffic_test", + "port_mappings": [ + { + "host_ip": "127.0.0.1", + "container_port": 8080, + "host_port": 10080, + "range": 2, + "protocol": "udp" + } + ], + "networks": { + "podman": { + "static_ips": [ "10.88.0.20" ], + "interface_name": "eth0" + } + }, + "network_info": { + "podman": { + "name": "podman", + "id": "udp_traffic_id", + "driver": "bridge", + "network_interface": "podman0", + "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], + "ipam_options": { "driver": "host-local" }, + "dns_enabled": false, + "ipv6_enabled": false, + "internal": false + } + } +} diff --git a/test/testfiles/bridge-udp-stale-conntrack.json b/test/testfiles/bridge-udp-stale-conntrack.json new file mode 100644 index 000000000..b3a39d026 --- /dev/null +++ b/test/testfiles/bridge-udp-stale-conntrack.json @@ -0,0 +1,32 @@ +{ + "container_id": "udp_traffic_container", + "container_name": "udp_traffic_test", + "port_mappings": [ + { + "host_ip": "127.0.0.1", + "container_port": 8080, + "host_port": 10080, + "range": 1, + "protocol": "udp" + } + ], + "networks": { + "podman": { + "static_ips": [ "10.88.0.20" ], + "interface_name": "eth0" + } + }, + "network_info": { + "podman": { + "name": "podman", + "id": "udp_traffic_id", + "driver": "bridge", + "network_interface": "podman0", + "subnets": [ { "subnet": "10.88.0.0/16", "gateway": "10.88.0.1" } ], + "ipam_options": { "driver": "host-local" }, + "dns_enabled": false, + "ipv6_enabled": false, + "internal": false + } + } +}