
Add possibility to reconfigure bridge IP address when there is a new value. New boolean flag added to net configuration to force IP change if it is need. Otherwise code behaves as previously and throws error
347 lines
8.5 KiB
Go
347 lines
8.5 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/utils"
|
|
"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, 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, 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,
|
|
},
|
|
}
|
|
|
|
if err := netlink.LinkAdd(br); err != nil {
|
|
if err != syscall.EEXIST {
|
|
return nil, fmt.Errorf("could not add %q: %v", brName, err)
|
|
}
|
|
|
|
// it's ok if the device already exists as long as config is similar
|
|
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) error {
|
|
var hostVethName string
|
|
|
|
err := netns.Do(func(hostNS ns.NetNS) error {
|
|
// create the veth pair in the container and move host end into host netns
|
|
hostVeth, _, err := ip.SetupVeth(ifName, mtu, hostNS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostVethName = hostVeth.Attrs().Name
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// need to lookup hostVeth again as its index has changed during ns move
|
|
hostVeth, err := netlink.LinkByName(hostVethName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to lookup %q: %v", hostVethName, err)
|
|
}
|
|
|
|
// connect host veth end to the bridge
|
|
if err = netlink.LinkSetMaster(hostVeth, br); err != nil {
|
|
return fmt.Errorf("failed to connect %q to bridge %v: %v", hostVethName, br.Attrs().Name, err)
|
|
}
|
|
|
|
// set hairpin mode
|
|
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
|
|
return fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVethName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func calcGatewayIP(ipn *net.IPNet) net.IP {
|
|
nid := ipn.IP.Mask(ipn.Mask)
|
|
return ip.NextIP(nid)
|
|
}
|
|
|
|
func setupBridge(n *NetConf) (*netlink.Bridge, error) {
|
|
// create bridge if necessary
|
|
br, err := ensureBridge(n.BrName, n.MTU)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
|
|
}
|
|
|
|
return br, nil
|
|
}
|
|
|
|
func cmdAdd(args *skel.CmdArgs) error {
|
|
n, err := loadNetConf(args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.IsDefaultGW {
|
|
n.IsGW = true
|
|
}
|
|
|
|
br, 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()
|
|
|
|
if err = setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode); err != nil {
|
|
return err
|
|
}
|
|
|
|
// run the IPAM plugin and get back the config to apply
|
|
result, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: make this optional when IPv6 is supported
|
|
if result.IP4 == nil {
|
|
return errors.New("IPAM plugin returned missing IPv4 config")
|
|
}
|
|
|
|
if result.IP4.Gateway == nil && n.IsGW {
|
|
result.IP4.Gateway = calcGatewayIP(&result.IP4.IP)
|
|
}
|
|
|
|
if err := netns.Do(func(_ ns.NetNS) error {
|
|
// set the default gateway if requested
|
|
if n.IsDefaultGW {
|
|
_, defaultNet, err := net.ParseCIDR("0.0.0.0/0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, route := range result.IP4.Routes {
|
|
if defaultNet.String() == route.Dst.String() {
|
|
if route.GW != nil && !route.GW.Equal(result.IP4.Gateway) {
|
|
return fmt.Errorf(
|
|
"isDefaultGateway ineffective because IPAM sets default route via %q",
|
|
route.GW,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
result.IP4.Routes = append(
|
|
result.IP4.Routes,
|
|
types.Route{Dst: *defaultNet, GW: result.IP4.Gateway},
|
|
)
|
|
|
|
// TODO: IPV6
|
|
}
|
|
|
|
return ipam.ConfigureIface(args.IfName, result)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
if n.IsGW {
|
|
gwn := &net.IPNet{
|
|
IP: result.IP4.Gateway,
|
|
Mask: result.IP4.IP.Mask,
|
|
}
|
|
|
|
if err = ensureBridgeAddr(br, gwn, n.ForceAddress); 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)
|
|
if err = ip.SetupIPMasq(ip.Network(&result.IP4.IP), chain, comment); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
result.DNS = n.DNS
|
|
return result.Print()
|
|
}
|
|
|
|
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)
|
|
}
|