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 }