Files
weave/net/portlock_test.go
levaliciousandClaude Opus 5.5 685d084af7
images / images (push) Successful in 7m6s
cni: lock each pod's port to the pod's own addresses
A pod could put any source MAC or address on the wire: weave has no layer-2
anti-spoofing, so a pod with NET_RAW could pose as another pod or as a node's
bridge, on its own node (local bridging never reaches the router) and, through
weave, on others. That undercut every rule that trusts a source MAC or address,
including the egress holder's check of which node a tunnel packet came from.

The CNI plugin now locks the host side of each pod's veth with tc ingress
filters: IPv4 from the pod's MAC and address, ARP speaking for exactly those
(or probing from 0.0.0.0) pass; everything else is dropped before the bridge
sees it. The u32 keys follow iproute2's layout for "ether src" (-8, -4) and
"ip src" (12). ADD fails if the port cannot be locked, so an unlocked pod never
starts. The filters go away with the veth.

Pods attached before this plugin get locked by weave-init: "weaveutil
lock-ports" walks libcni's result cache (each attachment's netns, interface and
address) and locks their ports, leaving locked ones alone. weave-init mounts
the cache and /var/run/netns read-only for it.

TestPortLockInKernel (root) builds a bridge and two pods in throwaway
namespaces and checks what reaches the other pod; reweave/test/portlock runs
the same against a live cluster, across nodes, after checking every port is
locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 22:51:16 -04:00

116 lines
4.2 KiB
Go

package net
import (
"net"
"testing"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// The keys iproute2 writes for "u32 match ether src 02:11:22:33:44:55 match ip src 10.32.0.7/32" (tc filter show,
// iproute2 6.x): the port lock has to match the same bytes.
func TestPortLockMatchesIproute2Layout(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
fs, err := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
if err != nil {
t.Fatal(err)
}
if len(fs) != 4 {
t.Fatalf("want 4 filters, got %d", len(fs))
}
ipv4 := fs[0].(*netlink.U32)
want := []netlink.TcU32Key{
{Val: 0x02112233, Mask: 0xffffffff, Off: -8},
{Val: 0x44550000, Mask: 0xffff0000, Off: -4},
{Val: 0x0a200007, Mask: 0xffffffff, Off: 12},
}
if ipv4.Protocol != unix.ETH_P_IP || ipv4.Priority != portLockPrioIPv4 || ipv4.Sel.Flags&nl.TC_U32_TERMINAL == 0 {
t.Fatalf("ipv4 filter attrs: %+v flags %x", ipv4.FilterAttrs, ipv4.Sel.Flags)
}
assertKeys(t, "ipv4", ipv4.Sel.Keys, want)
for _, f := range fs {
if f.Attrs().LinkIndex != 7 || f.Attrs().Parent != netlink.MakeHandle(0xffff, 0) {
t.Fatalf("filter not on ingress of link 7: %+v", f.Attrs())
}
}
}
func TestPortLockARP(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
fs, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
common := []netlink.TcU32Key{
{Val: 0x02112233, Mask: 0xffffffff, Off: -8},
{Val: 0x44550000, Mask: 0xffff0000, Off: -4},
{Val: 0x00010800, Mask: 0xffffffff, Off: 0},
{Val: 0x06040000, Mask: 0xffff0000, Off: 4},
{Val: 0x02112233, Mask: 0xffffffff, Off: 8},
}
own := append(append([]netlink.TcU32Key{}, common...),
netlink.TcU32Key{Val: 0x44550a20, Mask: 0xffffffff, Off: 12}, // sha 44:55, spa 10.32
netlink.TcU32Key{Val: 0x00070000, Mask: 0xffff0000, Off: 16}, // spa .0.7
)
probe := append(append([]netlink.TcU32Key{}, common...),
netlink.TcU32Key{Val: 0x44550000, Mask: 0xffffffff, Off: 12},
netlink.TcU32Key{Val: 0x00000000, Mask: 0xffff0000, Off: 16},
)
arp, pr := fs[1].(*netlink.U32), fs[2].(*netlink.U32)
if arp.Protocol != unix.ETH_P_ARP || pr.Protocol != unix.ETH_P_ARP {
t.Fatal("ARP filters must be protocol arp")
}
assertKeys(t, "arp", arp.Sel.Keys, own)
assertKeys(t, "probe", pr.Sel.Keys, probe)
drop := fs[3].(*netlink.MatchAll)
if drop.Protocol != unix.ETH_P_ALL || drop.Priority != portLockPrioDrop {
t.Fatalf("drop filter attrs: %+v", drop.FilterAttrs)
}
if a := drop.Actions[0].(*netlink.GenericAction); a.Attrs().Action != netlink.TC_ACT_SHOT {
t.Fatalf("last filter must drop, has action %v", a.Attrs().Action)
}
for _, f := range fs[:3] {
if a := f.(*netlink.U32).Actions[0].(*netlink.GenericAction); a.Attrs().Action != netlink.TC_ACT_OK {
t.Fatalf("prio %d must pass, has action %v", f.Attrs().Priority, a.Attrs().Action)
}
}
}
func TestPortLockRefusesWhatItCannotPin(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
if _, err := PortLockFilters(7, mac, net.ParseIP("fd00::1")); err == nil {
t.Fatal("an IPv6 address must be refused: the lock pins IPv4 and ARP")
}
long, _ := net.ParseMAC("02:11:22:33:44:55:66:77")
if _, err := PortLockFilters(7, long, net.ParseIP("10.32.0.7")); err == nil {
t.Fatal("a non-Ethernet hardware address must be refused")
}
}
func TestSameFilterComparesKeysAndActions(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
a, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
b, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.8"))
if !sameFilter(a[0], a[0]) || sameFilter(a[0], b[0]) {
t.Fatal("ipv4 filters for different addresses must differ")
}
if !sameFilter(a[3], b[3]) {
t.Fatal("the drop filters are the same for every pod")
}
if sameFilter(a[1], a[2]) {
t.Fatal("the ARP and probe filters differ in priority and keys")
}
}
func assertKeys(t *testing.T, what string, got, want []netlink.TcU32Key) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("%s: want %d keys, got %d: %+v", what, len(want), len(got), got)
}
for i := range want {
if got[i].Val != want[i].Val || got[i].Mask != want[i].Mask || got[i].Off != want[i].Off {
t.Fatalf("%s key %d: want %08x/%08x at %d, got %08x/%08x at %d", what, i,
want[i].Val, want[i].Mask, want[i].Off, got[i].Val, got[i].Mask, got[i].Off)
}
}
}