package netpol import ( "net" "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" ) func mustIP(s string) net.IP { ip := net.ParseIP(s) if ip == nil { panic("bad IP: " + s) } return ip } func newPolicy(ns, name string, mods ...func(*netv1.NetworkPolicy)) netv1.NetworkPolicy { p := netv1.NetworkPolicy{ ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: name}, Spec: netv1.NetworkPolicySpec{}, } for _, m := range mods { m(&p) } return p } func tcpPort(port int) netv1.NetworkPolicyPort { proto := corev1.ProtocolTCP p := intstr.FromInt32(int32(port)) return netv1.NetworkPolicyPort{Protocol: &proto, Port: &p} } // Pod-only selector that matches everything (`{}`). func emptySelector() *metav1.LabelSelector { return &metav1.LabelSelector{} } func selectorMatching(kv map[string]string) *metav1.LabelSelector { return &metav1.LabelSelector{MatchLabels: kv} } // Helper: collect Isolated keys for the given pod into a string list. func isolationFor(out Output, podKey string) (in, eg bool) { if _, ok := out.Isolated[Isolation{PodKey: podKey, Direction: DirIngress}]; ok { in = true } if _, ok := out.Isolated[Isolation{PodKey: podKey, Direction: DirEgress}]; ok { eg = true } return } // TestTranslate_NoPolicies — pod with no matching policy is unrestricted. func TestTranslate_NoPolicies(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "p1", Labels: map[string]string{"app": "web"}, HostIface: "flock00000001", IPs: []net.IP{mustIP("2001:db8::1")}, } out, err := Translate(Inputs{LocalPods: []Pod{pod}}, nil) if err != nil { t.Fatal(err) } if len(out.Rules) != 0 { t.Fatalf("expected no rules, got %d", len(out.Rules)) } in, eg := isolationFor(out, "ns1/p1") if in || eg { t.Fatalf("pod should not be isolated: in=%v eg=%v", in, eg) } } // TestTranslate_DefaultDeny — a policy with empty Ingress + PolicyTypes // = [Ingress] selects the pod and isolates it; no allow rules emitted. func TestTranslate_DefaultDenyIngress(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", Labels: map[string]string{"app": "web"}, HostIface: "flock00000001", IPs: []net.IP{mustIP("2001:db8::1")}, } policy := newPolicy("ns1", "default-deny", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} }) out, err := Translate(Inputs{ LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}, }, nil) if err != nil { t.Fatal(err) } if len(out.Rules) != 0 { t.Fatalf("expected no rules from a deny-all, got %d", len(out.Rules)) } in, eg := isolationFor(out, "ns1/web") if !in { t.Fatalf("ingress should be isolated") } if eg { t.Fatalf("egress should NOT be isolated (policy only set ingress)") } } // TestTranslate_DefaultDenyEgress_InferredFromEgressList — when // PolicyTypes is omitted but Spec.Egress is non-empty, egress should // also be isolated by inference. func TestTranslate_DefaultDenyEgress_InferredFromEgressList(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", Labels: map[string]string{"app": "web"}, HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")}, } policy := newPolicy("ns1", "egress-rule", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.Egress = []netv1.NetworkPolicyEgressRule{{}} }) out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil) in, eg := isolationFor(out, "ns1/web") if !in || !eg { t.Fatalf("both directions should be isolated: in=%v eg=%v", in, eg) } } // TestTranslate_PodSelectorPeer_SameNamespace — peer is a single pod in // the same namespace, identified by label. func TestTranslate_PodSelectorPeer(t *testing.T) { web := Pod{ Namespace: "ns1", Name: "web", Labels: map[string]string{"app": "web"}, HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")}, } clientIP := mustIP("2001:db8::2") peer := PeerPod{ Namespace: "ns1", Name: "client", Labels: map[string]string{"app": "client"}, IPs: []net.IP{clientIP}, } policy := newPolicy("ns1", "allow-from-client", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *selectorMatching(map[string]string{"app": "web"}) p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ From: []netv1.NetworkPolicyPeer{{ PodSelector: selectorMatching(map[string]string{"app": "client"}), }}, Ports: []netv1.NetworkPolicyPort{tcpPort(80)}, }} }) out, err := Translate(Inputs{ LocalPods: []Pod{web}, PeerPods: []PeerPod{peer}, Policies: []netv1.NetworkPolicy{policy}, }, nil) if err != nil { t.Fatal(err) } if len(out.Rules) != 1 { t.Fatalf("expected 1 rule, got %d: %+v", len(out.Rules), out.Rules) } r := out.Rules[0] if r.PodKey != "ns1/web" || r.Direction != DirIngress { t.Fatalf("rule has wrong subject: %+v", r) } if len(r.PeerCIDRs) != 1 || !r.PeerCIDRs[0].IP.Equal(clientIP) { t.Fatalf("peer CIDR wrong: %+v", r.PeerCIDRs) } if len(r.Ports) != 1 || r.Ports[0].Protocol != "tcp" || r.Ports[0].Port != 80 { t.Fatalf("port wrong: %+v", r.Ports) } } // TestTranslate_NamespaceSelector — peer is "every pod in any namespace // with label tier=trusted". func TestTranslate_NamespaceSelector(t *testing.T) { web := Pod{ Namespace: "ns1", Name: "web", Labels: map[string]string{"app": "web"}, HostIface: "f1", IPs: []net.IP{mustIP("2001:db8::1")}, } out, err := Translate(Inputs{ LocalPods: []Pod{web}, Namespaces: []Namespace{ {Name: "ns1", Labels: map[string]string{}}, {Name: "trusted-1", Labels: map[string]string{"tier": "trusted"}}, {Name: "trusted-2", Labels: map[string]string{"tier": "trusted"}}, {Name: "untrusted", Labels: map[string]string{"tier": "wild"}}, }, PeerPods: []PeerPod{ {Namespace: "trusted-1", Name: "a", IPs: []net.IP{mustIP("2001:db8::a")}}, {Namespace: "trusted-2", Name: "b", IPs: []net.IP{mustIP("2001:db8::b")}}, {Namespace: "untrusted", Name: "x", IPs: []net.IP{mustIP("2001:db8::ff")}}, }, Policies: []netv1.NetworkPolicy{newPolicy("ns1", "allow-trusted", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ From: []netv1.NetworkPolicyPeer{{ NamespaceSelector: selectorMatching(map[string]string{"tier": "trusted"}), }}, }} })}, }, nil) if err != nil { t.Fatal(err) } if len(out.Rules) != 1 { t.Fatalf("expected 1 rule, got %d", len(out.Rules)) } got := map[string]bool{} for _, c := range out.Rules[0].PeerCIDRs { got[c.IP.String()] = true } if !got["2001:db8::a"] || !got["2001:db8::b"] { t.Fatalf("trusted pod IPs missing: %v", got) } if got["2001:db8::ff"] { t.Fatalf("untrusted pod IP leaked into rule") } } // TestTranslate_IPBlockWithExcept — ipBlock with an except range. func TestTranslate_IPBlockWithExcept(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("10.0.0.1")}, } policy := newPolicy("ns1", "ipblock", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ From: []netv1.NetworkPolicyPeer{{ IPBlock: &netv1.IPBlock{ CIDR: "10.0.0.0/8", Except: []string{"10.99.0.0/16", "10.42.42.0/24"}, }, }}, }} }) out, err := Translate(Inputs{ LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}, }, 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] if len(r.PeerCIDRs) != 1 || r.PeerCIDRs[0].String() != "10.0.0.0/8" { t.Fatalf("peer CIDR wrong: %v", r.PeerCIDRs) } if len(r.PeerExcept) != 2 { t.Fatalf("expected 2 except, got %d", len(r.PeerExcept)) } } // TestTranslate_AllowAllPeers — empty From list means "from anywhere". func TestTranslate_AllowAllPeers(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}, } policy := newPolicy("ns1", "allow-all-on-port", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ Ports: []netv1.NetworkPolicyPort{tcpPort(443)}, }} }) out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil) if len(out.Rules) != 1 { t.Fatalf("expected 1 rule, got %d", len(out.Rules)) } r := out.Rules[0] if len(r.PeerCIDRs) != 0 || len(r.PeerExcept) != 0 { t.Fatalf("expected allow-all peers, got CIDRs=%v Except=%v", r.PeerCIDRs, r.PeerExcept) } } // TestTranslate_AllowAllPorts — empty Ports list means "all ports". func TestTranslate_AllowAllPorts(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}, } policy := newPolicy("ns1", "allow-from-all", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ From: []netv1.NetworkPolicyPeer{{ PodSelector: emptySelector(), }}, }} }) peer := PeerPod{ Namespace: "ns1", Name: "x", IPs: []net.IP{mustIP("2001:db8::aa")}, } out, _ := Translate(Inputs{ LocalPods: []Pod{pod}, PeerPods: []PeerPod{peer}, Policies: []netv1.NetworkPolicy{policy}, }, nil) if len(out.Rules) != 1 { t.Fatalf("expected 1 rule, got %d", len(out.Rules)) } r := out.Rules[0] if len(r.Ports) != 1 || r.Ports[0] != (PortMatch{}) { t.Fatalf("expected single any-port match, got %+v", r.Ports) } } // TestTranslate_PortRange — endPort field. func TestTranslate_PortRange(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}, } policy := newPolicy("ns1", "range", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} proto := corev1.ProtocolTCP port := intstr.FromInt32(8000) end := int32(8999) p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ Ports: []netv1.NetworkPolicyPort{{Protocol: &proto, Port: &port, EndPort: &end}}, }} }) out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil) if len(out.Rules) != 1 || out.Rules[0].Ports[0].Port != 8000 || out.Rules[0].Ports[0].EndPort != 8999 { t.Fatalf("range not preserved: %+v", out.Rules) } } // TestTranslate_NamedPortRejected — named ports aren't supported yet; // translator must skip the rule and warn. func TestTranslate_NamedPortRejected(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}, } proto := corev1.ProtocolTCP named := intstr.FromString("http") policy := newPolicy("ns1", "named", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ Ports: []netv1.NetworkPolicyPort{{Protocol: &proto, Port: &named}}, }} }) var warns []string out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, func(s string) { warns = append(warns, s) }) if len(out.Rules) != 0 { t.Fatalf("expected named-port rule to be skipped") } if len(warns) == 0 { t.Fatalf("expected a warning about named ports") } // The pod should still be isolated since the policy selected it. in, _ := isolationFor(out, "ns1/web") if !in { t.Fatalf("pod should be isolated even when its rule is dropped") } } // TestTranslate_PolicyOnlyAppliesToOwnNamespace — a policy in nsA does // NOT select pods in nsB even if their labels match. func TestTranslate_PolicyScopedToNamespace(t *testing.T) { a := Pod{Namespace: "nsA", Name: "p", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}} b := Pod{Namespace: "nsB", Name: "p", HostIface: "f2", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::2")}} policy := newPolicy("nsA", "deny", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *selectorMatching(map[string]string{"app": "web"}) p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} }) out, _ := Translate(Inputs{LocalPods: []Pod{a, b}, Policies: []netv1.NetworkPolicy{policy}}, nil) inA, _ := isolationFor(out, "nsA/p") inB, _ := isolationFor(out, "nsB/p") if !inA { t.Fatalf("nsA/p should be isolated") } if inB { t.Fatalf("nsB/p must NOT be isolated by a policy in nsA") } } // TestTranslate_PodWithoutAllocationSkipped — pod with no IPs is silently // skipped (its rule could not match any traffic anyway). func TestTranslate_PodWithoutAllocationSkipped(t *testing.T) { pod := Pod{Namespace: "ns1", Name: "p", HostIface: "f1", Labels: map[string]string{"app": "web"}} policy := newPolicy("ns1", "deny", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} }) out, _ := Translate(Inputs{LocalPods: []Pod{pod}, Policies: []netv1.NetworkPolicy{policy}}, nil) in, _ := isolationFor(out, "ns1/p") if in { t.Fatalf("pod without IP should not appear in output") } } // TestTranslate_Determinism — translating the same Inputs twice produces // equal outputs (Rules in equal order, Isolated equal). func TestTranslate_Determinism(t *testing.T) { pod := Pod{ Namespace: "ns1", Name: "web", HostIface: "f1", Labels: map[string]string{"app": "web"}, IPs: []net.IP{mustIP("2001:db8::1")}, } peers := []PeerPod{ {Namespace: "ns1", Name: "z", Labels: map[string]string{"app": "client"}, IPs: []net.IP{mustIP("2001:db8::2")}}, {Namespace: "ns1", Name: "a", Labels: map[string]string{"app": "client"}, IPs: []net.IP{mustIP("2001:db8::3")}}, } policies := []netv1.NetworkPolicy{ newPolicy("ns1", "z-second", func(p *netv1.NetworkPolicy) { p.Spec.PodSelector = *emptySelector() p.Spec.PolicyTypes = []netv1.PolicyType{netv1.PolicyTypeIngress} p.Spec.Ingress = []netv1.NetworkPolicyIngressRule{{ From: []netv1.NetworkPolicyPeer{{ PodSelector: selectorMatching(map[string]string{"app": "client"}), }}, }} }), } in := Inputs{LocalPods: []Pod{pod}, PeerPods: peers, Policies: policies} a, _ := Translate(in, nil) b, _ := Translate(in, nil) if len(a.Rules) != len(b.Rules) { t.Fatalf("rule count differs: %d vs %d", len(a.Rules), len(b.Rules)) } for i := range a.Rules { if a.Rules[i].PodKey != b.Rules[i].PodKey || len(a.Rules[i].PeerCIDRs) != len(b.Rules[i].PeerCIDRs) { t.Fatalf("rule[%d] differs", i) } } }