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>
This commit is contained in:
Edward Haas 2021-06-07 15:42:12 +03:00
parent 8de0287741
commit a3cde17fc0
5 changed files with 199 additions and 24 deletions

View File

@ -33,7 +33,7 @@ var (
ErrLinkNotFound = errors.New("link not found")
)
func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
func makeVethPair(name, peer string, mtu int, mac string) (netlink.Link, error) {
veth := &netlink.Veth{
LinkAttrs: netlink.LinkAttrs{
Name: name,
@ -42,6 +42,13 @@ func makeVethPair(name, peer string, mtu int) (netlink.Link, error) {
},
PeerName: peer,
}
if mac != "" {
m, err := net.ParseMAC(mac)
if err != nil {
return nil, err
}
veth.LinkAttrs.HardwareAddr = m
}
if err := netlink.LinkAdd(veth); err != nil {
return nil, err
}
@ -62,7 +69,7 @@ func peerExists(name string) bool {
return true
}
func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink.Link, err error) {
func makeVeth(name, vethPeerName string, mtu int, mac string) (peerName string, veth netlink.Link, err error) {
for i := 0; i < 10; i++ {
if vethPeerName != "" {
peerName = vethPeerName
@ -73,7 +80,7 @@ func makeVeth(name, vethPeerName string, mtu int) (peerName string, veth netlink
}
}
veth, err = makeVethPair(name, peerName, mtu)
veth, err = makeVethPair(name, peerName, mtu, mac)
switch {
case err == nil:
return
@ -132,8 +139,8 @@ func ifaceFromNetlinkLink(l netlink.Link) net.Interface {
// devices and move the host-side veth into the provided hostNS namespace.
// hostVethName: If hostVethName is not specified, the host-side veth name will use a random string.
// On success, SetupVethWithName returns (hostVeth, containerVeth, nil)
func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu)
func SetupVethWithName(contVethName, hostVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu, contVethMac)
if err != nil {
return net.Interface{}, net.Interface{}, err
}
@ -175,8 +182,8 @@ func SetupVethWithName(contVethName, hostVethName string, mtu int, hostNS ns.Net
// Call SetupVeth from inside the container netns. It will create both veth
// devices and move the host-side veth into the provided hostNS namespace.
// On success, SetupVeth returns (hostVeth, containerVeth, nil)
func SetupVeth(contVethName string, mtu int, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
return SetupVethWithName(contVethName, "", mtu, hostNS)
func SetupVeth(contVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (net.Interface, net.Interface, error) {
return SetupVethWithName(contVethName, "", mtu, contVethMac, hostNS)
}
// DelLinkByName removes an interface link.

View File

@ -72,7 +72,7 @@ var _ = Describe("Link", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, hostNetNS)
hostVeth, containerVeth, err = ip.SetupVeth(fmt.Sprintf(ifaceFormatString, ifaceCounter), mtu, "", hostNetNS)
if err != nil {
return err
}
@ -159,7 +159,7 @@ var _ = Describe("Link", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
_, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err.Error()).To(Equal(fmt.Sprintf("container veth name provided (%s) already exists", containerVethName)))
return nil
@ -189,7 +189,7 @@ var _ = Describe("Link", func() {
It("returns useful error", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
_, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
_, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err.Error()).To(HavePrefix("failed to move veth to host netns: "))
return nil
@ -207,7 +207,7 @@ var _ = Describe("Link", func() {
_ = containerNetNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, hostNetNS)
hostVeth, _, err := ip.SetupVeth(containerVethName, mtu, "", hostNetNS)
Expect(err).NotTo(HaveOccurred())
hostVethName = hostVeth.Name
return nil
@ -233,6 +233,32 @@ var _ = Describe("Link", func() {
})
})
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() {

View File

@ -55,6 +55,25 @@ type NetConf struct {
HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"`
Vlan int `json:"vlan"`
Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
} `json:"args,omitempty"`
RuntimeConfig struct {
Mac string `json:"mac,omitempty"`
} `json:"runtimeConfig,omitempty"`
mac string
}
type BridgeArgs struct {
Mac string `json:"mac,omitempty"`
}
// MacEnvArgs represents CNI_ARGS
type MacEnvArgs struct {
types.CommonArgs
MAC types.UnmarshallableString `json:"mac,omitempty"`
}
type gwInfo struct {
@ -70,7 +89,7 @@ func init() {
runtime.LockOSThread()
}
func loadNetConf(bytes []byte) (*NetConf, string, error) {
func loadNetConf(bytes []byte, envArgs string) (*NetConf, string, error) {
n := &NetConf{
BrName: defaultBrName,
}
@ -80,6 +99,26 @@ func loadNetConf(bytes []byte) (*NetConf, string, error) {
if n.Vlan < 0 || n.Vlan > 4094 {
return nil, "", fmt.Errorf("invalid VLAN ID %d (must be between 0 and 4094)", n.Vlan)
}
if envArgs != "" {
e := MacEnvArgs{}
if err := types.LoadArgs(envArgs, &e); err != nil {
return nil, "", err
}
if e.MAC != "" {
n.mac = string(e.MAC)
}
}
if mac := n.Args.Cni.Mac; mac != "" {
n.mac = mac
}
if mac := n.RuntimeConfig.Mac; mac != "" {
n.mac = mac
}
return n, n.CNIVersion, nil
}
@ -273,7 +312,7 @@ func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) {
return nil, fmt.Errorf("faild to find host namespace: %v", err)
}
_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId)
_, brGatewayIface, err := setupVeth(hostNS, br, name, br.MTU, false, vlanId, "")
if err != nil {
return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err)
}
@ -287,13 +326,13 @@ func ensureVlanInterface(br *netlink.Bridge, vlanId int) (netlink.Link, error) {
return brGatewayVeth, nil
}
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) {
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int, mac string) (*current.Interface, *current.Interface, error) {
contIface := &current.Interface{}
hostIface := &current.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)
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, mac, hostNS)
if err != nil {
return err
}
@ -380,7 +419,7 @@ func enableIPForward(family int) error {
func cmdAdd(args *skel.CmdArgs) error {
var success bool = false
n, cniVersion, err := loadNetConf(args.StdinData)
n, cniVersion, err := loadNetConf(args.StdinData, args.Args)
if err != nil {
return err
}
@ -406,7 +445,7 @@ func cmdAdd(args *skel.CmdArgs) error {
}
defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan)
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan, n.mac)
if err != nil {
return err
}
@ -585,7 +624,7 @@ func cmdAdd(args *skel.CmdArgs) error {
}
func cmdDel(args *skel.CmdArgs) error {
n, _, err := loadNetConf(args.StdinData)
n, _, err := loadNetConf(args.StdinData, args.Args)
if err != nil {
return err
}
@ -776,7 +815,7 @@ func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error)
func cmdCheck(args *skel.CmdArgs) error {
n, _, err := loadNetConf(args.StdinData)
n, _, err := loadNetConf(args.StdinData, args.Args)
if err != nil {
return err
}

View File

@ -78,6 +78,21 @@ type testCase struct {
DelErr020 string
AddErr010 string
DelErr010 string
envArgs string // CNI_ARGS
runtimeConfig struct {
mac string
}
args struct {
cni struct {
mac string
}
}
// Unlike the parameters above, the following parameters
// are expected values to be checked against.
// e.g. the mac address has several sources: CNI_ARGS, Args and RuntimeConfig.
expectedMac string
}
// Range definition for each entry in the ranges list
@ -148,6 +163,18 @@ const (
ipamEndStr = `
}`
argsFormat = `,
"args": {
"cni": {
"mac": %q
}
}`
runtimeConfig = `,
"RuntimeConfig": {
"mac": %q
}`
)
// netConfJSON() generates a JSON network configuration string
@ -160,6 +187,12 @@ func (tc testCase) netConfJSON(dataDir string) string {
if tc.ipMasq {
conf += tc.ipMasqConfig()
}
if tc.args.cni.mac != "" {
conf += fmt.Sprintf(argsFormat, tc.args.cni.mac)
}
if tc.runtimeConfig.mac != "" {
conf += fmt.Sprintf(runtimeConfig, tc.runtimeConfig.mac)
}
if !tc.isLayer2 {
conf += netDefault
@ -223,6 +256,7 @@ func (tc testCase) createCmdArgs(targetNS ns.NetNS, dataDir string) *skel.CmdArg
Netns: targetNS.Path(),
IfName: IFNAME,
StdinData: []byte(conf),
Args: tc.envArgs,
}
}
@ -428,7 +462,10 @@ func (tester *testerV10x) cmdAddTest(tc testCase, dataDir string) (types.Result,
Expect(result.Interfaces[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
Expect(result.Interfaces[2].Mac).To(HaveLen(17)) //mac is random
Expect(result.Interfaces[2].Mac).To(HaveLen(17))
if tc.expectedMac != "" {
Expect(result.Interfaces[2].Mac).To(Equal(tc.expectedMac))
}
Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists
@ -725,7 +762,10 @@ func (tester *testerV04x) cmdAddTest(tc testCase, dataDir string) (types.Result,
Expect(result.Interfaces[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
Expect(result.Interfaces[2].Mac).To(HaveLen(17)) //mac is random
Expect(result.Interfaces[2].Mac).To(HaveLen(17))
if tc.expectedMac != "" {
Expect(result.Interfaces[2].Mac).To(Equal(tc.expectedMac))
}
Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists
@ -1022,7 +1062,10 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) (types.Result,
Expect(result.Interfaces[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME))
Expect(result.Interfaces[2].Mac).To(HaveLen(17)) //mac is random
Expect(result.Interfaces[2].Mac).To(HaveLen(17))
if tc.expectedMac != "" {
Expect(result.Interfaces[2].Mac).To(Equal(tc.expectedMac))
}
Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists
@ -1967,6 +2010,66 @@ var _ = Describe("bridge Operations", func() {
})
}
It(fmt.Sprintf("[%s] uses an explicit MAC addresses for the container iface (from CNI_ARGS)", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
const expectedMac = "02:00:00:00:00:00"
tc := testCase{
cniVersion: ver,
subnet: "10.1.2.0/24",
envArgs: "MAC=" + expectedMac,
expectedMac: expectedMac,
}
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
return nil
})
Expect(err).NotTo(HaveOccurred())
})
It(fmt.Sprintf("[%s] uses an explicit MAC addresses for the container iface (from Args)", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
const expectedMac = "02:00:00:00:00:00"
tc := testCase{
cniVersion: ver,
subnet: "10.1.2.0/24",
envArgs: "MAC=" + "02:00:00:00:04:56",
expectedMac: expectedMac,
}
tc.args.cni.mac = expectedMac
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
return nil
})
Expect(err).NotTo(HaveOccurred())
})
It(fmt.Sprintf("[%s] uses an explicit MAC addresses for the container iface (from RuntimeConfig)", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
const expectedMac = "02:00:00:00:00:00"
tc := testCase{
cniVersion: ver,
subnet: "10.1.2.0/24",
envArgs: "MAC=" + "02:00:00:00:04:56",
expectedMac: expectedMac,
}
tc.args.cni.mac = "02:00:00:00:07:89"
tc.runtimeConfig.mac = expectedMac
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
return nil
})
Expect(err).NotTo(HaveOccurred())
})
It(fmt.Sprintf("[%s] checks ip release in case of error", ver), func() {
err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()
@ -2099,7 +2202,7 @@ var _ = Describe("bridge Operations", func() {
tests = append(tests, createCaseFn("0.4.0", 5000, fmt.Errorf("invalid VLAN ID 5000 (must be between 0 and 4094)")))
for _, test := range tests {
_, _, err := loadNetConf([]byte(test.netConfJSON("")))
_, _, err := loadNetConf([]byte(test.netConfJSON("")), "")
if test.err == nil {
Expect(err).To(BeNil())
} else {

View File

@ -66,7 +66,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu
containerInterface := &current.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error {
hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, hostNS)
hostVeth, contVeth0, err := ip.SetupVeth(ifName, mtu, "", hostNS)
if err != nil {
return err
}