netpol: NetworkPolicy v1 enforcement via nftables
Build flock Image / build (push) Has been cancelled
Build flock Image / build (push) Has been cancelled
New pkg/agent/netpol implementing standard networking.k8s.io/v1 NetworkPolicy. Pipeline: pods + policies + namespaces → Translate → Render → Apply Supports ingress + egress, all three peer types (podSelector, namespaceSelector, ipBlock with except), numeric ports + port ranges, default-deny semantics derived from PolicyTypes (or inferred from non-empty Spec.Egress when unset). Apply path is `nft -f -` shell-out — single transaction, atomic, kernel guarantees partial-failure rollback. Idempotent dedup via last-applied script. Reconcile triggers: informer events, 30s self-heal tick, every CNI ADD/DEL. Verified against the three live cluster NetPols (calico-apiserver, remote-proxies/lodge-home-assistant, storage/garage-admin-restrict). Fuzz target stitches Translate + Render with random selector and peer inputs; 21 unit tests cover the policy semantics. Named ports skip with a warn — deferred until kubelet exposes them in a form that doesn't require shadowing pod state. Dockerfile: + nftables. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package netpol
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
// FuzzTranslate_AndRender stitches the Translator and Renderer together
|
||||
// against synthetic NetworkPolicies built from fuzzed bytes. We are not
|
||||
// trying to produce *valid* policies — the goal is to confirm that:
|
||||
//
|
||||
// 1. Neither stage panics on weird input.
|
||||
// 2. Render output is balanced (every "{" has a matching "}").
|
||||
// 3. Rendering twice is byte-stable.
|
||||
// 4. The Pods set in Output is consistent with Isolated (every isolated
|
||||
// PodKey has a matching entry in Pods).
|
||||
//
|
||||
// The translator's warn callback is captured to ensure it never panics
|
||||
// with unexpected message types either.
|
||||
func FuzzTranslate_AndRender(f *testing.F) {
|
||||
type seed struct {
|
||||
policyNS, policyName string
|
||||
podSelectorKey, podSelValue string
|
||||
peerSelectorKey, peerSelV string
|
||||
peerNS, peerName, peerIP string
|
||||
port uint16
|
||||
ipBlockCIDR, ipBlockExcept string
|
||||
}
|
||||
for _, s := range []seed{
|
||||
{policyNS: "ns1", policyName: "p1", podSelectorKey: "app", podSelValue: "web", port: 80},
|
||||
{policyNS: "ns1", policyName: "p1", peerSelectorKey: "app", peerSelV: "client", peerNS: "ns1", peerName: "c1", peerIP: "2001:db8::aa", port: 443},
|
||||
{policyNS: "ns1", policyName: "p1", ipBlockCIDR: "10.0.0.0/8", ipBlockExcept: "10.99.0.0/16", port: 0},
|
||||
{policyNS: "", policyName: ""}, // pathological
|
||||
{policyNS: "ns1", policyName: "p1", podSelectorKey: "app\x00", podSelValue: "web\nnewline"},
|
||||
{policyNS: "ns1", policyName: "p1", port: 65535},
|
||||
{policyNS: "ns1", policyName: "p1", port: 1},
|
||||
} {
|
||||
f.Add(s.policyNS, s.policyName, s.podSelectorKey, s.podSelValue,
|
||||
s.peerSelectorKey, s.peerSelV, s.peerNS, s.peerName, s.peerIP,
|
||||
s.port, s.ipBlockCIDR, s.ipBlockExcept)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T,
|
||||
policyNS, policyName,
|
||||
podSelectorKey, podSelValue,
|
||||
peerSelectorKey, peerSelV,
|
||||
peerNS, peerName, peerIP string,
|
||||
port uint16,
|
||||
ipBlockCIDR, ipBlockExcept string,
|
||||
) {
|
||||
// Build a synthetic policy.
|
||||
policy := netv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: policyNS, Name: policyName},
|
||||
Spec: netv1.NetworkPolicySpec{
|
||||
PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress},
|
||||
},
|
||||
}
|
||||
if podSelectorKey != "" {
|
||||
policy.Spec.PodSelector = metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{podSelectorKey: podSelValue},
|
||||
}
|
||||
} else {
|
||||
policy.Spec.PodSelector = metav1.LabelSelector{}
|
||||
}
|
||||
ingress := netv1.NetworkPolicyIngressRule{}
|
||||
if peerSelectorKey != "" {
|
||||
ingress.From = append(ingress.From, netv1.NetworkPolicyPeer{
|
||||
PodSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{peerSelectorKey: peerSelV},
|
||||
},
|
||||
})
|
||||
}
|
||||
if ipBlockCIDR != "" {
|
||||
peer := netv1.NetworkPolicyPeer{
|
||||
IPBlock: &netv1.IPBlock{CIDR: ipBlockCIDR},
|
||||
}
|
||||
if ipBlockExcept != "" {
|
||||
peer.IPBlock.Except = []string{ipBlockExcept}
|
||||
}
|
||||
ingress.From = append(ingress.From, peer)
|
||||
}
|
||||
if port != 0 {
|
||||
tcp := corev1.ProtocolTCP
|
||||
p := intstr.FromInt32(int32(port))
|
||||
ingress.Ports = append(ingress.Ports, netv1.NetworkPolicyPort{
|
||||
Protocol: &tcp, Port: &p,
|
||||
})
|
||||
}
|
||||
policy.Spec.Ingress = append(policy.Spec.Ingress, ingress)
|
||||
|
||||
// Local pod, possibly matching the policy.
|
||||
pod := Pod{
|
||||
Namespace: "ns1", Name: "web",
|
||||
Labels: map[string]string{podSelectorKey: podSelValue, "app": "web"},
|
||||
HostIface: "flock00000001",
|
||||
IPs: []net.IP{mustIP("2001:db8::1")},
|
||||
}
|
||||
// Peer pod, possibly matching the peer selector.
|
||||
var peers []PeerPod
|
||||
if peerName != "" {
|
||||
peerIPParsed := net.ParseIP(peerIP)
|
||||
if peerIPParsed != nil {
|
||||
peers = append(peers, PeerPod{
|
||||
Namespace: peerNS, Name: peerName,
|
||||
Labels: map[string]string{peerSelectorKey: peerSelV},
|
||||
IPs: []net.IP{peerIPParsed},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
out, err := Translate(Inputs{
|
||||
LocalPods: []Pod{pod},
|
||||
PeerPods: peers,
|
||||
Namespaces: []Namespace{
|
||||
{Name: "ns1", Labels: map[string]string{"kubernetes.io/metadata.name": "ns1"}},
|
||||
},
|
||||
Policies: []netv1.NetworkPolicy{policy},
|
||||
}, func(string) {})
|
||||
if err != nil {
|
||||
return // any error is acceptable
|
||||
}
|
||||
|
||||
// Property: every isolated PodKey appears in Output.Pods.
|
||||
for iso := range out.Isolated {
|
||||
if _, ok := out.Pods[iso.PodKey]; !ok {
|
||||
t.Fatalf("isolated %s has no Pods entry", iso.PodKey)
|
||||
}
|
||||
}
|
||||
|
||||
script := Render(out)
|
||||
// Property: balanced braces.
|
||||
if got := strings.Count(script, "{") - strings.Count(script, "}"); got != 0 {
|
||||
t.Fatalf("unbalanced braces (%d):\n%s", got, script)
|
||||
}
|
||||
// Property: deterministic (run again, compare).
|
||||
script2 := Render(out)
|
||||
if script != script2 {
|
||||
t.Fatalf("Render not deterministic")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user