What happened:
On an IPv4-only EKS cluster, a NetworkPolicy egress rule with an IPv6 ipBlock that carries except entries permanently breaks eBPF map reconciliation for every pod the policy selects. Every egress map refresh fails with:
{"level":"error","caller":"ebpf/bpf_client.go","msg":"Failed to update kernel map during bulk refresh for key \x40\x00\x00\x00\x01\x00\x00\x00: unable to update map: invalid argument"}
{"level":"error","caller":"ebpf/bpf_client.go","msg":"BPF map update failed unable to update map: invalid argument"}
{"level":"error","caller":"controllers/policyendpoints_controller.go","msg":"Map updates failed for podIdentifier <rs>@<ns>: egress map write: unable to update map: invalid argument"}
The failing key decodes to prefix length 64 (little endian 0x40 0x00 0x00 0x00) with 4 bytes of data 0x01 0x00 0x00 0x00, which is 100::/64 (one of our v6 except entries) encoded as an IPv4 LPM trie key. A prefix length of 64 on a 4-byte-data LPM trie is what the kernel rejects with EINVAL.
Because the refresh aborts on the first bad key, the pod's egress map silently freezes at whatever it contained when the probes were first attached. Endpoint churn (deploys of the destination workloads) leaves the map pointing at stale pod IPs, and the stale-key deletion phase never runs at all, so the map also retains allow entries for IPs that have since been recycled to unrelated pods. We observed 3042 aborted refreshes over 7 days in production before finding it; removing the IPv6 ipBlock from the policy made the very next reconcile succeed and purge 10 stale entries.
Root cause (present in v1.4.0 and still on main):
In pkg/fwruleprocessor/fw_rule_processor.go, ComputeMapEntriesFromEndpointRules:
- The per-rule loop harvests
firewallRule.Except into exceptCidrs BEFORE the f.shouldSkipRule(...) check that filters IPv6 CIDRs on IPv4 clusters. So the ::/0 rule itself is skipped correctly, but its v6 except entries survive.
- The later
for exceptCidr := range exceptCidrs loop adds each except as a deny entry into cidrsMap with no v4/v6 filtering of its own.
- The final encode loop calls
utils.ComputeTrieKey(*firewallMapKey, f.enableIPv6) with enableIPv6=false, producing a malformed v4-shaped key (v6 prefix length plus the first 4 bytes of the v6 address) for each v6 except.
Then in aws-ebpf-sdk-go (v1.0.15, pkg/maps/loader.go), BulkRefreshMapEntries runs BulkUpdateMapEntry first and returns on its error, so a single malformed key aborts the entire refresh including step 3, the stale-key deletion sweep. One bad key therefore wedges both additions and deletions indefinitely.
What you expected to happen:
IPv6 except CIDRs should be filtered on IPv4 clusters the same way the parent IPv6 rule is (and vice versa), so a dual-stack-defensive policy is inert rather than destructive on a single-stack cluster. Ideally the bulk refresh would also tolerate a per-key failure instead of aborting the whole refresh and the deletion phase.
How to reproduce it:
- IPv4-only EKS cluster with the VPC CNI network policy agent enabled (standard mode).
- Apply a NetworkPolicy with
policyTypes: [Egress] containing normal v4 rules plus:
- to:
- ipBlock:
cidr: "::/0"
except:
- "100::/64"
- "fe80::/10"
ports:
- protocol: TCP
port: 443
- Watch
/var/log/aws-routed-eni/network-policy-agent.log: every reconcile for the selected pods logs unable to update map: invalid argument, and the pod's egress map (via aws-eks-na-cli ebpf dump-maps) never converges with the PolicyEndpoint after endpoint churn.
Environment:
- EKS, IPv4 cluster, network policy standard mode
- amazon-k8s-cni
v1.22.4-eksbuild.3, aws-network-policy-agent v1.4.0-eksbuild.1
- aws-ebpf-sdk-go
v1.0.15
What happened:
On an IPv4-only EKS cluster, a NetworkPolicy egress rule with an IPv6 ipBlock that carries
exceptentries permanently breaks eBPF map reconciliation for every pod the policy selects. Every egress map refresh fails with:The failing key decodes to prefix length 64 (little endian
0x40 0x00 0x00 0x00) with 4 bytes of data0x01 0x00 0x00 0x00, which is100::/64(one of our v6 except entries) encoded as an IPv4 LPM trie key. A prefix length of 64 on a 4-byte-data LPM trie is what the kernel rejects with EINVAL.Because the refresh aborts on the first bad key, the pod's egress map silently freezes at whatever it contained when the probes were first attached. Endpoint churn (deploys of the destination workloads) leaves the map pointing at stale pod IPs, and the stale-key deletion phase never runs at all, so the map also retains allow entries for IPs that have since been recycled to unrelated pods. We observed 3042 aborted refreshes over 7 days in production before finding it; removing the IPv6 ipBlock from the policy made the very next reconcile succeed and purge 10 stale entries.
Root cause (present in v1.4.0 and still on main):
In
pkg/fwruleprocessor/fw_rule_processor.go,ComputeMapEntriesFromEndpointRules:firewallRule.ExceptintoexceptCidrsBEFORE thef.shouldSkipRule(...)check that filters IPv6 CIDRs on IPv4 clusters. So the::/0rule itself is skipped correctly, but its v6 except entries survive.for exceptCidr := range exceptCidrsloop adds each except as a deny entry intocidrsMapwith no v4/v6 filtering of its own.utils.ComputeTrieKey(*firewallMapKey, f.enableIPv6)withenableIPv6=false, producing a malformed v4-shaped key (v6 prefix length plus the first 4 bytes of the v6 address) for each v6 except.Then in aws-ebpf-sdk-go (
v1.0.15,pkg/maps/loader.go),BulkRefreshMapEntriesrunsBulkUpdateMapEntryfirst and returns on its error, so a single malformed key aborts the entire refresh including step 3, the stale-key deletion sweep. One bad key therefore wedges both additions and deletions indefinitely.What you expected to happen:
IPv6 except CIDRs should be filtered on IPv4 clusters the same way the parent IPv6 rule is (and vice versa), so a dual-stack-defensive policy is inert rather than destructive on a single-stack cluster. Ideally the bulk refresh would also tolerate a per-key failure instead of aborting the whole refresh and the deletion phase.
How to reproduce it:
policyTypes: [Egress]containing normal v4 rules plus:/var/log/aws-routed-eni/network-policy-agent.log: every reconcile for the selected pods logsunable to update map: invalid argument, and the pod's egress map (viaaws-eks-na-cli ebpf dump-maps) never converges with the PolicyEndpoint after endpoint churn.Environment:
v1.22.4-eksbuild.3, aws-network-policy-agentv1.4.0-eksbuild.1v1.0.15