5d9b6bfeec
Build flock Image / build (push) Has been cancelled
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>
161 lines
4.3 KiB
Go
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())
|
|
}
|
|
}
|