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
This commit is contained in:
1 parent
9261a48983
commit
685d084af7
13 files changed
+1099
-1
No files matched your search
+220
@@ -0,0 +1,220 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netlink/nl"
|
||||
"github.com/vishvananda/netns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// A pod's port is locked to the pod's own addresses: what the pod sends in through the host side of its veth has to
|
||||
// carry the pod's MAC as its Ethernet source and be either IPv4 from the pod's address or ARP speaking for exactly
|
||||
// that MAC and address (or probing, from 0.0.0.0). Everything else is dropped before the bridge sees it, so a pod can
|
||||
// claim neither another pod's identity nor a node's, on its own node or, through weave, on any other.
|
||||
//
|
||||
// The lock is a set of tc filters on the veth's ingress qdisc. They match the way iproute2 lays out "ether src"
|
||||
// (the Ethernet source at -8 and -4 from the network header) and "ip src" (at 12), and they go away with the veth.
|
||||
|
||||
const (
|
||||
portLockPrioIPv4 = 1
|
||||
portLockPrioARP = 2
|
||||
portLockPrioProbe = 3
|
||||
portLockPrioDrop = 9
|
||||
)
|
||||
|
||||
var ingressParent = netlink.MakeHandle(0xffff, 0)
|
||||
|
||||
// PortLockFilters are the filters that lock the port with the given index to a pod's MAC and IPv4 address.
|
||||
func PortLockFilters(index int, mac net.HardwareAddr, ip net.IP) ([]netlink.Filter, error) {
|
||||
if len(mac) != 6 {
|
||||
return nil, fmt.Errorf("port lock needs an Ethernet MAC, got %q", mac)
|
||||
}
|
||||
ip4 := ip.To4()
|
||||
if ip4 == nil {
|
||||
return nil, fmt.Errorf("port lock needs an IPv4 address, got %q", ip)
|
||||
}
|
||||
src := etherSource(mac)
|
||||
arp := func(spa net.IP) []netlink.TcU32Key {
|
||||
keys := append([]netlink.TcU32Key{}, src...)
|
||||
return append(keys,
|
||||
// Ethernet/IPv4 ARP: hardware type 1, protocol 0x0800, address lengths 6 and 4 (any operation)
|
||||
netlink.TcU32Key{Mask: 0xffffffff, Val: 0x00010800, Off: 0},
|
||||
netlink.TcU32Key{Mask: 0xffff0000, Val: 0x06040000, Off: 4},
|
||||
// the sender hardware address (8..13) and the sender protocol address (14..17)
|
||||
netlink.TcU32Key{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(mac[0:4]), Off: 8},
|
||||
netlink.TcU32Key{Mask: 0xffffffff, Val: uint32(binary.BigEndian.Uint16(mac[4:6]))<<16 | uint32(binary.BigEndian.Uint16(spa[0:2])), Off: 12},
|
||||
netlink.TcU32Key{Mask: 0xffff0000, Val: uint32(binary.BigEndian.Uint16(spa[2:4])) << 16, Off: 16},
|
||||
)
|
||||
}
|
||||
ipv4 := append(append([]netlink.TcU32Key{}, src...), netlink.TcU32Key{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(ip4), Off: 12})
|
||||
return []netlink.Filter{
|
||||
portLockU32(index, portLockPrioIPv4, unix.ETH_P_IP, ipv4),
|
||||
portLockU32(index, portLockPrioARP, unix.ETH_P_ARP, arp(ip4)),
|
||||
portLockU32(index, portLockPrioProbe, unix.ETH_P_ARP, arp(net.IPv4zero.To4())),
|
||||
&netlink.MatchAll{
|
||||
FilterAttrs: netlink.FilterAttrs{LinkIndex: index, Parent: ingressParent, Priority: portLockPrioDrop, Protocol: unix.ETH_P_ALL},
|
||||
Actions: []netlink.Action{gact(netlink.TC_ACT_SHOT)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// etherSource matches the Ethernet source address, which sits 8 bytes before the network header.
|
||||
func etherSource(mac net.HardwareAddr) []netlink.TcU32Key {
|
||||
return []netlink.TcU32Key{
|
||||
{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(mac[0:4]), Off: -8},
|
||||
{Mask: 0xffff0000, Val: uint32(binary.BigEndian.Uint16(mac[4:6])) << 16, Off: -4},
|
||||
}
|
||||
}
|
||||
|
||||
func portLockU32(index int, prio uint16, proto uint16, keys []netlink.TcU32Key) *netlink.U32 {
|
||||
// clipped: the vendored netlink serialises cap(Keys) keys, and spare capacity would go out as extra all-zero keys
|
||||
keys = keys[:len(keys):len(keys)]
|
||||
return &netlink.U32{
|
||||
FilterAttrs: netlink.FilterAttrs{LinkIndex: index, Parent: ingressParent, Priority: prio, Protocol: proto},
|
||||
Sel: &netlink.TcU32Sel{Flags: nl.TC_U32_TERMINAL, Keys: keys},
|
||||
Actions: []netlink.Action{gact(netlink.TC_ACT_OK)},
|
||||
}
|
||||
}
|
||||
|
||||
func gact(action netlink.TcAct) netlink.Action {
|
||||
return &netlink.GenericAction{ActionAttrs: netlink.ActionAttrs{Action: action}}
|
||||
}
|
||||
|
||||
func ingressQdisc(index int) *netlink.Ingress {
|
||||
return &netlink.Ingress{QdiscAttrs: netlink.QdiscAttrs{LinkIndex: index, Handle: ingressParent, Parent: netlink.HANDLE_INGRESS}}
|
||||
}
|
||||
|
||||
// LockPort locks a pod's port (the host side of its veth) to the pod's MAC and IPv4 address. A port already locked
|
||||
// to them is left alone; anything else on its ingress is replaced, and the pod's own traffic is let through before
|
||||
// the rest is dropped, so a running pod never loses its own frames.
|
||||
func LockPort(link netlink.Link, mac net.HardwareAddr, ip net.IP) error {
|
||||
index := link.Attrs().Index
|
||||
want, err := PortLockFilters(index, mac, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if locked, _ := portHasFilters(link, want); locked {
|
||||
return nil
|
||||
}
|
||||
// removing the ingress qdisc takes its filters with it
|
||||
if err := netlink.QdiscDel(ingressQdisc(index)); err != nil && !errors.Is(err, unix.ENOENT) && !errors.Is(err, unix.EINVAL) {
|
||||
return fmt.Errorf("clearing ingress of %s: %w", link.Attrs().Name, err)
|
||||
}
|
||||
if err := netlink.QdiscAdd(ingressQdisc(index)); err != nil {
|
||||
return fmt.Errorf("adding ingress qdisc to %s: %w", link.Attrs().Name, err)
|
||||
}
|
||||
for _, f := range want {
|
||||
if err := netlink.FilterAdd(f); err != nil {
|
||||
return fmt.Errorf("adding port lock filter (prio %d) to %s: %w", f.Attrs().Priority, link.Attrs().Name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LockContainerPort locks the port of a pod attached by the CNI plugin: the host side of the veth whose container
|
||||
// end is ifName in ns, to that end's MAC and the pod's address.
|
||||
func LockContainerPort(ns netns.NsHandle, ifName string, ip net.IP) error {
|
||||
var mac net.HardwareAddr
|
||||
peer := 0
|
||||
if err := WithNetNSLink(ns, ifName, func(link netlink.Link) error {
|
||||
// a veth's link attribute is its peer's index, in the peer's namespace: the host's
|
||||
mac, peer = link.Attrs().HardwareAddr, link.Attrs().ParentIndex
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if peer == 0 {
|
||||
return fmt.Errorf("%s has no peer: not a veth", ifName)
|
||||
}
|
||||
host, err := netlink.LinkByIndex(peer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("host side of %s: %w", ifName, err)
|
||||
}
|
||||
if host.Type() != "veth" || !strings.HasPrefix(host.Attrs().Name, vethPrefix+"pl") {
|
||||
return fmt.Errorf("host side of %s is %s (%s), not a weave pod veth", ifName, host.Attrs().Name, host.Type())
|
||||
}
|
||||
return LockPort(host, mac, ip)
|
||||
}
|
||||
|
||||
// PortLocked reports whether a port is locked to exactly this MAC and address.
|
||||
func PortLocked(link netlink.Link, mac net.HardwareAddr, ip net.IP) (bool, error) {
|
||||
want, err := PortLockFilters(link.Attrs().Index, mac, ip)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return portHasFilters(link, want)
|
||||
}
|
||||
|
||||
func portHasFilters(link netlink.Link, want []netlink.Filter) (bool, error) {
|
||||
have, err := netlink.FilterList(link, ingressParent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var got []netlink.Filter
|
||||
for _, f := range have {
|
||||
// u32 lists the hash table that holds a filter as a filter of its own, without a selector
|
||||
if u, ok := f.(*netlink.U32); ok && u.Sel == nil {
|
||||
continue
|
||||
}
|
||||
got = append(got, f)
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
return false, nil
|
||||
}
|
||||
for _, w := range want {
|
||||
found := false
|
||||
for _, g := range got {
|
||||
if sameFilter(g, w) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func sameFilter(a, b netlink.Filter) bool {
|
||||
if a.Attrs().Priority != b.Attrs().Priority || a.Attrs().Protocol != b.Attrs().Protocol || a.Type() != b.Type() {
|
||||
return false
|
||||
}
|
||||
switch fa := a.(type) {
|
||||
case *netlink.U32:
|
||||
fb := b.(*netlink.U32)
|
||||
if fa.Sel == nil || fb.Sel == nil || len(fa.Sel.Keys) != len(fb.Sel.Keys) || fa.Sel.Flags&nl.TC_U32_TERMINAL == 0 {
|
||||
return false
|
||||
}
|
||||
for i := range fa.Sel.Keys {
|
||||
ka, kb := fa.Sel.Keys[i], fb.Sel.Keys[i]
|
||||
if ka.Mask != kb.Mask || ka.Val != kb.Val || ka.Off != kb.Off {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return sameActions(fa.Actions, fb.Actions)
|
||||
case *netlink.MatchAll:
|
||||
return sameActions(fa.Actions, b.(*netlink.MatchAll).Actions)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sameActions(a, b []netlink.Action) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
ga, ok1 := a[i].(*netlink.GenericAction)
|
||||
gb, ok2 := b[i].(*netlink.GenericAction)
|
||||
if !ok1 || !ok2 || ga.Attrs().Action != gb.Attrs().Action {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
"github.com/vishvananda/netns"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// TestPortLockInKernel builds a bridge with two pods in throwaway network namespaces, locks pod A's port and sends
|
||||
// frames from A: what A may send reaches B, what claims anything but A's own addresses does not. It needs root.
|
||||
func TestPortLockInKernel(t *testing.T) {
|
||||
if os.Geteuid() != 0 {
|
||||
t.Skip("needs root (creates network namespaces)")
|
||||
}
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
orig, err := netns.Get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer orig.Close()
|
||||
defer netns.Set(orig)
|
||||
|
||||
hub, err := netns.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer hub.Close()
|
||||
podA, _ := netns.New()
|
||||
defer podA.Close()
|
||||
podB, _ := netns.New()
|
||||
defer podB.Close()
|
||||
must(t, netns.Set(hub))
|
||||
|
||||
br := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br0"}}
|
||||
must(t, netlink.LinkAdd(br))
|
||||
must(t, netlink.LinkSetUp(br))
|
||||
ipA, ipB := net.IPv4(10, 99, 0, 1).To4(), net.IPv4(10, 99, 0, 2).To4()
|
||||
hostA, macA := attach(t, hub, podA, br, "vethwepltsta", ipA)
|
||||
_, macB := attach(t, hub, podB, br, "vethwepltstb", ipB)
|
||||
|
||||
// as the CNI plugin does it: from the pod's end of the veth, in the pod's namespace
|
||||
must(t, LockContainerPort(podA, "eth0", ipA))
|
||||
if ok, err := PortLocked(hostA, macA, ipA); err != nil || !ok {
|
||||
dumpIngress(t, hostA)
|
||||
t.Fatalf("port not locked after LockContainerPort: %v %v", ok, err)
|
||||
}
|
||||
// locking again changes nothing (the filters are compared, not rebuilt)
|
||||
must(t, LockPort(hostA, macA, ipA))
|
||||
if ok, _ := PortLocked(hostA, macA, ipA); !ok {
|
||||
t.Fatal("port lost its lock on a repeated LockPort")
|
||||
}
|
||||
|
||||
send := socketIn(t, podA, "eth0")
|
||||
recv := socketIn(t, podB, "eth0")
|
||||
defer unix.Close(send)
|
||||
defer unix.Close(recv)
|
||||
other, _ := net.ParseMAC("02:00:00:00:00:99")
|
||||
spoofIP := net.IPv4(10, 99, 0, 200).To4()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
frame []byte
|
||||
passes bool
|
||||
}{
|
||||
{"ipv4 from its own addresses", ipv4Frame(macB, macA, ipA, ipB, "own-ip"), true},
|
||||
{"ipv4 from another MAC", ipv4Frame(macB, other, ipA, ipB, "mac-spoof"), false},
|
||||
{"ipv4 from another address", ipv4Frame(macB, macA, spoofIP, ipB, "ip-spoof"), false},
|
||||
{"arp for itself", arpFrame(macA, macA, ipA, ipB, "own-arp"), true},
|
||||
{"arp probe", arpFrame(macA, macA, net.IPv4zero.To4(), ipB, "probe"), true},
|
||||
{"arp claiming another address", arpFrame(macA, macA, spoofIP, ipB, "arp-ip-spoof"), false},
|
||||
{"arp claiming another MAC", arpFrame(macA, other, ipA, ipB, "arp-sha-spoof"), false},
|
||||
{"arp from another MAC", arpFrame(other, other, ipA, ipB, "arp-eth-spoof"), false},
|
||||
{"ipv6", etherFrame(macB, macA, 0x86dd, []byte("ipv6-frame")), false},
|
||||
{"other ethertype", etherFrame(macB, macA, 0x88b5, []byte("experimental")), false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
drain(recv)
|
||||
if _, err := unix.Write(send, c.frame); err != nil {
|
||||
t.Fatalf("%s: send: %v", c.name, err)
|
||||
}
|
||||
got := sawPayload(recv, c.frame[len(c.frame)-12:], 300*time.Millisecond)
|
||||
if got != c.passes {
|
||||
t.Errorf("%s: reached B = %v, want %v", c.name, got, c.passes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dumpIngress(t *testing.T, link netlink.Link) {
|
||||
have, _ := netlink.FilterList(link, netlink.MakeHandle(0xffff, 0))
|
||||
for _, f := range have {
|
||||
t.Logf("have %s %+v", f.Type(), f.Attrs())
|
||||
if u, ok := f.(*netlink.U32); ok && u.Sel != nil {
|
||||
t.Logf(" sel flags %x keys %+v actions %+v", u.Sel.Flags, u.Sel.Keys, u.Actions)
|
||||
}
|
||||
if m, ok := f.(*netlink.MatchAll); ok {
|
||||
t.Logf(" actions %+v", m.Actions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func must(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// attach plugs a pod namespace into the bridge through a veth named host on the hub side, eth0 on the pod side.
|
||||
func attach(t *testing.T, hub, pod netns.NsHandle, br netlink.Link, host string, ip net.IP) (netlink.Link, net.HardwareAddr) {
|
||||
t.Helper()
|
||||
must(t, netlink.LinkAdd(&netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: host}, PeerName: "tmp" + host[len(host)-2:]}))
|
||||
h, err := netlink.LinkByName(host)
|
||||
must(t, err)
|
||||
must(t, netlink.LinkSetMaster(h, br))
|
||||
must(t, netlink.LinkSetUp(h))
|
||||
p, err := netlink.LinkByName("tmp" + host[len(host)-2:])
|
||||
must(t, err)
|
||||
must(t, netlink.LinkSetNsFd(p, int(pod)))
|
||||
must(t, netns.Set(pod))
|
||||
p, err = netlink.LinkByName("tmp" + host[len(host)-2:])
|
||||
must(t, err)
|
||||
must(t, netlink.LinkSetName(p, "eth0"))
|
||||
must(t, netlink.AddrAdd(p, &netlink.Addr{IPNet: &net.IPNet{IP: ip, Mask: net.CIDRMask(24, 32)}}))
|
||||
must(t, netlink.LinkSetUp(p))
|
||||
mac := p.Attrs().HardwareAddr
|
||||
must(t, netns.Set(hub))
|
||||
return h, mac
|
||||
}
|
||||
|
||||
// socketIn opens a raw packet socket on an interface of a namespace (the socket stays in that namespace).
|
||||
func socketIn(t *testing.T, ns netns.NsHandle, ifName string) int {
|
||||
t.Helper()
|
||||
cur, _ := netns.Get()
|
||||
defer cur.Close()
|
||||
must(t, netns.Set(ns))
|
||||
defer netns.Set(cur)
|
||||
link, err := netlink.LinkByName(ifName)
|
||||
must(t, err)
|
||||
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL)))
|
||||
must(t, err)
|
||||
must(t, unix.Bind(fd, &unix.SockaddrLinklayer{Protocol: htons(unix.ETH_P_ALL), Ifindex: link.Attrs().Index}))
|
||||
return fd
|
||||
}
|
||||
|
||||
func htons(v uint16) uint16 { return v<<8 | v>>8 }
|
||||
|
||||
func drain(fd int) {
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
if _, _, err := unix.Recvfrom(fd, buf, unix.MSG_DONTWAIT); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sawPayload(fd int, marker []byte, within time.Duration) bool {
|
||||
buf := make([]byte, 2048)
|
||||
deadline := time.Now().Add(within)
|
||||
for time.Now().Before(deadline) {
|
||||
n, _, err := unix.Recvfrom(fd, buf, unix.MSG_DONTWAIT)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if bytes.Contains(buf[:n], marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func etherFrame(dst, src net.HardwareAddr, ethertype uint16, payload []byte) []byte {
|
||||
f := make([]byte, 14, 14+len(payload)+12)
|
||||
copy(f[0:6], dst)
|
||||
copy(f[6:12], src)
|
||||
binary.BigEndian.PutUint16(f[12:14], ethertype)
|
||||
f = append(f, payload...)
|
||||
// a fixed-size tail the receiver looks for, so each case is told apart
|
||||
tail := make([]byte, 12)
|
||||
copy(tail, payload)
|
||||
return append(f, tail...)
|
||||
}
|
||||
|
||||
func ipv4Frame(dst, src net.HardwareAddr, from, to net.IP, tag string) []byte {
|
||||
body := []byte(tag)
|
||||
h := make([]byte, 20)
|
||||
h[0] = 0x45
|
||||
binary.BigEndian.PutUint16(h[2:4], uint16(20+len(body)+12))
|
||||
h[8] = 64
|
||||
h[9] = 253 // experimental protocol: nothing answers it
|
||||
copy(h[12:16], from)
|
||||
copy(h[16:20], to)
|
||||
binary.BigEndian.PutUint16(h[10:12], checksum(h))
|
||||
return etherFrame(dst, src, unix.ETH_P_IP, append(h, body...))
|
||||
}
|
||||
|
||||
func arpFrame(ethSrc, sha net.HardwareAddr, spa, tpa net.IP, tag string) []byte {
|
||||
a := make([]byte, 28)
|
||||
binary.BigEndian.PutUint16(a[0:2], 1)
|
||||
binary.BigEndian.PutUint16(a[2:4], unix.ETH_P_IP)
|
||||
a[4], a[5] = 6, 4
|
||||
binary.BigEndian.PutUint16(a[6:8], 1) // request
|
||||
copy(a[8:14], sha)
|
||||
copy(a[14:18], spa)
|
||||
copy(a[24:28], tpa)
|
||||
bcast, _ := net.ParseMAC("ff:ff:ff:ff:ff:ff")
|
||||
return etherFrame(bcast, ethSrc, unix.ETH_P_ARP, append(a, []byte(tag)...))
|
||||
}
|
||||
|
||||
func checksum(b []byte) uint16 {
|
||||
var s uint32
|
||||
for i := 0; i+1 < len(b); i += 2 {
|
||||
s += uint32(binary.BigEndian.Uint16(b[i:]))
|
||||
}
|
||||
for s>>16 != 0 {
|
||||
s = s&0xffff + s>>16
|
||||
}
|
||||
return ^uint16(s)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user