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
266 lines
8.2 KiB
Go
266 lines
8.2 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/containernetworking/cni/pkg/skel"
|
|
"github.com/containernetworking/cni/pkg/types"
|
|
current "github.com/containernetworking/cni/pkg/types/100"
|
|
"github.com/containernetworking/plugins/pkg/ipam"
|
|
weaveapi "github.com/rajch/weave/api"
|
|
"github.com/rajch/weave/lb/admit"
|
|
weavenet "github.com/rajch/weave/net"
|
|
ipamplugin "github.com/rajch/weave/plugin/ipam"
|
|
"github.com/vishvananda/netlink"
|
|
"github.com/vishvananda/netns"
|
|
)
|
|
|
|
var (
|
|
zeroNetwork = net.IPNet{IP: net.IPv4zero, Mask: net.IPv4Mask(0, 0, 0, 0)}
|
|
mask32 = net.IPv4Mask(0xff, 0xff, 0xff, 0xff)
|
|
)
|
|
|
|
type CNIPlugin struct {
|
|
weave *weaveapi.Client
|
|
}
|
|
|
|
func NewCNIPlugin(weave *weaveapi.Client) *CNIPlugin {
|
|
return &CNIPlugin{weave: weave}
|
|
}
|
|
|
|
func loadNetConf(bytes []byte) (*NetConf, error) {
|
|
n := &NetConf{
|
|
BrName: weavenet.WeaveBridgeName,
|
|
HairpinMode: true,
|
|
}
|
|
if err := json.Unmarshal(bytes, n); err != nil {
|
|
return nil, fmt.Errorf("failed to load netconf: %v", err)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (c *CNIPlugin) getIP(ipamType string, args *skel.CmdArgs) (newResult *current.Result, err error) {
|
|
var result types.Result
|
|
// Default IPAM is Weave's own
|
|
if ipamType == "" {
|
|
result, err = ipamplugin.NewIpam(c.weave).Allocate(args)
|
|
} else {
|
|
result, err = ipam.ExecAdd(ipamType, args.StdinData)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil {
|
|
return nil, fmt.Errorf("Received no usable result from IPAM plugin")
|
|
}
|
|
newResult, err = current.NewResultFromResult(result)
|
|
// Check if ipam returned no results without error
|
|
if err == nil && len(newResult.IPs) == 0 {
|
|
return nil, fmt.Errorf("IPAM plugin failed to allocate IP address")
|
|
}
|
|
return newResult, err
|
|
}
|
|
|
|
func (c *CNIPlugin) CmdAdd(args *skel.CmdArgs) error {
|
|
conf, err := loadNetConf(args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if conf.IsGW {
|
|
return fmt.Errorf("Gateway functionality not supported")
|
|
}
|
|
if conf.IPMasq {
|
|
return fmt.Errorf("IP Masquerading functionality not supported")
|
|
}
|
|
|
|
result, err := c.getIP(conf.IPAM.Type, args)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to allocate IP address: %s", err)
|
|
}
|
|
// Only expecting one address
|
|
ip := result.IPs[0]
|
|
|
|
// An EgressService member's first packet must already leave from its Service's address (or not at all):
|
|
// weave-npc installs the pod's egress rules before the pod has an interface. No answer, no pod (the
|
|
// runtime calls DEL, which releases the address, and kubelet retries).
|
|
if conf.EgressAdmission {
|
|
if ns, name := admit.PodFromArgs(args.Args); ns != "" && name != "" {
|
|
if _, err := admit.Admit(context.Background(), admit.SocketPath,
|
|
admit.Request{Namespace: ns, Name: name, IP: ip.Address.IP.String()}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// If config says nothing about routes or gateway, default one will be via the bridge
|
|
if len(result.Routes) == 0 && ip.Gateway == nil {
|
|
bridgeIP, err := weavenet.FindBridgeIP(conf.BrName, &ip.Address)
|
|
if err == weavenet.ErrBridgeNoIP {
|
|
bridgeArgs := *args
|
|
bridgeArgs.ContainerID = "weave:expose"
|
|
// It would be better if libcni let us send just the desired parameters,
|
|
// but there is a bug: https://github.com/containernetworking/cni/issues/410
|
|
// so just blank out the one we want to change
|
|
os.Setenv("CNI_CONTAINERID", bridgeArgs.ContainerID)
|
|
bridgeIPResult, err := c.getIP(conf.IPAM.Type, &bridgeArgs)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to allocate IP address for bridge: %s", err)
|
|
}
|
|
bridgeCIDR := bridgeIPResult.IPs[0].Address
|
|
|
|
if err := c.weave.Expose(&bridgeCIDR); err != nil {
|
|
return fmt.Errorf("unable to expose bridge %q: %s", conf.BrName, err)
|
|
}
|
|
|
|
bridgeIP = bridgeCIDR.IP
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
result.IPs[0].Gateway = bridgeIP
|
|
}
|
|
|
|
ns, err := netns.GetFromPath(args.Netns)
|
|
if err != nil {
|
|
return fmt.Errorf("error accessing namespace %q: %s", args.Netns, err)
|
|
}
|
|
defer ns.Close()
|
|
|
|
hostNs, err := netns.Get()
|
|
if err != nil {
|
|
return fmt.Errorf("error accessing host network namespace: %s", err)
|
|
}
|
|
defer hostNs.Close()
|
|
|
|
if ns.Equal(hostNs) {
|
|
return fmt.Errorf("can not specify host network namespace as network namespace to which container should be added")
|
|
}
|
|
|
|
id := args.ContainerID
|
|
if len(id) < 5 {
|
|
data := make([]byte, 5)
|
|
_, err := rand.Reader.Read(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id = fmt.Sprintf("%x", data)
|
|
}
|
|
|
|
if err := weavenet.AttachContainer(args.Netns, id, args.IfName, conf.BrName, conf.MTU, false, []*net.IPNet{&ip.Address}, false, conf.HairpinMode); err != nil {
|
|
return err
|
|
}
|
|
if err := weavenet.WithNetNSLink(ns, args.IfName, func(link netlink.Link) error {
|
|
return setupRoutes(link, args.IfName, ip.Address, ip.Gateway, result.Routes)
|
|
}); err != nil {
|
|
return fmt.Errorf("error setting up routes: %s", err)
|
|
}
|
|
// the pod may only speak as itself: its port drops whatever claims another MAC or address. The pod has not
|
|
// started yet; if its port cannot be locked, it does not start.
|
|
if err := weavenet.LockContainerPort(ns, args.IfName, ip.Address.IP); err != nil {
|
|
return fmt.Errorf("error locking the pod's port: %s", err)
|
|
}
|
|
|
|
result.DNS = conf.DNS
|
|
return types.PrintResult(result, conf.CNIVersion)
|
|
}
|
|
|
|
func setupRoutes(link netlink.Link, name string, ipnet net.IPNet, gw net.IP, routes []*types.Route) error {
|
|
var err error
|
|
if routes == nil { // If config says nothing about routes, add a default one
|
|
if !ipnet.Contains(gw) {
|
|
// The bridge IP is not on the same subnet; add a specific route to it
|
|
gw32 := &net.IPNet{IP: gw, Mask: mask32}
|
|
if err = weavenet.AddRoute(link, netlink.SCOPE_LINK, gw32, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
routes = []*types.Route{{Dst: zeroNetwork}}
|
|
}
|
|
for _, r := range routes {
|
|
if r.GW != nil {
|
|
err = weavenet.AddRoute(link, netlink.SCOPE_UNIVERSE, &r.Dst, r.GW)
|
|
} else {
|
|
err = weavenet.AddRoute(link, netlink.SCOPE_UNIVERSE, &r.Dst, gw)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to add route '%v via %v dev %v': %v", r.Dst, gw, name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *CNIPlugin) CmdCheck(args *skel.CmdArgs) error {
|
|
// TODO: Figure out how to do a proper CNI check here
|
|
// For now, return not implemented error
|
|
|
|
return errors.New("CNI plugin check not implemented")
|
|
}
|
|
|
|
// As of CNI 0.5 spec:
|
|
//
|
|
// "Plugins should generally complete a DEL action without error even if some resources are missing"
|
|
//
|
|
// this method should therefore return nil in most, if not all, cases.
|
|
func (c *CNIPlugin) CmdDel(args *skel.CmdArgs) error {
|
|
conf, err := loadNetConf(args.StdinData)
|
|
if err != nil {
|
|
logOnStderr(err)
|
|
return nil
|
|
}
|
|
|
|
// As of CNI 0.3 spec, runtimes can send blank if they just want the address deallocated
|
|
if args.Netns != "" {
|
|
if err = weavenet.WithNetNSByPath(args.Netns, func() error {
|
|
link, err := netlink.LinkByName(args.IfName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return netlink.LinkDel(link)
|
|
}); err != nil {
|
|
// We log the error and carry on instead of returning nil, as there may still be resources to free in IPAM.
|
|
logOnStderr(fmt.Errorf("error removing interface %q: %s", args.IfName, err))
|
|
}
|
|
}
|
|
|
|
// Default IPAM is Weave's own
|
|
if conf.IPAM.Type == "" {
|
|
err = ipamplugin.NewIpam(c.weave).Release(args)
|
|
} else {
|
|
err = ipam.ExecDel(conf.IPAM.Type, args.StdinData)
|
|
}
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), fmt.Sprintf("Delete: no addresses for %s", args.ContainerID)) {
|
|
// ignore the error in the case where there are no IP addresses associated with container
|
|
fmt.Fprintln(os.Stderr, "weave-cni:", fmt.Sprintf("Delete: no addresses for %s", args.ContainerID))
|
|
return nil
|
|
}
|
|
logOnStderr(fmt.Errorf("unable to release IP address: %s", err))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Standard error is inherited from the caller (e.g. Kubelet, when running Weave Net's CNI plugin
|
|
// under Kubernetes) hence provided errors should eventually appear in the caller's log files.
|
|
func logOnStderr(err error) {
|
|
fmt.Fprintln(os.Stderr, "weave-cni:", err)
|
|
}
|
|
|
|
type NetConf struct {
|
|
types.NetConf
|
|
BrName string `json:"bridge"`
|
|
IsGW bool `json:"isGateway"`
|
|
IPMasq bool `json:"ipMasq"`
|
|
MTU int `json:"mtu"`
|
|
HairpinMode bool `json:"hairpinMode"`
|
|
// EgressAdmission: weave-npc must admit every pod before it gets its interface (EgressService members)
|
|
EgressAdmission bool `json:"egressAdmission"`
|
|
}
|