From a3cde17fc0d0749a33fba2773c6b9cc218169114 Mon Sep 17 00:00:00 2001 From: Edward Haas Date: Mon, 7 Jun 2021 15:42:12 +0300 Subject: [PATCH] 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 --- pkg/ip/link_linux.go | 21 ++++-- pkg/ip/link_linux_test.go | 34 +++++++-- plugins/main/bridge/bridge.go | 55 +++++++++++--- plugins/main/bridge/bridge_test.go | 111 +++++++++++++++++++++++++++-- plugins/main/ptp/ptp.go | 2 +- 5 files changed, 199 insertions(+), 24 deletions(-) diff --git a/pkg/ip/link_linux.go b/pkg/ip/link_linux.go index f8781cf19..fbe14997a 100644 --- a/pkg/ip/link_linux.go +++ b/pkg/ip/link_linux.go @@ -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. diff --git a/pkg/ip/link_linux_test.go b/pkg/ip/link_linux_test.go index 7249c9ef9..eb47de0ff 100644 --- a/pkg/ip/link_linux_test.go +++ b/pkg/ip/link_linux_test.go @@ -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() { diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index 69ffa98f1..bc374ed5f 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -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 := ¤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) + 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 } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 4e3b6287b..29d08eb24 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -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 { diff --git a/plugins/main/ptp/ptp.go b/plugins/main/ptp/ptp.go index 58e41087f..6eb121e13 100644 --- a/plugins/main/ptp/ptp.go +++ b/plugins/main/ptp/ptp.go @@ -66,7 +66,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu containerInterface := ¤t.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 }