Files
flock/pkg/agent/netpol/reconciler_test.go
Donavan Fritz 5d9b6bfeec
Build flock Image / build (push) Has been cancelled
netpol: anchor base-chain jump on veth only, not pod IP
The previous base-chain jump matched iifname/oifname AND saddr/daddr ==
pod eth0 IP. Anycast traffic has the anycast IP as daddr, not the pod's
eth0 unicast — so anycast packets skipped the policy chain entirely and
fell through to the forward chain's policy=accept.

The veth uniquely belongs to one pod. Anything traversing it is to or
from that pod by definition (anycast, unicast, future overlay routes).
Match on iifname/oifname alone; let the pod-side chain's accept lines +
trailing drop be the policy.

Validated end-to-end on host001: anycast nginx pod with default-deny
ingress NetPol now correctly drops traffic from any peer; adding an
allow-from-podSelector rule unblocks only the matched peer.

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

161 lines
4.3 KiB
Go

package netpol
import (
"context"
"io"
"log/slog"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// fakeApplier captures Apply calls for assertion. Drop-in for *Applier in
// tests because Reconciler depends only on the (Apply, Clear) pair.
type fakeApplier struct {
mu sync.Mutex
calls []string
last string
err error
}
func (f *fakeApplier) Apply(_ context.Context, script string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.err != nil {
return f.err
}
if script == f.last {
return nil // de-dup like the real Applier
}
f.last = script
f.calls = append(f.calls, script)
return nil
}
func (f *fakeApplier) Clear(_ context.Context) error { return nil }
func (f *fakeApplier) lastScript() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.last
}
func (f *fakeApplier) callCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.calls)
}
// applierIface is satisfied by *Applier and *fakeApplier; we narrow
// Reconciler to this in tests by adapting via a tiny wrapper.
type applierIface interface {
Apply(context.Context, string) error
Clear(context.Context) error
}
// reconcileOnce drives one pass synchronously without spinning a goroutine.
func reconcileOnce(t *testing.T, world *World, local LocalPodSource, app applierIface) {
t.Helper()
in := Inputs{
LocalPods: local(),
PeerPods: world.snapshotPeerPods(),
Namespaces: world.snapshotNamespaces(),
Policies: world.snapshotPolicies(),
}
out, err := Translate(in, nil)
if err != nil {
t.Fatal(err)
}
if err := app.Apply(context.Background(), Render(out)); err != nil {
t.Fatal(err)
}
}
// silentLogger returns a slog.Logger discarding everything — keeps test
// output tidy.
func silentLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
}
func TestReconciler_NoIsolatedPods_ShortScript(t *testing.T) {
world := NewWorld(silentLogger())
local := func() []Pod { return nil }
app := &fakeApplier{}
reconcileOnce(t, world, local, app)
got := app.lastScript()
if !strings.Contains(got, "table inet flock_netpol") {
t.Fatalf("missing table:\n%s", got)
}
// Without any isolated pods the base chain has policy accept and no
// jumps. That's the desired "open" state.
if strings.Contains(got, "jump pod_") {
t.Fatalf("unexpected jump in open state:\n%s", got)
}
}
func TestReconciler_PolicyIsolatesLocalPod(t *testing.T) {
world := NewWorld(silentLogger())
// Seed a default-deny policy in ns1.
world.onPolicy(&netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "deny-all"},
Spec: netv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{},
PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
},
}, false)
local := func() []Pod {
return []Pod{{
Namespace: "ns1", Name: "web",
Labels: map[string]string{"app": "web"},
HostIface: "flock00000001",
IPs: []net.IP{mustIP("2001:db8::1")},
}}
}
app := &fakeApplier{}
reconcileOnce(t, world, local, app)
got := app.lastScript()
if !strings.Contains(got, "_ingress {") {
t.Fatalf("expected pod ingress chain:\n%s", got)
}
if !strings.Contains(got, "drop") {
t.Fatalf("expected default-deny drop:\n%s", got)
}
if !strings.Contains(got, `oifname "flock00000001" jump pod_`) {
t.Fatalf("expected base-chain jump anchored on veth:\n%s", got)
}
}
func TestReconciler_DedupesIdenticalRender(t *testing.T) {
world := NewWorld(silentLogger())
local := func() []Pod {
return []Pod{{
Namespace: "ns1", Name: "web", HostIface: "f1",
IPs: []net.IP{mustIP("2001:db8::1")},
}}
}
app := &fakeApplier{}
reconcileOnce(t, world, local, app)
reconcileOnce(t, world, local, app)
reconcileOnce(t, world, local, app)
if got := app.callCount(); got != 1 {
t.Fatalf("expected 1 unique apply, got %d", got)
}
}
func TestReconciler_OnChangeFiresTrigger(t *testing.T) {
world := NewWorld(silentLogger())
var triggered atomic.Int32
world.OnChange(func() { triggered.Add(1) })
world.onNamespace(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, false)
world.onPolicy(&netv1.NetworkPolicy{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "p"}}, false)
if triggered.Load() != 2 {
t.Fatalf("expected 2 OnChange calls, got %d", triggered.Load())
}
}