From d7afdb4124367e401f64ce206e5e26b1ea74846f Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 12:14:37 +0800 Subject: [PATCH] =?UTF-8?q?test(platform):=20NetworkPolicy=20=E6=B1=82?= =?UTF-8?q?=E5=80=BC=E5=99=A8=E6=8C=89=E8=A7=84=E8=8C=83=E5=A4=84=E7=90=86?= =?UTF-8?q?=20ipBlock=E3=80=81=E7=A9=BA=20From=E3=80=81=E8=A1=A8=E8=BE=BE?= =?UTF-8?q?=E5=BC=8F=E9=80=89=E6=8B=A9=E5=99=A8=E4=B8=8E=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/platform/postgres_test.go | 135 ++++++++++++++++++++++++++--- 1 file changed, 125 insertions(+), 10 deletions(-) diff --git a/internal/platform/postgres_test.go b/internal/platform/postgres_test.go index c30d030..7360eb7 100644 --- a/internal/platform/postgres_test.go +++ b/internal/platform/postgres_test.go @@ -4,6 +4,7 @@ import ( "net/netip" "reflect" "regexp" + "slices" "strconv" "strings" "testing" @@ -18,33 +19,80 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" ) -// admits reports whether np lets a pod with podLabels in namespace ns open a -// TCP connection to port. -func admits(np *networkingv1.NetworkPolicy, ns string, podLabels map[string]string, port int32) bool { +// podAddr is the address the source pods of these checks dial from: one in k3s's +// default pod network, which an ipBlock peer has to exclude to keep them out. +var podAddr = netip.MustParseAddr("10.42.0.17") + +// admits reports whether np lets a pod with podLabels in namespace ns, dialing +// from podAddr, open a TCP connection to port on the pods np selects. It follows +// the NetworkPolicy rules rather than the shapes this package happens to render: +// a policy that does not govern ingress restricts nothing; a rule with no ports +// covers every port and one with no peers every source; a port without a number +// covers every port of its protocol; a peer's selectors are full label selectors, +// matched against the pod and against its namespace's labels as Objects renders +// them (the name label only); an ipBlock admits the pod by its address. A named +// port fails the test: it resolves against a container spec this does not see. +func admits(t *testing.T, np *networkingv1.NetworkPolicy, ns string, podLabels map[string]string, port int32) bool { + t.Helper() + if len(np.Spec.PolicyTypes) > 0 && !slices.Contains(np.Spec.PolicyTypes, networkingv1.PolicyTypeIngress) { + return true + } + selector := func(s *metav1.LabelSelector) labels.Selector { + sel, err := metav1.LabelSelectorAsSelector(s) + if err != nil { + t.Fatalf("policy %s: selector %v: %v", np.Name, s, err) + } + return sel + } + prefix := func(cidr string) netip.Prefix { + p, err := netip.ParsePrefix(cidr) + if err != nil { + t.Fatalf("policy %s: ipBlock %q: %v", np.Name, cidr, err) + } + return p + } for _, rule := range np.Spec.Ingress { portOK := len(rule.Ports) == 0 for _, p := range rule.Ports { - if (p.Protocol == nil || *p.Protocol == corev1.ProtocolTCP) && p.Port != nil && p.Port.IntVal == port { + switch { + case p.Protocol != nil && *p.Protocol != corev1.ProtocolTCP: + case p.Port == nil: portOK = true + case p.Port.Type == intstr.String: + t.Fatalf("policy %s: named port %q, which this evaluator cannot resolve", np.Name, p.Port.StrVal) + case p.EndPort != nil: + portOK = portOK || (p.Port.IntVal <= port && port <= *p.EndPort) + default: + portOK = portOK || p.Port.IntVal == port } } if !portOK { continue } + if len(rule.From) == 0 { + return true + } for _, peer := range rule.From { if peer.IPBlock != nil { + in := prefix(peer.IPBlock.CIDR).Contains(podAddr) + for _, e := range peer.IPBlock.Except { + in = in && !prefix(e).Contains(podAddr) + } + if in { + return true + } continue } nsOK := peer.NamespaceSelector == nil && ns == np.Namespace if peer.NamespaceSelector != nil { - nsOK = labels.SelectorFromSet(peer.NamespaceSelector.MatchLabels). - Matches(labels.Set{"kubernetes.io/metadata.name": ns}) + nsOK = selector(peer.NamespaceSelector).Matches(labels.Set{"kubernetes.io/metadata.name": ns}) } - podOK := peer.PodSelector == nil || - labels.SelectorFromSet(peer.PodSelector.MatchLabels).Matches(labels.Set(podLabels)) + podOK := peer.PodSelector == nil || selector(peer.PodSelector).Matches(labels.Set(podLabels)) if nsOK && podOK { return true } @@ -53,6 +101,73 @@ func admits(np *networkingv1.NetworkPolicy, ns string, podLabels map[string]stri return false } +// The evaluator itself, against the NetworkPolicy rules it claims to follow: the +// fence checks below mean nothing if it waves through a shape it does not read. +func TestAdmits_FollowsTheNetworkPolicyRules(t *testing.T) { + tcp, udp := corev1.ProtocolTCP, corev1.ProtocolUDP + port := func(n int32) *intstr.IntOrString { p := intstr.FromInt32(n); return &p } + endPort := func(n int32) *int32 { return &n } + api := map[string]string{"component": "api"} + other := map[string]string{"component": "reaper"} + policy := func(rules ...networkingv1.NetworkPolicyIngressRule) *networkingv1.NetworkPolicy { + return netpol("under-test", "felis", metav1.LabelSelector{}, rules) + } + inFelis := &metav1.LabelSelector{MatchLabels: map[string]string{"kubernetes.io/metadata.name": "felis"}} + on5432 := []networkingv1.NetworkPolicyPort{{Protocol: &tcp, Port: port(5432)}} + + for _, c := range []struct { + name string + np *networkingv1.NetworkPolicy + ns string + labels map[string]string + port int32 + want bool + }{ + {"no rules admit nothing", policy(), "felis", api, 5432, false}, + {"a rule without peers admits every source", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432}), "minecraft", other, 5432, true}, + {"a rule without ports covers every port", policy(networkingv1.NetworkPolicyIngressRule{ + From: []networkingv1.NetworkPolicyPeer{{PodSelector: &metav1.LabelSelector{MatchLabels: api}}}}), "felis", api, 9999, true}, + {"a port without a number covers every TCP port", policy(networkingv1.NetworkPolicyIngressRule{ + Ports: []networkingv1.NetworkPolicyPort{{Protocol: &tcp}}}), "felis", api, 9999, true}, + {"a UDP port admits no TCP connection", policy(networkingv1.NetworkPolicyIngressRule{ + Ports: []networkingv1.NetworkPolicyPort{{Protocol: &udp, Port: port(5432)}}}), "felis", api, 5432, false}, + {"a port range covers its inside", policy(networkingv1.NetworkPolicyIngressRule{ + Ports: []networkingv1.NetworkPolicyPort{{Port: port(5000), EndPort: endPort(6000)}}}), "felis", api, 5432, true}, + {"a port range stops at its end", policy(networkingv1.NetworkPolicyIngressRule{ + Ports: []networkingv1.NetworkPolicyPort{{Port: port(5000), EndPort: endPort(5431)}}}), "felis", api, 5432, false}, + {"another port", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{PodSelector: &metav1.LabelSelector{MatchLabels: api}}}}), "felis", api, 5433, false}, + {"an ipBlock over the pod network admits a pod", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{IPBlock: &networkingv1.IPBlock{CIDR: "0.0.0.0/0"}}}}), "minecraft", other, 5432, true}, + {"an ipBlock excepting the pod's address", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{IPBlock: &networkingv1.IPBlock{CIDR: "10.0.0.0/8", Except: []string{"10.42.0.0/16"}}}}}), "minecraft", other, 5432, false}, + {"an ipBlock elsewhere", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{IPBlock: &networkingv1.IPBlock{CIDR: "192.168.0.0/16"}}}}), "minecraft", other, 5432, false}, + {"a pod selector alone stays in the policy's namespace", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{PodSelector: &metav1.LabelSelector{MatchLabels: api}}}}), "minecraft", api, 5432, false}, + {"an empty namespace selector spans every namespace", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{NamespaceSelector: &metav1.LabelSelector{}, PodSelector: &metav1.LabelSelector{MatchLabels: api}}}}), "minecraft", api, 5432, true}, + {"a namespace selector without a pod selector admits the whole namespace", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{NamespaceSelector: inFelis}}}), "felis", other, 5432, true}, + {"a namespace selector keeps other namespaces out", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{NamespaceSelector: inFelis}}}), "minecraft", api, 5432, false}, + {"matchExpressions narrow a selector", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{PodSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "component", Operator: metav1.LabelSelectorOpIn, Values: []string{"api"}}}}}}}), "felis", other, 5432, false}, + {"matchExpressions admit what they match", policy(networkingv1.NetworkPolicyIngressRule{Ports: on5432, + From: []networkingv1.NetworkPolicyPeer{{PodSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "component", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"api"}}}}}}}), "felis", other, 5432, true}, + {"a policy that governs only egress restricts no ingress", &networkingv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "egress-only", Namespace: "felis"}, + Spec: networkingv1.NetworkPolicySpec{PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeEgress}}, + }, "minecraft", other, 5432, true}, + } { + if got := admits(t, c.np, c.ns, c.labels, c.port); got != c.want { + t.Errorf("%s: admitted = %v, want %v", c.name, got, c.want) + } + } +} + func podTemplateOf(t *testing.T, objs []Object, kind, name string) corev1.PodTemplateSpec { t.Helper() for _, o := range objs { @@ -137,11 +252,11 @@ func TestPostgresIngress_AdmitsExactlyTheDatabaseClients(t *testing.T) { // the Minecraft namespace must not borrow them. {"api labels outside the control namespace", p.MinecraftNamespace, podTemplateOf(t, objs, "Deployment", "felis-api").Labels, false}, } { - if got := admits(np, c.ns, c.labels, PostgresPort); got != c.want { + if got := admits(t, np, c.ns, c.labels, PostgresPort); got != c.want { t.Errorf("%s (%s, %v): admitted = %v, want %v", c.who, c.ns, c.labels, got, c.want) } } - if admits(np, p.ControlNamespace, podTemplateOf(t, objs, "Deployment", "felis-api").Labels, PostgresPort+1) { + if admits(t, np, p.ControlNamespace, podTemplateOf(t, objs, "Deployment", "felis-api").Labels, PostgresPort+1) { t.Error("the policy admits felis-api on a port other than the database's") } }