Files
flock/pkg/agent/netpol/cluster_fixtures_test.go
T
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

251 lines
8.3 KiB
Go

package netpol
import (
"net"
"strings"
"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"
)
// These fixtures mirror the three NetworkPolicies live in the sjc001
// cluster on 2026-04-25. They serve as integration-shaped tests: the
// translator + renderer must produce a sensible nft script for each.
//
// Source of truth (refresh by running `kubectl get netpol -A -o yaml`):
//
// - calico-apiserver/allow-apiserver
// - remote-proxies/lodge-home-assistant-ingress
// - storage/garage-admin-restrict
// allowApiserverPolicy: TCP/5443 ingress to apiserver=true pods, no peer
// restriction (allow-from-anywhere on that port).
func allowApiserverPolicy() netv1.NetworkPolicy {
tcp := corev1.ProtocolTCP
port := intstr.FromInt32(5443)
return netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: "calico-apiserver", Name: "allow-apiserver"},
Spec: netv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{MatchLabels: map[string]string{"apiserver": "true"}},
PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
Ingress: []netv1.NetworkPolicyIngressRule{{
Ports: []netv1.NetworkPolicyPort{{Protocol: &tcp, Port: &port}},
}},
},
}
}
// lodgeHomeAssistantPolicy: TCP/8080 from any pod in the `edge` namespace
// to pods labelled app=lodge-home-assistant.
func lodgeHomeAssistantPolicy() netv1.NetworkPolicy {
tcp := corev1.ProtocolTCP
port := intstr.FromInt32(8080)
return netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: "remote-proxies", Name: "lodge-home-assistant-ingress"},
Spec: netv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{MatchLabels: map[string]string{"app": "lodge-home-assistant"}},
PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
Ingress: []netv1.NetworkPolicyIngressRule{{
From: []netv1.NetworkPolicyPeer{{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"kubernetes.io/metadata.name": "edge"},
},
}},
Ports: []netv1.NetworkPolicyPort{{Protocol: &tcp, Port: &port}},
}},
},
}
}
// garageAdminPolicy: complex two-rule policy.
//
// 1. Allow TCP/{3900, 80, 3901} from anywhere.
// 2. Allow TCP/3903 only from pods in `edge` or `storage`.
func garageAdminPolicy() netv1.NetworkPolicy {
tcp := corev1.ProtocolTCP
p3900 := intstr.FromInt32(3900)
p80 := intstr.FromInt32(80)
p3901 := intstr.FromInt32(3901)
p3903 := intstr.FromInt32(3903)
return netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: "storage", Name: "garage-admin-restrict"},
Spec: netv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{MatchLabels: map[string]string{"app": "garage"}},
PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
Ingress: []netv1.NetworkPolicyIngressRule{
{
Ports: []netv1.NetworkPolicyPort{
{Protocol: &tcp, Port: &p3900},
{Protocol: &tcp, Port: &p80},
{Protocol: &tcp, Port: &p3901},
},
},
{
From: []netv1.NetworkPolicyPeer{
{NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"kubernetes.io/metadata.name": "edge"},
}},
{NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"kubernetes.io/metadata.name": "storage"},
}},
},
Ports: []netv1.NetworkPolicyPort{{Protocol: &tcp, Port: &p3903}},
},
},
},
}
}
// TestClusterFixture_AllowApiserver — pod selected by the policy gets
// isolated; the rendered script accepts TCP/5443 from anywhere.
func TestClusterFixture_AllowApiserver(t *testing.T) {
pod := Pod{
Namespace: "calico-apiserver",
Name: "calico-apiserver-1",
Labels: map[string]string{"apiserver": "true"},
HostIface: "flock00000001",
IPs: []net.IP{mustIP("2001:db8::1")},
}
out, err := Translate(Inputs{
LocalPods: []Pod{pod},
Policies: []netv1.NetworkPolicy{allowApiserverPolicy()},
}, nil)
if err != nil {
t.Fatal(err)
}
in, _ := isolationFor(out, "calico-apiserver/calico-apiserver-1")
if !in {
t.Fatalf("apiserver pod should be isolated for ingress")
}
script := Render(out)
if !strings.Contains(script, "tcp dport 5443 accept") {
t.Fatalf("expected TCP/5443 allow:\n%s", script)
}
// No peer filter — allow-all-on-port.
if strings.Contains(script, "ip6 saddr {") || strings.Contains(script, "ip saddr {") {
t.Fatalf("expected no peer filter for allow-from-anywhere:\n%s", script)
}
}
// TestClusterFixture_LodgeHomeAssistant — pod isolated; only TCP/8080
// from edge namespace is allowed.
func TestClusterFixture_LodgeHomeAssistant(t *testing.T) {
pod := Pod{
Namespace: "remote-proxies",
Name: "lodge-home-assistant-0",
Labels: map[string]string{"app": "lodge-home-assistant"},
HostIface: "flock00000002",
IPs: []net.IP{mustIP("2001:db8::2")},
}
traefik := PeerPod{
Namespace: "edge", Name: "traefik-0",
Labels: map[string]string{"app": "traefik"},
IPs: []net.IP{mustIP("2001:db8::aa")},
}
stranger := PeerPod{
Namespace: "default", Name: "random",
Labels: map[string]string{"app": "random"},
IPs: []net.IP{mustIP("2001:db8::bb")},
}
out, err := Translate(Inputs{
LocalPods: []Pod{pod},
PeerPods: []PeerPod{traefik, stranger},
Namespaces: []Namespace{
{Name: "edge", Labels: map[string]string{"kubernetes.io/metadata.name": "edge"}},
{Name: "default", Labels: map[string]string{"kubernetes.io/metadata.name": "default"}},
{Name: "remote-proxies", Labels: map[string]string{"kubernetes.io/metadata.name": "remote-proxies"}},
},
Policies: []netv1.NetworkPolicy{lodgeHomeAssistantPolicy()},
}, 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]
// Peer should be exactly traefik's IP, not stranger's.
got := map[string]bool{}
for _, c := range r.PeerCIDRs {
got[c.IP.String()] = true
}
if !got["2001:db8::aa"] {
t.Fatalf("traefik IP missing from rule: %v", got)
}
if got["2001:db8::bb"] {
t.Fatalf("stranger IP leaked into rule")
}
script := Render(out)
if !strings.Contains(script, "tcp dport 8080 accept") {
t.Fatalf("expected TCP/8080 allow:\n%s", script)
}
}
// TestClusterFixture_Garage — verifies the two-rule policy:
//
// 1. ports {3900, 80, 3901} accept from any peer
// 2. port 3903 accept only from edge or storage namespaces
func TestClusterFixture_Garage(t *testing.T) {
pod := Pod{
Namespace: "storage", Name: "garage-0",
Labels: map[string]string{"app": "garage"},
HostIface: "flock00000003",
IPs: []net.IP{mustIP("2001:db8::3")},
}
storagePeer := PeerPod{
Namespace: "storage", Name: "garage-1",
Labels: map[string]string{"app": "garage"},
IPs: []net.IP{mustIP("2001:db8::31")},
}
edgePeer := PeerPod{
Namespace: "edge", Name: "traefik-0",
Labels: map[string]string{"app": "traefik"},
IPs: []net.IP{mustIP("2001:db8::41")},
}
stranger := PeerPod{
Namespace: "default", Name: "random",
Labels: map[string]string{"app": "random"},
IPs: []net.IP{mustIP("2001:db8::ff")},
}
out, err := Translate(Inputs{
LocalPods: []Pod{pod},
PeerPods: []PeerPod{storagePeer, edgePeer, stranger},
Namespaces: []Namespace{
{Name: "edge", Labels: map[string]string{"kubernetes.io/metadata.name": "edge"}},
{Name: "storage", Labels: map[string]string{"kubernetes.io/metadata.name": "storage"}},
{Name: "default", Labels: map[string]string{"kubernetes.io/metadata.name": "default"}},
},
Policies: []netv1.NetworkPolicy{garageAdminPolicy()},
}, nil)
if err != nil {
t.Fatal(err)
}
// Two ingress rules in the source policy → two Rules out (one per
// peer set, ports inline).
if len(out.Rules) != 2 {
t.Fatalf("expected 2 rules (one per ingress entry), got %d", len(out.Rules))
}
script := Render(out)
for _, want := range []string{
"tcp dport 3900 accept",
"tcp dport 80 accept",
"tcp dport 3901 accept",
"tcp dport 3903 accept",
} {
if !strings.Contains(script, want) {
t.Errorf("missing %q in script:\n%s", want, script)
}
}
// The 3903 rule must carry a peer filter for both edge and storage
// peer IPs but not the stranger.
if !strings.Contains(script, "2001:db8::31/128") || !strings.Contains(script, "2001:db8::41/128") {
t.Fatalf("expected edge+storage peer IPs in 3903 rule:\n%s", script)
}
if strings.Contains(script, "2001:db8::ff/128") {
t.Fatalf("stranger IP must not appear:\n%s", script)
}
}