images / images (push) Successful in 7m6s
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
115 lines
3.3 KiB
Go
115 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/vishvananda/netns"
|
|
|
|
weavenet "github.com/rajch/weave/net"
|
|
)
|
|
|
|
// lockPorts locks the ports of pods that were attached before the CNI plugin locked ports itself (it locks every
|
|
// pod it attaches). It works from the runtime's own record of each attachment, libcni's result cache: the pod's
|
|
// network namespace, the interface the plugin set up in it and the address it gave it. Ports already locked are
|
|
// left alone; pods whose namespace is gone are skipped.
|
|
func lockPorts(args []string) error {
|
|
if len(args) != 2 {
|
|
cmdUsage("lock-ports", "<cni-result-cache-dir> <network-name>")
|
|
}
|
|
dir, network := args[0], args[1]
|
|
entries, err := os.ReadDir(dir)
|
|
if os.IsNotExist(err) {
|
|
return nil // nothing attached yet
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var locked, gone, failed int
|
|
for _, e := range entries {
|
|
if e.IsDir() || !strings.HasPrefix(e.Name(), network+"-") {
|
|
continue
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "lock-ports: %s: %v\n", e.Name(), err)
|
|
failed++
|
|
continue
|
|
}
|
|
a, err := parseAttachment(data)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "lock-ports: %s: %v\n", e.Name(), err)
|
|
failed++
|
|
continue
|
|
}
|
|
if a.network != network {
|
|
continue
|
|
}
|
|
ns, err := netns.GetFromPath(a.netns)
|
|
if os.IsNotExist(err) {
|
|
gone++ // the pod is gone; the runtime has yet to clear its record
|
|
continue
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "lock-ports: %s: namespace %s: %v\n", a.containerID, a.netns, err)
|
|
failed++
|
|
continue
|
|
}
|
|
err = weavenet.LockContainerPort(ns, a.ifName, a.ip)
|
|
ns.Close()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "lock-ports: %s (%s): %v\n", a.containerID, a.ip, err)
|
|
failed++
|
|
continue
|
|
}
|
|
locked++
|
|
}
|
|
fmt.Printf("lock-ports: %d locked, %d gone, %d failed\n", locked, gone, failed)
|
|
if failed > 0 {
|
|
return fmt.Errorf("lock-ports: %d pods' ports not locked", failed)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type attachment struct {
|
|
network, containerID, netns, ifName string
|
|
ip net.IP
|
|
}
|
|
|
|
// parseAttachment reads one libcni cache entry ("cniCacheV1"): which network attached which container, in which
|
|
// namespace, through which interface, with which IPv4 address.
|
|
func parseAttachment(data []byte) (attachment, error) {
|
|
var c struct {
|
|
Kind string `json:"kind"`
|
|
ContainerID string `json:"containerId"`
|
|
IfName string `json:"ifName"`
|
|
NetworkName string `json:"networkName"`
|
|
NetNS string `json:"netns"`
|
|
Result struct {
|
|
IPs []struct {
|
|
Address string `json:"address"`
|
|
} `json:"ips"`
|
|
} `json:"result"`
|
|
}
|
|
if err := json.Unmarshal(data, &c); err != nil {
|
|
return attachment{}, err
|
|
}
|
|
if c.Kind != "cniCacheV1" {
|
|
return attachment{}, fmt.Errorf("unknown cache entry kind %q", c.Kind)
|
|
}
|
|
if c.NetNS == "" || c.IfName == "" {
|
|
return attachment{}, fmt.Errorf("cache entry for %s names no namespace or interface", c.ContainerID)
|
|
}
|
|
for _, a := range c.Result.IPs {
|
|
ip, _, err := net.ParseCIDR(a.Address)
|
|
if err == nil && ip.To4() != nil {
|
|
return attachment{network: c.NetworkName, containerID: c.ContainerID, netns: c.NetNS, ifName: c.IfName, ip: ip.To4()}, nil
|
|
}
|
|
}
|
|
return attachment{}, fmt.Errorf("cache entry for %s has no IPv4 address", c.ContainerID)
|
|
}
|