Files
flock/pkg/agent/netpol/translator_test.go
Donavan Fritz 39ede9130b
Build flock Image / build (push) Has been cancelled
netpol: NetworkPolicy v1 enforcement via nftables
New pkg/agent/netpol implementing standard networking.k8s.io/v1
NetworkPolicy. Pipeline:

  pods + policies + namespaces  →  Translate  →  Render  →  Apply

Supports ingress + egress, all three peer types (podSelector,
namespaceSelector, ipBlock with except), numeric ports + port ranges,
default-deny semantics derived from PolicyTypes (or inferred from
non-empty Spec.Egress when unset).

Apply path is `nft -f -` shell-out — single transaction, atomic, kernel
guarantees partial-failure rollback. Idempotent dedup via last-applied
script. Reconcile triggers: informer events, 30s self-heal tick, every
CNI ADD/DEL.

Verified against the three live cluster NetPols (calico-apiserver,
remote-proxies/lodge-home-assistant, storage/garage-admin-restrict).
Fuzz target stitches Translate + Render with random selector and peer
inputs; 21 unit tests cover the policy semantics.

Named ports skip with a warn — deferred until kubelet exposes them in a
form that doesn't require shadowing pod state.

Dockerfile: + nftables.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 09:25:58 -05:00

453 lines
15 KiB
Go

package netpol
import (
"net"
"testing"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func mustIP(s string) net.IP {
ip := net.ParseIP(s)
if ip == nil {
panic("bad IP: " + s)
}
return ip
}
func newPolicy(ns, name string, mods ...func(*netv1.NetworkPolicy)) netv1.NetworkPolicy {
p := netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name},
Spec: netv1.NetworkPolicySpec{},
}
for _, m := range mods {
m(&p)
}
return p
}
func tcpPort(port int) netv1.NetworkPolicyPort {
proto := corev1.ProtocolTCP
p := intstr.FromInt32(int32(port))
return netv1.NetworkPolicyPort{Protocol: &proto, Port: &p}
}
// Pod-only selector that matches everything (`{}`).
func emptySelector() *metav1.LabelSelector {
return &metav1.LabelSelector{}
}
func selectorMatching(kv map[string]string) *metav1.LabelSelector {
return &metav1.LabelSelector{MatchLabels: kv}
}
// Helper: collect Isolated keys for the given pod into a string list.
func isolationFor(out Output, podKey string) (in, eg bool) {
if _, ok := out.Isolated[Isolation{PodKey: podKey, Direction: DirIngress}]; ok {
in = true
}
if _, ok := out.Isolated[Isolation{PodKey: podKey, Direction: DirEgress}]; ok {
eg = true
}
return
}
// TestTranslate_NoPolicies — pod with no matching policy is unrestricted.
func TestTranslate_NoPolicies(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "p1",
Labels: map[string]string{"app": "web"},
HostIface: "flock00000001",
IPs: []net.IP{mustIP("2001:db8::1")},
}
out, err := Translate(Inputs{LocalPods: []Pod{pod}}, nil)
if err != nil {
t.Fatal(err)
}
if len(out.Rules) != 0 {
t.Fatalf("expected no rules, got %d", len(out.Rules))
}
in, eg := isolationFor(out, "ns1/p1")
if in || eg {
t.Fatalf("pod should not be isolated: in=%v eg=%v", in, eg)
}
}
// TestTranslate_DefaultDeny — a policy with empty Ingress + PolicyTypes
// = [Ingress] selects the pod and isolates it; no allow rules emitted.
func TestTranslate_DefaultDenyIngress(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web",
Labels: map[string]string{"app": "web"},
HostIface: "flock00000001",
IPs: []net.IP{mustIP("2001:db8::1")},
}
policy := newPolicy("ns1", "default-deny", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
})
out, err := Translate(Inputs{
LocalPods: []Pod{pod},
Policies: []netv1.NetworkPolicy{policy},
}, nil)
if err != nil {
t.Fatal(err)
}
if len(out.Rules) != 0 {
t.Fatalf("expected no rules from a deny-all, got %d", len(out.Rules))
}
in, eg := isolationFor(out, "ns1/web")
if !in {
t.Fatalf("ingress should be isolated")
}
if eg {
t.Fatalf("egress should NOT be isolated (policy only set ingress)")
}
}
// TestTranslate_DefaultDenyEgress_InferredFromEgressList — when
// PolicyTypes is omitted but Spec.Egress is non-empty, egress should
// also be isolated by inference.
func TestTranslate_DefaultDenyEgress_InferredFromEgressList(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web",
Labels: map[string]string{"app": "web"},
HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")},
}
policy := newPolicy("ns1", "egress-rule", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.Egress = []netv1.NetworkPolicyEgressRule{{}}
})
out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil)
in, eg := isolationFor(out, "ns1/web")
if !in || !eg {
t.Fatalf("both directions should be isolated: in=%v eg=%v", in, eg)
}
}
// TestTranslate_PodSelectorPeer_SameNamespace — peer is a single pod in
// the same namespace, identified by label.
func TestTranslate_PodSelectorPeer(t *testing.T) {
web := Pod{
Namespace: "ns1", Name: "web",
Labels: map[string]string{"app": "web"},
HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")},
}
clientIP := mustIP("2001:db8::2")
peer := PeerPod{
Namespace: "ns1", Name: "client",
Labels: map[string]string{"app": "client"},
IPs: []net.IP{clientIP},
}
policy := newPolicy("ns1", "allow-from-client", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *selectorMatching(map[string]string{"app": "web"})
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
PodSelector: selectorMatching(map[string]string{"app": "client"}),
}},
Ports: []netv1.NetworkPolicyPort{tcpPort(80)},
}}
})
out, err := Translate(Inputs{
LocalPods: []Pod{web},
PeerPods: []PeerPod{peer},
Policies: []netv1.NetworkPolicy{policy},
}, nil)
if err != nil {
t.Fatal(err)
}
if len(out.Rules) != 1 {
t.Fatalf("expected 1 rule, got %d: %+v", len(out.Rules), out.Rules)
}
r := out.Rules[0]
if r.PodKey != "ns1/web" || r.Direction != DirIngress {
t.Fatalf("rule has wrong subject: %+v", r)
}
if len(r.PeerCIDRs) != 1 || !r.PeerCIDRs[0].IP.Equal(clientIP) {
t.Fatalf("peer CIDR wrong: %+v", r.PeerCIDRs)
}
if len(r.Ports) != 1 || r.Ports[0].Protocol != "tcp" || r.Ports[0].Port != 80 {
t.Fatalf("port wrong: %+v", r.Ports)
}
}
// TestTranslate_NamespaceSelector — peer is "every pod in any namespace
// with label tier=trusted".
func TestTranslate_NamespaceSelector(t *testing.T) {
web := Pod{
Namespace: "ns1", Name: "web",
Labels: map[string]string{"app": "web"},
HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")},
}
out, err := Translate(Inputs{
LocalPods: []Pod{web},
Namespaces: []Namespace{
{Name: "ns1", Labels: map[string]string{}},
{Name: "trusted-1", Labels: map[string]string{"tier": "trusted"}},
{Name: "trusted-2", Labels: map[string]string{"tier": "trusted"}},
{Name: "untrusted", Labels: map[string]string{"tier": "wild"}},
},
PeerPods: []PeerPod{
{Namespace: "trusted-1", Name: "a", IPs: []net.IP{mustIP("2001:db8::a")}},
{Namespace: "trusted-2", Name: "b", IPs: []net.IP{mustIP("2001:db8::b")}},
{Namespace: "untrusted", Name: "x", IPs: []net.IP{mustIP("2001:db8::ff")}},
},
Policies: []netv1.NetworkPolicy{newPolicy("ns1", "allow-trusted", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
NamespaceSelector: selectorMatching(map[string]string{"tier": "trusted"}),
}},
}}
})},
}, nil)
if err != nil {
t.Fatal(err)
}
if len(out.Rules) != 1 {
t.Fatalf("expected 1 rule, got %d", len(out.Rules))
}
got := map[string]bool{}
for _, c := range out.Rules[0].PeerCIDRs {
got[c.IP.String()] = true
}
if !got["2001:db8::a"] || !got["2001:db8::b"] {
t.Fatalf("trusted pod IPs missing: %v", got)
}
if got["2001:db8::ff"] {
t.Fatalf("untrusted pod IP leaked into rule")
}
}
// TestTranslate_IPBlockWithExcept — ipBlock with an except range.
func TestTranslate_IPBlockWithExcept(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("10.0.0.1")},
}
policy := newPolicy("ns1", "ipblock", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
IPBlock: &netv1.IPBlock{
CIDR: "10.0.0.0/8",
Except: []string{"10.99.0.0/16", "10.42.42.0/24"},
},
}},
}}
})
out, err := Translate(Inputs{
LocalPods: []Pod{pod},
Policies: []netv1.NetworkPolicy{policy},
}, nil)
if err != nil {
t.Fatal(err)
}
if len(out.Rules) != 1 {
t.Fatalf("expected 1 rule, got %d", len(out.Rules))
}
r := out.Rules[0]
if len(r.PeerCIDRs) != 1 || r.PeerCIDRs[0].String() != "10.0.0.0/8" {
t.Fatalf("peer CIDR wrong: %v", r.PeerCIDRs)
}
if len(r.PeerExcept) != 2 {
t.Fatalf("expected 2 except, got %d", len(r.PeerExcept))
}
}
// TestTranslate_AllowAllPeers — empty From list means "from anywhere".
func TestTranslate_AllowAllPeers(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("2001:db8::1")},
}
policy := newPolicy("ns1", "allow-all-on-port", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
Ports: []netv1.NetworkPolicyPort{tcpPort(443)},
}}
})
out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil)
if len(out.Rules) != 1 {
t.Fatalf("expected 1 rule, got %d", len(out.Rules))
}
r := out.Rules[0]
if len(r.PeerCIDRs) != 0 || len(r.PeerExcept) != 0 {
t.Fatalf("expected allow-all peers, got CIDRs=%v Except=%v", r.PeerCIDRs, r.PeerExcept)
}
}
// TestTranslate_AllowAllPorts — empty Ports list means "all ports".
func TestTranslate_AllowAllPorts(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("2001:db8::1")},
}
policy := newPolicy("ns1", "allow-from-all", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
PodSelector: emptySelector(),
}},
}}
})
peer := PeerPod{
Namespace: "ns1", Name: "x",
IPs: []net.IP{mustIP("2001:db8::aa")},
}
out, _ := Translate(Inputs{
LocalPods: []Pod{pod}, PeerPods: []PeerPod{peer},
Policies: []netv1.NetworkPolicy{policy},
}, nil)
if len(out.Rules) != 1 {
t.Fatalf("expected 1 rule, got %d", len(out.Rules))
}
r := out.Rules[0]
if len(r.Ports) != 1 || r.Ports[0] != (PortMatch{}) {
t.Fatalf("expected single any-port match, got %+v", r.Ports)
}
}
// TestTranslate_PortRange — endPort field.
func TestTranslate_PortRange(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("2001:db8::1")},
}
policy := newPolicy("ns1", "range", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
proto := corev1.ProtocolTCP
port := intstr.FromInt32(8000)
end := int32(8999)
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
Ports: []netv1.NetworkPolicyPort{{Protocol: &proto, Port: &port, EndPort: &end}},
}}
})
out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil)
if len(out.Rules) != 1 || out.Rules[0].Ports[0].Port != 8000 || out.Rules[0].Ports[0].EndPort != 8999 {
t.Fatalf("range not preserved: %+v", out.Rules)
}
}
// TestTranslate_NamedPortRejected — named ports aren't supported yet;
// translator must skip the rule and warn.
func TestTranslate_NamedPortRejected(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("2001:db8::1")},
}
proto := corev1.ProtocolTCP
named := intstr.FromString("http")
policy := newPolicy("ns1", "named", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
Ports: []netv1.NetworkPolicyPort{{Protocol: &proto, Port: &named}},
}}
})
var warns []string
out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, func(s string) {
warns = append(warns, s)
})
if len(out.Rules) != 0 {
t.Fatalf("expected named-port rule to be skipped")
}
if len(warns) == 0 {
t.Fatalf("expected a warning about named ports")
}
// The pod should still be isolated since the policy selected it.
in, _ := isolationFor(out, "ns1/web")
if !in {
t.Fatalf("pod should be isolated even when its rule is dropped")
}
}
// TestTranslate_PolicyOnlyAppliesToOwnNamespace — a policy in nsA does
// NOT select pods in nsB even if their labels match.
func TestTranslate_PolicyScopedToNamespace(t *testing.T) {
a := Pod{Namespace: "nsA", Name: "p", HostIface: "f1",
Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}}
b := Pod{Namespace: "nsB", Name: "p", HostIface: "f2",
Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::2")}}
policy := newPolicy("nsA", "deny", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *selectorMatching(map[string]string{"app": "web"})
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
})
out, _ := Translate(Inputs{LocalPods: []Pod{a, b}, Policies: []netv1.NetworkPolicy{policy}}, nil)
inA, _ := isolationFor(out, "nsA/p")
inB, _ := isolationFor(out, "nsB/p")
if !inA {
t.Fatalf("nsA/p should be isolated")
}
if inB {
t.Fatalf("nsB/p must NOT be isolated by a policy in nsA")
}
}
// TestTranslate_PodWithoutAllocationSkipped — pod with no IPs is silently
// skipped (its rule could not match any traffic anyway).
func TestTranslate_PodWithoutAllocationSkipped(t *testing.T) {
pod := Pod{Namespace: "ns1", Name: "p", HostIface: "f1",
Labels: map[string]string{"app": "web"}}
policy := newPolicy("ns1", "deny", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
})
out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil)
in, _ := isolationFor(out, "ns1/p")
if in {
t.Fatalf("pod without IP should not appear in output")
}
}
// TestTranslate_Determinism — translating the same Inputs twice produces
// equal outputs (Rules in equal order, Isolated equal).
func TestTranslate_Determinism(t *testing.T) {
pod := Pod{
Namespace: "ns1", Name: "web", HostIface: "f1",
Labels: map[string]string{"app": "web"},
IPs: []net.IP{mustIP("2001:db8::1")},
}
peers := []PeerPod{
{Namespace: "ns1", Name: "z", Labels: map[string]string{"app": "client"}, IPs: []net.IP{mustIP("2001:db8::2")}},
{Namespace: "ns1", Name: "a", Labels: map[string]string{"app": "client"}, IPs: []net.IP{mustIP("2001:db8::3")}},
}
policies := []netv1.NetworkPolicy{
newPolicy("ns1", "z-second", func(p *netv1.NetworkPolicy) {
p.Spec.PodSelector = *emptySelector()
p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress}
p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
PodSelector: selectorMatching(map[string]string{"app": "client"}),
}},
}}
}),
}
in := Inputs{LocalPods: []Pod{pod}, PeerPods: peers, Policies: policies}
a, _ := Translate(in, nil)
b, _ := Translate(in, nil)
if len(a.Rules) != len(b.Rules) {
t.Fatalf("rule count differs: %d vs %d", len(a.Rules), len(b.Rules))
}
for i := range a.Rules {
if a.Rules[i].PodKey != b.Rules[i].PodKey || len(a.Rules[i].PeerCIDRs) != len(b.Rules[i].PeerCIDRs) {
t.Fatalf("rule[%d] differs", i)
}
}
}