test(platform): NetworkPolicy 求值器按规范处理 ipBlock、空 From、表达式选择器与端口范围

This commit is contained in:
Lemon-miaow committed 2026-09-27 12:14:37 +08:00
1 parent dfc2dd8888
commit d7afdb4124
1 file changed
+125 -10
+125 -10
View File
@@ -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")
}
}