Skip to content

Commit 8be5425

Browse files
committed
bridge: re-generate a bridge MAC when the kernel has zeroed it out
The kernel zeroes a bridge's MAC address when its last port is removed. The ensureAddr function pins the bridge's MAC address using the address read when the bridge was looked up, which is before the veth is attached. So for an existing bridge that had previously lost all its ports, the address it tried to pin is all zeros, and pinning an all-zero address fails with EINVAL. In fact, this can happen in practice: an ADD that fails has its veth cleaned up, which, if it was the first ADD, leaves the bridge portless and hence its MAC zeroed, so the next ADD fails to pin it. Detect that case and regenerate a random MAC address the same way the kernel does. This is fine as it only happens if the bridge has no ports, so no peer can hold stale state about the old address. Signed-off-by: Tom Wieczorek <twieczorek@mirantis.com>
1 parent cdd1542 commit 8be5425

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

plugins/main/bridge/bridge.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package main
1616

1717
import (
18+
"crypto/rand"
1819
"encoding/json"
1920
"errors"
2021
"fmt"
@@ -303,8 +304,27 @@ func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool)
303304
}
304305

305306
// Set the bridge's MAC to itself. Otherwise, the bridge will take the
306-
// lowest-numbered mac on the bridge, and will change as ifs churn
307-
if err := netlink.LinkSetHardwareAddr(br, br.Attrs().HardwareAddr); err != nil {
307+
// lowest-numbered mac on the bridge, and will change as ifs churn. The
308+
// cached attrs hold the MAC the bridge had before any veths were attached
309+
// during this invocation.
310+
hwAddr := br.Attrs().HardwareAddr
311+
312+
// Check whether the kernel has zeroed the bridge's MAC.
313+
// This only happens when the bridge's last port was detached.
314+
if isZeroMAC(hwAddr) {
315+
// There are no neighbors that could hold stale state about the previous
316+
// MAC, so it's fine to generate a suitable stand-in the same way the
317+
// kernel does.
318+
// https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/etherdevice.h?h=v7.1#n237
319+
hwAddr = make(net.HardwareAddr, 6)
320+
if _, err := rand.Read(hwAddr); err != nil {
321+
panic(fmt.Sprintf("failed to generate random MAC address: %v", err))
322+
}
323+
hwAddr[0] &= 0xfe // clear multicast bit
324+
hwAddr[0] |= 0x02 // set local assignment bit (IEEE802)
325+
}
326+
327+
if err := netlink.LinkSetHardwareAddr(br, hwAddr); err != nil {
308328
return fmt.Errorf("could not set bridge's mac: %v", err)
309329
}
310330

@@ -676,6 +696,10 @@ func cmdAdd(args *skel.CmdArgs) error {
676696
if err != nil {
677697
return fmt.Errorf("failed to set bridge addr: %v", err)
678698
}
699+
// Reload the bridge so it reflects the MAC that ensureAddr just pinned.
700+
if br, err = bridgeByName(n.BrName); err != nil {
701+
return err
702+
}
679703
}
680704
}
681705

@@ -1120,3 +1144,16 @@ func cmdStatus(args *skel.CmdArgs) error {
11201144

11211145
return nil
11221146
}
1147+
1148+
// Indicates whether a hardware address is nil, empty, or consists solely of
1149+
// zeros. Technically, the kernel will always report such addresses as an array
1150+
// of zeros, but vishvananda/netlink will "normalize" them to nil.
1151+
func isZeroMAC(mac net.HardwareAddr) bool {
1152+
for _, b := range mac {
1153+
if b != 0 {
1154+
return false
1155+
}
1156+
}
1157+
1158+
return true
1159+
}

plugins/main/bridge/bridge_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717
import (
1818
"context"
1919
"encoding/json"
20+
"errors"
2021
"fmt"
2122
"net"
2223
"os"
@@ -27,6 +28,8 @@ import (
2728
"github.com/networkplumbing/go-nft/nft"
2829
. "github.com/onsi/ginkgo/v2"
2930
. "github.com/onsi/gomega"
31+
gomegaformat "github.com/onsi/gomega/format"
32+
gomegatypes "github.com/onsi/gomega/types"
3033
"github.com/vishvananda/netlink"
3134
"github.com/vishvananda/netlink/nl"
3235
"sigs.k8s.io/knftables"
@@ -2445,6 +2448,70 @@ var _ = Describe("bridge Operations", func() {
24452448
})
24462449
}
24472450

2451+
It(fmt.Sprintf("[%s] keeps a stable MAC even if the bridge previously lost all its ports", ver), func() {
2452+
err := originalNS.Do(func(ns.NetNS) error {
2453+
defer GinkgoRecover()
2454+
2455+
// Test dual-stack here: A dual-stack ADD configures a
2456+
// gateway per family, so it sets the bridge MAC more than
2457+
// once in a single invocation.
2458+
tc := testCase{
2459+
cniVersion: ver,
2460+
ranges: []rangeInfo{
2461+
{subnet: "10.1.2.0/24"},
2462+
{subnet: "2001:db8:42::/64"},
2463+
},
2464+
expGWCIDRs: []string{"10.1.2.1/24", "2001:db8:42::1/64"},
2465+
}
2466+
2467+
br, _, err := setupBridge(tc.netConf())
2468+
Expect(err).NotTo(HaveOccurred())
2469+
2470+
// Attach and remove a port so the kernel first generates,
2471+
// and then zeroes the bridge's MAC.
2472+
cleanupVethPort, err := attachVethPort(br, "")
2473+
Expect(err).NotTo(HaveOccurred())
2474+
defer func() { Expect(cleanupVethPort()).To(Succeed()) }()
2475+
link, err := netlinksafe.LinkByName(BRNAME)
2476+
Expect(err).NotTo(HaveOccurred())
2477+
kernelMAC := link.Attrs().HardwareAddr
2478+
Expect(kernelMAC).NotTo(beAZeroMAC(), "kernel didn't generate a MAC")
2479+
Expect(kernelMAC[0]&0x01).To(BeZero(), "kernel-generated MAC is not unicast")
2480+
Expect(kernelMAC[0]&0x02).NotTo(BeZero(), "kernel-generated MAC is not locally administered")
2481+
Expect(cleanupVethPort()).To(Succeed())
2482+
link, err = netlinksafe.LinkByName(BRNAME)
2483+
Expect(err).NotTo(HaveOccurred())
2484+
// If the expectation below fails, it may be because the
2485+
// kernel behavior has changed and it keeps the original
2486+
// address.
2487+
Expect(link.Attrs().HardwareAddr).To(beAZeroMAC(), "kernel should have zeroed the MAC after the bridge lost its last port")
2488+
2489+
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
2490+
2491+
// The plugin must have generated a valid, locally administered
2492+
// unicast MAC address. This MAC address must have survived the
2493+
// removal of the container veth during DEL.
2494+
link, err = netlinksafe.LinkByName(BRNAME)
2495+
Expect(err).NotTo(HaveOccurred())
2496+
bridgeMAC := link.Attrs().HardwareAddr
2497+
Expect(bridgeMAC).NotTo(beAZeroMAC(), "bridge plugin should have generated a MAC")
2498+
Expect(bridgeMAC).NotTo(Equal(kernelMAC), "generated MAC is the same as the kernel-generated one")
2499+
Expect(bridgeMAC[0]&0x01).To(BeZero(), "generated MAC should be unicast")
2500+
Expect(bridgeMAC[0]&0x02).NotTo(BeZero(), "generated MAC should be locally administered")
2501+
2502+
// A second pod's ADD on the same bridge must keep that MAC.
2503+
// Every pod on the node shares the bridge. Between the two
2504+
// ADDs, it briefly had no ports. Because the generated MAC
2505+
// address was pinned, the kernel does not zero it.
2506+
cmdAddDelTest(originalNS, targetNS, tc, dataDir)
2507+
link, err = netlinksafe.LinkByName(BRNAME)
2508+
Expect(err).NotTo(HaveOccurred())
2509+
Expect(link.Attrs().HardwareAddr).To(Equal(bridgeMAC), "generated MAC changed, but it should have been pinned")
2510+
return nil
2511+
})
2512+
Expect(err).NotTo(HaveOccurred())
2513+
})
2514+
24482515
It(fmt.Sprintf("[%s] uses an explicit MAC addresses for the container iface (from CNI_ARGS)", ver), func() {
24492516
err := originalNS.Do(func(ns.NetNS) error {
24502517
defer GinkgoRecover()
@@ -2823,3 +2890,25 @@ func assertMacSpoofCheckRules(assert func(actual interface{}, expectedLen int))
28232890
"macspoofchk-dummy-0-eth0",
28242891
)), 2)
28252892
}
2893+
2894+
func beAZeroMAC() gomegatypes.GomegaMatcher {
2895+
return zeroMACMatcher{}
2896+
}
2897+
2898+
type zeroMACMatcher struct{}
2899+
2900+
func (zeroMACMatcher) Match(actual any) (bool, error) {
2901+
if addr, ok := actual.(net.HardwareAddr); ok {
2902+
return isZeroMAC(addr), nil
2903+
}
2904+
2905+
return false, errors.New("expected a net.HardwareAddr")
2906+
}
2907+
2908+
func (zeroMACMatcher) FailureMessage(actual any) string {
2909+
return gomegaformat.Message(fmt.Sprint(actual), "to be a zero MAC")
2910+
}
2911+
2912+
func (zeroMACMatcher) NegatedFailureMessage(actual any) string {
2913+
return gomegaformat.Message(fmt.Sprint(actual), "not to be a zero MAC")
2914+
}

0 commit comments

Comments
 (0)