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

View File

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

View File

@ -55,6 +55,25 @@ type NetConf struct {
HairpinMode bool `json:"hairpinMode"` HairpinMode bool `json:"hairpinMode"`
PromiscMode bool `json:"promiscMode"` PromiscMode bool `json:"promiscMode"`
Vlan int `json:"vlan"` 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 { type gwInfo struct {
@ -70,7 +89,7 @@ func init() {
runtime.LockOSThread() runtime.LockOSThread()
} }
func loadNetConf(bytes []byte) (*NetConf, string, error) { func loadNetConf(bytes []byte, envArgs string) (*NetConf, string, error) {
n := &NetConf{ n := &NetConf{
BrName: defaultBrName, BrName: defaultBrName,
} }
@ -80,6 +99,26 @@ func loadNetConf(bytes []byte) (*NetConf, string, error) {
if n.Vlan < 0 || n.Vlan > 4094 { if n.Vlan < 0 || n.Vlan > 4094 {
return nil, "", fmt.Errorf("invalid VLAN ID %d (must be between 0 and 4094)", n.Vlan) 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 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) 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 { if err != nil {
return nil, fmt.Errorf("faild to create vlan gateway %q: %v", name, err) 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 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{} contIface := &current.Interface{}
hostIface := &current.Interface{} hostIface := &current.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error { err := netns.Do(func(hostNS ns.NetNS) error {
// create the veth pair in the container and move host end into host netns // 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 { if err != nil {
return err return err
} }
@ -380,7 +419,7 @@ func enableIPForward(family int) error {
func cmdAdd(args *skel.CmdArgs) error { func cmdAdd(args *skel.CmdArgs) error {
var success bool = false var success bool = false
n, cniVersion, err := loadNetConf(args.StdinData) n, cniVersion, err := loadNetConf(args.StdinData, args.Args)
if err != nil { if err != nil {
return err return err
} }
@ -406,7 +445,7 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
defer netns.Close() 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 { if err != nil {
return err return err
} }
@ -585,7 +624,7 @@ func cmdAdd(args *skel.CmdArgs) error {
} }
func cmdDel(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 { if err != nil {
return err return err
} }
@ -776,7 +815,7 @@ func validateCniContainerInterface(intf current.Interface) (cniBridgeIf, error)
func cmdCheck(args *skel.CmdArgs) error { func cmdCheck(args *skel.CmdArgs) error {
n, _, err := loadNetConf(args.StdinData) n, _, err := loadNetConf(args.StdinData, args.Args)
if err != nil { if err != nil {
return err return err
} }

View File

@ -78,6 +78,21 @@ type testCase struct {
DelErr020 string DelErr020 string
AddErr010 string AddErr010 string
DelErr010 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 // Range definition for each entry in the ranges list
@ -148,6 +163,18 @@ const (
ipamEndStr = ` ipamEndStr = `
}` }`
argsFormat = `,
"args": {
"cni": {
"mac": %q
}
}`
runtimeConfig = `,
"RuntimeConfig": {
"mac": %q
}`
) )
// netConfJSON() generates a JSON network configuration string // netConfJSON() generates a JSON network configuration string
@ -160,6 +187,12 @@ func (tc testCase) netConfJSON(dataDir string) string {
if tc.ipMasq { if tc.ipMasq {
conf += tc.ipMasqConfig() 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 { if !tc.isLayer2 {
conf += netDefault conf += netDefault
@ -223,6 +256,7 @@ func (tc testCase) createCmdArgs(targetNS ns.NetNS, dataDir string) *skel.CmdArg
Netns: targetNS.Path(), Netns: targetNS.Path(),
IfName: IFNAME, IfName: IFNAME,
StdinData: []byte(conf), 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[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) 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())) Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists // 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[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) 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())) Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists // 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[1].Mac).To(HaveLen(17))
Expect(result.Interfaces[2].Name).To(Equal(IFNAME)) 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())) Expect(result.Interfaces[2].Sandbox).To(Equal(tester.targetNS.Path()))
// Make sure bridge link exists // 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() { It(fmt.Sprintf("[%s] checks ip release in case of error", ver), func() {
err := originalNS.Do(func(ns.NetNS) error { err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover() 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)"))) tests = append(tests, createCaseFn("0.4.0", 5000, fmt.Errorf("invalid VLAN ID 5000 (must be between 0 and 4094)")))
for _, test := range tests { for _, test := range tests {
_, _, err := loadNetConf([]byte(test.netConfJSON(""))) _, _, err := loadNetConf([]byte(test.netConfJSON("")), "")
if test.err == nil { if test.err == nil {
Expect(err).To(BeNil()) Expect(err).To(BeNil())
} else { } else {

View File

@ -66,7 +66,7 @@ func setupContainerVeth(netns ns.NetNS, ifName string, mtu int, pr *current.Resu
containerInterface := &current.Interface{} containerInterface := &current.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error { 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 { if err != nil {
return err return err
} }