plugins/pkg/ip/link_linux_test.go
Edward Haas a3cde17fc0 bridge: Add mac field to specify container iface mac
Controlling the mac address of the interface (veth peer) in the
container is useful for functionalities that depend on the mac address.
Examples range from dynamic IP allocations based on an identifier (the
mac) and up to firewall rules (e.g. no-mac-spoofing).

Enforcing a mac address at an early stage and not through a chained
plugin assures the configuration does not have wrong intermediate
configuration. This is especially critical when a dynamic IP may be
provided already in this period.
But it also has implications for future abilities that may land on the
bridge plugin, e.g. supporting no-mac-spoofing.

The field name used (`mac`) fits with other plugins which control the
mac address of the container interface.

The mac address may be specified through the following methods:
- CNI_ARGS
- Args
- RuntimeConfig [1]

The list is ordered by priority, from lowest to higher. The higher
priority method overrides any previous settings.
(e.g. if the mac is specified in RuntimeConfig, it will override any
specifications of the mac mentioned in CNI_ARGS or Args)

[1] To use RuntimeConfig, the network configuration should include the
`capabilities` field with `mac` specified (`"capabilities": {"mac": true}`).

Signed-off-by: Edward Haas <edwardh@redhat.com>
2021-06-29 10:50:19 +03:00

337 lines
9.0 KiB
Go

// Copyright 2016 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 ip_test
import (
"bytes"
"crypto/rand"
"fmt"
"net"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/containernetworking/plugins/pkg/ip"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
"github.com/vishvananda/netlink"
)
func getHwAddr(linkname string) string {
veth, err := netlink.LinkByName(linkname)
Expect(err).NotTo(HaveOccurred())
return fmt.Sprintf("%s", veth.Attrs().HardwareAddr)
}
var _ = Describe("Link", func() {
const (
ifaceFormatString string = "i%d"
mtu int = 1400
ip4onehwaddr = "0a:58:01:01:01:01"
)
var (
hostNetNS ns.NetNS
containerNetNS ns.NetNS
ifaceCounter int = 0
hostVeth net.Interface
containerVeth net.Interface
hostVethName string
containerVethName string
ip4one = net.ParseIP("1.1.1.1")
ip4two = net.ParseIP("1.1.1.2")
originalRandReader = rand.Reader
)
BeforeEach(func() {
var err error
hostNetNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
containerNetNS, err = testutils.NewNS()
Expect(err).NotTo(HaveOccurred())
fakeBytes := make([]byte, 20)
//to be reset in AfterEach block
rand.Reader = bytes.NewReader(fakeBytes)
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, "", hostNetNS)
if err != nil {
return err
}
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Name
containerVethName = containerVeth.Name
return nil
})
})
AfterEach(func() {
Expect(containerNetNS.Close()).To(Succeed())
Expect(hostNetNS.Close()).To(Succeed())
ifaceCounter++
rand.Reader = originalRandReader
})
Describe("GetVethPeerIfindex", func() {
It("returns the link and peer index of the named interface", func() {
By("looking up the container veth index using the host veth name")
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
gotHostLink, gotContainerIndex, err := ip.GetVethPeerIfindex(hostVethName)
Expect(err).NotTo(HaveOccurred())
By("checking we got back the host link")
attrs := gotHostLink.Attrs()
Expect(attrs.Index).To(Equal(hostVeth.Index))
Expect(attrs.Name).To(Equal(hostVeth.Name))
By("checking we got back the container veth index")
Expect(gotContainerIndex).To(Equal(containerVeth.Index))
return nil
})
By("looking up the host veth index using the container veth name")
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
gotContainerLink, gotHostIndex, err := ip.GetVethPeerIfindex(containerVethName)
Expect(err).NotTo(HaveOccurred())
By("checking we got back the container link")
attrs := gotContainerLink.Attrs()
Expect(attrs.Index).To(Equal(containerVeth.Index))
Expect(attrs.Name).To(Equal(containerVeth.Name))
By("checking we got back the host veth index")
Expect(gotHostIndex).To(Equal(hostVeth.Index))
return nil
})
})
})
It("SetupVeth must put the veth endpoints into the separate namespaces", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
containerVethFromName, err := netlink.LinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
Expect(containerVethFromName.Attrs().Index).To(Equal(containerVeth.Index))
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVethFromName, err := netlink.LinkByName(hostVethName)
Expect(err).NotTo(HaveOccurred())
Expect(hostVethFromName.Attrs().Index).To(Equal(hostVeth.Index))
return nil
})
})
Context("when container already has an interface with the same name", func() {
It("returns useful error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName)))
return nil
})
})
})
Context("deleting an non-existent device", func() {
It("returns known error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// This string should match the expected error codes in the cmdDel functions of some of the plugins
_, err := ip.DelLinkByNameAddr("THIS_DONT_EXIST")
Expect(err).To(Equal(ip.ErrLinkNotFound))
return nil
})
})
})
Context("when there is no name available for the host-side", func() {
BeforeEach(func() {
//adding different interface to container ns
containerVethName += "0"
})
It("returns useful error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err.Error()).To(HavePrefix("failed to move veth to host netns: "))
return nil
})
})
})
Context("when there is no name conflict for the host or container interfaces", func() {
BeforeEach(func() {
//adding different interface to container and host ns
containerVethName += "0"
rand.Reader = originalRandReader
})
It("successfully creates the second veth pair", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Name
return nil
})
//verify veths are in different namespaces
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(hostVethName)
Expect(err).NotTo(HaveOccurred())
return nil
})
})
It("successfully creates a veth pair with an explicit mac", func() {
const mac = "02:00:00:00:01:23"
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, mac, hostNetNS)
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Name
link, err := netlink.LinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().HardwareAddr.String()).To(Equal(mac))
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
link, err := netlink.LinkByName(hostVethName)
Expect(err).NotTo(HaveOccurred())
Expect(link.Attrs().HardwareAddr.String()).NotTo(Equal(mac))
return nil
})
})
})
It("DelLinkByName must delete the veth endpoints", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// this will delete the host endpoint too
err := ip.DelLinkByName(containerVethName)
Expect(err).NotTo(HaveOccurred())
_, err = netlink.LinkByName(containerVethName)
Expect(err).To(HaveOccurred())
return nil
})
_ = hostNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, err := netlink.LinkByName(hostVethName)
Expect(err).To(HaveOccurred())
return nil
})
})
It("DelLinkByNameAddr should return no IPs when no IPs are configured", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
// this will delete the host endpoint too
addr, err := ip.DelLinkByNameAddr(containerVethName)
Expect(err).NotTo(HaveOccurred())
Expect(addr).To(HaveLen(0))
return nil
})
})
It("SetHWAddrByIP must change the interface hwaddr and be predictable", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
var err error
hwaddrBefore := getHwAddr(containerVethName)
err = ip.SetHWAddrByIP(containerVethName, ip4one, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter1 := getHwAddr(containerVethName)
Expect(hwaddrBefore).NotTo(Equal(hwaddrAfter1))
Expect(hwaddrAfter1).To(Equal(ip4onehwaddr))
return nil
})
})
It("SetHWAddrByIP must be injective", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
err := ip.SetHWAddrByIP(containerVethName, ip4one, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter1 := getHwAddr(containerVethName)
err = ip.SetHWAddrByIP(containerVethName, ip4two, nil)
Expect(err).NotTo(HaveOccurred())
hwaddrAfter2 := getHwAddr(containerVethName)
Expect(hwaddrAfter1).NotTo(Equal(hwaddrAfter2))
return nil
})
})
})