102 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|