go-firewall/container_test.go
2026-08-10 17:17:03 -05:00

102 lines
4.7 KiB
Go

package firewall
import (
"testing"
"github.com/stretchr/testify/require"
)
// The detector must catch every interface a container runtime allocates without
// swallowing an operator's own interfaces, which are managed normally. The
// br-<12 hex> case is the sharp edge: Docker's generated bridges must match while
// a hand-named br-lan must not.
func TestIsContainerRuntimeIface(t *testing.T) {
runtime := []string{
"docker0", "docker_gwbridge", "br-1a2b3c4d5e6f",
"podman0", "podman1", "cni-podman0",
"cni0", "flannel.1", "cilium_host", "weave", "kube-ipvs0",
"cali1234567890a", "lxc00aa",
"!docker0", "cali+", // Negated and wildcard forms still identify the runtime.
}
for _, name := range runtime {
require.True(t, isContainerRuntimeIface(name), "%q must be detected as a container-runtime interface", name)
}
operator := []string{
"", "eth0", "ens3", "wlan0", "bridge0",
"br-1a2b3c4d5e6", // Too short for a Docker network ID.
"br-1A2B3C4D5E6F", // Docker lowercases its hex; an uppercase name is not its.
"dockerish0", // Not an interface Docker creates.
// An operator's bridge that happens to be 12 hex characters. A generated
// network ID practically always carries a numeral, so requiring one keeps
// names spelled in hex letters managed.
"br-deadbeefcafe", "br-cafedecadeff",
"br-1a2b3c4d5e6f7", // One character too long for a network ID.
"br-lan", "br-wan", "br-100", "vmbr0", "virbr0", "lxdbr0",
// A veth pair is not a container-runtime signal on its own: systemd-nspawn,
// libvirt and hand-built netns setups all use them, and a runtime's own rules
// match the bridge rather than the container-side leg.
"veth1a2b3c", "vethwe-bridge",
"tun0", "wg0", "bond0", "lo",
}
for _, name := range operator {
require.False(t, isContainerRuntimeIface(name), "%q is the operator's interface and must stay managed", name)
}
}
func TestIsContainerRuntimeChain(t *testing.T) {
runtime := []string{
"DOCKER", "DOCKER-USER", "DOCKER-ISOLATION-STAGE-1", "DOCKER-INGRESS",
"DOCKER-FORWARD", "DOCKER-BRIDGE", "DOCKER-CT", "DOCKER-INTERNAL",
"NETAVARK_FORWARD", "NETAVARK-ISOLATION-2", "NETAVARK-HOSTPORT-DNAT",
"CNI-FORWARD", "CNI-ADMIN",
"KUBE-SERVICES", "KUBE-NODEPORTS", "KUBE-FIREWALL",
"CALI-INPUT", "cali-fw-cali123", "CILIUM_POST_mangle", "FLANNEL-FWD", "WEAVE",
}
for _, name := range runtime {
require.True(t, isContainerRuntimeChain(name), "%q must be detected as a container-runtime chain", name)
}
operator := []string{"", "INPUT", "OUTPUT", "FORWARD", "ufw-user-input", "f2b-sshd", "DOCKERISH"}
for _, name := range operator {
require.False(t, isContainerRuntimeChain(name), "%q must stay managed", name)
}
}
// Docker's native nftables backend names its chains generically
// (filter-forward-in, nat-postrouting-out), so the table name is the only signal.
// The listing form carries the family, which must be tolerated.
func TestIsContainerRuntimeTable(t *testing.T) {
for _, name := range []string{"docker-bridges", "ip docker-bridges", "ip6 docker-bridges", "inet netavark", "ip kube-proxy"} {
require.True(t, isContainerRuntimeTable(name), "%q must be detected as a container-runtime table", name)
}
for _, name := range []string{"", "inet filter", "ip nat", "inet go-firewall", "filter"} {
require.False(t, isContainerRuntimeTable(name), "%q must stay managed", name)
}
}
// Docker and podman add a hairpin masquerade per published port, and it carries
// no interface match, so shape is the only signal. It must not swallow an
// operator's ordinary masquerade or a whole-subnet self-masquerade.
func TestIsHairpinMasquerade(t *testing.T) {
hairpin := []*NATRule{
{Kind: Masquerade, Source: "172.17.0.2/32", Destination: "172.17.0.2/32", Proto: TCP, Port: 80},
{Kind: Masquerade, Source: "10.89.0.4", Destination: "10.89.0.4/32", Proto: UDP, Port: 53},
{Kind: Masquerade, Source: "fd00::2/128", Destination: "fd00::2/128", Proto: TCP, Port: 443},
}
for _, r := range hairpin {
require.True(t, r.isContainerRuntime(), "hairpin masquerade %s must be out of scope", r.Source)
}
managed := []*NATRule{
{Kind: Masquerade, Interface: "eth0"}, // Ordinary egress NAT.
{Kind: Masquerade, Source: "10.0.0.0/24", Destination: "10.0.0.0/24"}, // A subnet, not a host.
{Kind: Masquerade, Source: "10.0.0.5/32", Destination: "10.0.0.6/32"}, // Different hosts.
{Kind: Masquerade, Source: "10.0.0.5/32"}, // No destination.
{Kind: SNAT, Source: "10.0.0.5/32", Destination: "10.0.0.5/32", ToAddress: "203.0.113.1"},
{Kind: DNAT, Destination: "203.0.113.1/32", ToAddress: "10.0.0.5"},
}
for _, r := range managed {
require.False(t, r.isContainerRuntime(), "%+v is the operator's NAT rule and must stay managed", *r)
}
}