
Updates the spec and plugins to return an array of interfaces and IP details to the runtime including: - interface names and MAC addresses configured by the plugin - whether the interfaces are sandboxed (container/VM) or host (bridge, veth, etc) - multiple IP addresses configured by IPAM and which interface they have been assigned to Returning interface details is useful for runtimes, as well as allowing more flexible chaining of CNI plugins themselves. For example, some meta plugins may need to know the host-side interface to be able to apply firewall or traffic shaping rules to the container.
413 lines
11 KiB
Go
413 lines
11 KiB
Go
// Copyright 2014 CNI authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"runtime"
|
|
"syscall"
|
|
|
|
"github.com/containernetworking/cni/pkg/ip"
|
|
"github.com/containernetworking/cni/pkg/ipam"
|
|
"github.com/containernetworking/cni/pkg/ns"
|
|
"github.com/containernetworking/cni/pkg/skel"
|
|
"github.com/containernetworking/cni/pkg/types"
|
|
"github.com/containernetworking/cni/pkg/types/current"
|
|
"github.com/containernetworking/cni/pkg/utils"
|
|
"github.com/containernetworking/cni/pkg/version"
|
|
"github.com/vishvananda/netlink"
|
|
)
|
|
|
|
const defaultBrName = "cni0"
|
|
|
|
type NetConf struct {
|
|
types.NetConf
|
|
BrName string `json:"bridge"`
|
|
IsGW bool `json:"isGateway"`
|
|
IsDefaultGW bool `json:"isDefaultGateway"`
|
|
ForceAddress bool `json:"forceAddress"`
|
|
IPMasq bool `json:"ipMasq"`
|
|
MTU int `json:"mtu"`
|
|
HairpinMode bool `json:"hairpinMode"`
|
|
}
|
|
|
|
func init() {
|
|
// this ensures that main runs only on main thread (thread group leader).
|
|
// since namespace ops (unshare, setns) are done for a single thread, we
|
|
// must ensure that the goroutine does not jump from OS thread to thread
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
func loadNetConf(bytes []byte) (*NetConf, string, error) {
|
|
n := &NetConf{
|
|
BrName: defaultBrName,
|
|
}
|
|
if err := json.Unmarshal(bytes, n); err != nil {
|
|
return nil, "", fmt.Errorf("failed to load netconf: %v", err)
|
|
}
|
|
return n, n.CNIVersion, nil
|
|
}
|
|
|
|
func ensureBridgeAddr(br *netlink.Bridge, ipn *net.IPNet, forceAddress bool) error {
|
|
addrs, err := netlink.AddrList(br, syscall.AF_INET)
|
|
if err != nil && err != syscall.ENOENT {
|
|
return fmt.Errorf("could not get list of IP addresses: %v", err)
|
|
}
|
|
|
|
// if there're no addresses on the bridge, it's ok -- we'll add one
|
|
if len(addrs) > 0 {
|
|
ipnStr := ipn.String()
|
|
for _, a := range addrs {
|
|
// string comp is actually easiest for doing IPNet comps
|
|
if a.IPNet.String() == ipnStr {
|
|
return nil
|
|
}
|
|
|
|
// If forceAddress is set to true then reconfigure IP address otherwise throw error
|
|
if forceAddress {
|
|
if err = deleteBridgeAddr(br, a.IPNet); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return fmt.Errorf("%q already has an IP address different from %v", br.Name, ipn.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
|
if err := netlink.AddrAdd(br, addr); err != nil {
|
|
return fmt.Errorf("could not add IP address to %q: %v", br.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteBridgeAddr(br *netlink.Bridge, ipn *net.IPNet) error {
|
|
addr := &netlink.Addr{IPNet: ipn, Label: ""}
|
|
|
|
if err := netlink.LinkSetDown(br); err != nil {
|
|
return fmt.Errorf("could not set down bridge %q: %v", br.Name, err)
|
|
}
|
|
|
|
if err := netlink.AddrDel(br, addr); err != nil {
|
|
return fmt.Errorf("could not remove IP address from %q: %v", br.Name, err)
|
|
}
|
|
|
|
if err := netlink.LinkSetUp(br); err != nil {
|
|
return fmt.Errorf("could not set up bridge %q: %v", br.Name, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func bridgeByName(name string) (*netlink.Bridge, error) {
|
|
l, err := netlink.LinkByName(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not lookup %q: %v", name, err)
|
|
}
|
|
br, ok := l.(*netlink.Bridge)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%q already exists but is not a bridge", name)
|
|
}
|
|
return br, nil
|
|
}
|
|
|
|
func ensureBridge(brName string, mtu int) (*netlink.Bridge, error) {
|
|
br := &netlink.Bridge{
|
|
LinkAttrs: netlink.LinkAttrs{
|
|
Name: brName,
|
|
MTU: mtu,
|
|
// Let kernel use default txqueuelen; leaving it unset
|
|
// means 0, and a zero-length TX queue messes up FIFO
|
|
// traffic shapers which use TX queue length as the
|
|
// default packet limit
|
|
TxQLen: -1,
|
|
},
|
|
}
|
|
|
|
err := netlink.LinkAdd(br)
|
|
if err != nil && err != syscall.EEXIST {
|
|
return nil, fmt.Errorf("could not add %q: %v", brName, err)
|
|
}
|
|
|
|
// Re-fetch link to read all attributes and if it already existed,
|
|
// ensure it's really a bridge with similar configuration
|
|
br, err = bridgeByName(brName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := netlink.LinkSetUp(br); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return br, nil
|
|
}
|
|
|
|
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
|
|
contIface := ¤t.Interface{}
|
|
hostIface := ¤t.Interface{}
|
|
|
|
err := netns.Do(func(hostNS ns.NetNS) error {
|
|
// create the veth pair in the container and move host end into host netns
|
|
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
contIface.Name = containerVeth.Attrs().Name
|
|
contIface.Mac = containerVeth.Attrs().HardwareAddr.String()
|
|
contIface.Sandbox = netns.Path()
|
|
hostIface.Name = hostVeth.Attrs().Name
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// need to lookup hostVeth again as its index has changed during ns move
|
|
hostVeth, err := netlink.LinkByName(hostIface.Name)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
|
|
}
|
|
hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
|
|
|
|
// connect host veth end to the bridge
|
|
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err)
|
|
}
|
|
|
|
// set hairpin mode
|
|
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err)
|
|
}
|
|
|
|
return hostIface, contIface, nil
|
|
}
|
|
|
|
func calcGatewayIP(ipn *net.IPNet) net.IP {
|
|
nid := ipn.IP.Mask(ipn.Mask)
|
|
return ip.NextIP(nid)
|
|
}
|
|
|
|
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
|
|
// create bridge if necessary
|
|
br, err := ensureBridge(n.BrName, n.MTU)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
|
}
|
|
|
|
return br, ¤t.Interface{
|
|
Name: br.Attrs().Name,
|
|
Mac: br.Attrs().HardwareAddr.String(),
|
|
}, nil
|
|
}
|
|
|
|
func cmdAdd(args *skel.CmdArgs) error {
|
|
n, cniVersion, err := loadNetConf(args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.IsDefaultGW {
|
|
n.IsGW = true
|
|
}
|
|
|
|
br, brInterface, err := setupBridge(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
netns, err := ns.GetNS(args.Netns)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
|
|
}
|
|
defer netns.Close()
|
|
|
|
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// run the IPAM plugin and get back the config to apply
|
|
r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert whatever the IPAM result was into the current Result type
|
|
result, err := current.NewResultFromResult(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(result.IPs) == 0 {
|
|
return errors.New("IPAM plugin returned missing IP config")
|
|
}
|
|
|
|
result.Interfaces = []*current.Interface{brInterface, hostInterface, containerInterface}
|
|
|
|
for _, ipc := range result.IPs {
|
|
// All IPs currently refer to the container interface
|
|
ipc.Interface = 2
|
|
if ipc.Gateway == nil && n.IsGW {
|
|
ipc.Gateway = calcGatewayIP(&ipc.Address)
|
|
}
|
|
}
|
|
|
|
if err := netns.Do(func(_ ns.NetNS) error {
|
|
// set the default gateway if requested
|
|
if n.IsDefaultGW {
|
|
for _, ipc := range result.IPs {
|
|
defaultNet := &net.IPNet{}
|
|
switch {
|
|
case ipc.Address.IP.To4() != nil:
|
|
defaultNet.IP = net.IPv4zero
|
|
defaultNet.Mask = net.IPMask(net.IPv4zero)
|
|
case len(ipc.Address.IP) == net.IPv6len && ipc.Address.IP.To4() == nil:
|
|
defaultNet.IP = net.IPv6zero
|
|
defaultNet.Mask = net.IPMask(net.IPv6zero)
|
|
default:
|
|
return fmt.Errorf("Unknown IP object: %v", ipc)
|
|
}
|
|
|
|
for _, route := range result.Routes {
|
|
if defaultNet.String() == route.Dst.String() {
|
|
if route.GW != nil && !route.GW.Equal(ipc.Gateway) {
|
|
return fmt.Errorf(
|
|
"isDefaultGateway ineffective because IPAM sets default route via %q",
|
|
route.GW,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Routes = append(
|
|
result.Routes,
|
|
&types.Route{Dst: *defaultNet, GW: ipc.Gateway},
|
|
)
|
|
}
|
|
}
|
|
|
|
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ip.SetHWAddrByIP(args.IfName, result.IPs[0].Address.IP, nil /* TODO IPv6 */); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Refetch the veth since its MAC address may changed
|
|
link, err := netlink.LinkByName(args.IfName)
|
|
if err != nil {
|
|
return fmt.Errorf("could not lookup %q: %v", args.IfName, err)
|
|
}
|
|
containerInterface.Mac = link.Attrs().HardwareAddr.String()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.IsGW {
|
|
var firstV4Addr net.IP
|
|
for _, ipc := range result.IPs {
|
|
gwn := &net.IPNet{
|
|
IP: ipc.Gateway,
|
|
Mask: ipc.Address.Mask,
|
|
}
|
|
if ipc.Gateway.To4() != nil && firstV4Addr == nil {
|
|
firstV4Addr = ipc.Gateway
|
|
}
|
|
|
|
if err = ensureBridgeAddr(br, gwn, n.ForceAddress); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if firstV4Addr != nil {
|
|
if err := ip.SetHWAddrByIP(n.BrName, firstV4Addr, nil /* TODO IPv6 */); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := ip.EnableIP4Forward(); err != nil {
|
|
return fmt.Errorf("failed to enable forwarding: %v", err)
|
|
}
|
|
}
|
|
|
|
if n.IPMasq {
|
|
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
|
comment := utils.FormatComment(n.Name, args.ContainerID)
|
|
for _, ipc := range result.IPs {
|
|
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refetch the bridge since its MAC address may change when the first
|
|
// veth is added or after its IP address is set
|
|
br, err = bridgeByName(n.BrName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
brInterface.Mac = br.Attrs().HardwareAddr.String()
|
|
|
|
result.DNS = n.DNS
|
|
|
|
return types.PrintResult(result, cniVersion)
|
|
}
|
|
|
|
func cmdDel(args *skel.CmdArgs) error {
|
|
n, _, err := loadNetConf(args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ipam.ExecDel(n.IPAM.Type, args.StdinData); err != nil {
|
|
return err
|
|
}
|
|
|
|
if args.Netns == "" {
|
|
return nil
|
|
}
|
|
|
|
var ipn *net.IPNet
|
|
err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
|
|
var err error
|
|
ipn, err = ip.DelLinkByNameAddr(args.IfName, netlink.FAMILY_V4)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.IPMasq {
|
|
chain := utils.FormatChainName(n.Name, args.ContainerID)
|
|
comment := utils.FormatComment(n.Name, args.ContainerID)
|
|
if err = ip.TeardownIPMasq(ipn, chain, comment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func main() {
|
|
skel.PluginMain(cmdAdd, cmdDel, version.All)
|
|
}
|