Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@

# Keep in sync with the paths in .github/workflows/ci.yaml.
!src/
!files/
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
- .dockerignore
# keep in sync with .dockerignore
- src/**
- files/**
push: *triggers

jobs:
Expand Down
10 changes: 8 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ ARG \
HASH=34bd82d47e981940751619c9cc44c095bb90bfcaf8d71865cbb822c37690a764

FROM --platform=$BUILDPLATFORM $GOLANG_IMAGE AS sources
RUN apk add --no-cache patch
ENV GOTOOLCHAIN=local GOTELEMETRY=off
WORKDIR /go/src/containernetworking-plugins
ARG VERSION HASH
RUN --mount=type=tmpfs,target=/tmp \
RUN --mount=source=files/patches,target=/run/patches,ro \
--mount=type=tmpfs,target=/tmp \
set -euo pipefail \
&& wget -qO /tmp/sources.tar.gz https://github.com/containernetworking/plugins/archive/refs/tags/v$VERSION.tar.gz \
&& { echo $HASH /tmp/sources.tar.gz | sha256sum -c -; } || { sha256sum /tmp/sources.tar.gz; exit 1; } \
&& tar xf /tmp/sources.tar.gz --strip-components=1
&& tar xf /tmp/sources.tar.gz --strip-components=1 \
&& for p in /run/patches/*; do \
echo Applying "$p" >&2; \
patch -p1 <"$p"; \
done


FROM sources AS bins
Expand Down
149 changes: 149 additions & 0 deletions files/patches/0001-bridge-always-pin-the-bridge-MAC-address.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
From 222058b490000246306210646c50a790f0be3a4c Mon Sep 17 00:00:00 2001
From: Tom Wieczorek <twieczorek@mirantis.com>
Date: Wed, 17 Jun 2026 13:05:21 +0200
Subject: [PATCH 1/2] bridge: always pin the bridge MAC address

The ensureAddr function pins the bridge's MAC address so it doesn't
change as ports come and go. The pinning happens after the gateway
address is set, so if a run set the address but then failed before
pinning, subsequent runs never reattempted to pin the MAC, leaving
behind a bridge with a floating MAC.

Always pin the MAC, even if the gateway address is already present,
regardless of the outcomes of previous attempts.

Signed-off-by: Tom Wieczorek <twieczorek@mirantis.com>
---
plugins/main/bridge/bridge.go | 12 ++++--
plugins/main/bridge/bridge_test.go | 67 ++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go
index 476e6d5f..860ba8d1 100644
--- a/plugins/main/bridge/bridge.go
+++ b/plugins/main/bridge/bridge.go
@@ -271,11 +271,13 @@ func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool)
}

ipnStr := ipn.String()
+ addrFound := false
for _, a := range addrs {

// string comp is actually easiest for doing IPNet comps
if a.IPNet.String() == ipnStr {
- return nil
+ addrFound = true
+ break
}

// Multiple IPv6 addresses are allowed on the bridge if the
@@ -293,9 +295,11 @@ func ensureAddr(br netlink.Link, family int, ipn *net.IPNet, forceAddress bool)
}
}

- addr := &netlink.Addr{IPNet: ipn, Label: ""}
- if err := netlink.AddrAdd(br, addr); err != nil && err != syscall.EEXIST {
- return fmt.Errorf("could not add IP address %s to %q: %v", ipnStr, br.Attrs().Name, err)
+ if !addrFound {
+ addr := &netlink.Addr{IPNet: ipn, Label: ""}
+ if err := netlink.AddrAdd(br, addr); err != nil && err != syscall.EEXIST {
+ return fmt.Errorf("could not add IP address %s to %q: %v", ipnStr, br.Attrs().Name, err)
+ }
}

// Set the bridge's MAC to itself. Otherwise, the bridge will take the
diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go
index 3ed19022..cd3f562f 100644
--- a/plugins/main/bridge/bridge_test.go
+++ b/plugins/main/bridge/bridge_test.go
@@ -21,6 +21,7 @@ import (
"net"
"os"
"strings"
+ "sync"

"github.com/coreos/go-iptables/iptables"
"github.com/networkplumbing/go-nft/nft"
@@ -407,6 +408,30 @@ func (tc testCase) expectedCIDRs() ([]*net.IPNet, []*net.IPNet) {
return cidrsV4, cidrsV6
}

+// Attaches a veth port with an optional fixed MAC address to a bridge in the
+// current network namespace. To be cleaned up via the returned function.
+func attachVethPort(br netlink.Link, mac string) (func() error, error) {
+ linkAttrs := netlink.NewLinkAttrs()
+ linkAttrs.Name = "testport0"
+ if mac != "" {
+ hwAddr, err := net.ParseMAC(mac)
+ if err != nil {
+ return nil, err
+ }
+
+ linkAttrs.HardwareAddr = hwAddr
+ }
+ veth := &netlink.Veth{LinkAttrs: linkAttrs, PeerName: "testport1"}
+ if err := netlink.LinkAdd(veth); err != nil {
+ return nil, err
+ }
+ if err := netlink.LinkSetMaster(veth, br); err != nil {
+ return nil, err
+ }
+
+ return sync.OnceValue(func() error { return netlink.LinkDel(veth) }), nil
+}
+
// delBridgeAddrs() deletes addresses from the bridge
func delBridgeAddrs(testNS ns.NetNS) {
err := testNS.Do(func(ns.NetNS) error {
@@ -2377,6 +2402,48 @@ var _ = Describe("bridge Operations", func() {
})
Expect(err).NotTo(HaveOccurred())
})
+
+ It(fmt.Sprintf("[%s] (%d) keeps a stable MAC even if the gateway address already exists", ver, i), func() {
+ err := originalNS.Do(func(ns.NetNS) error {
+ defer GinkgoRecover()
+
+ tc.cniVersion = ver
+ br, _, err := setupBridge(tc.netConf())
+ Expect(err).NotTo(HaveOccurred())
+ link, err := netlinksafe.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ originalMAC := link.Attrs().HardwareAddr
+
+ // Pre-add the gateway address the plugin is going to
+ // configure, as left behind by an ADD that failed midway.
+ _, subnet, err := net.ParseCIDR(tc.subnet)
+ Expect(err).NotTo(HaveOccurred())
+ gwIP := calcGatewayIP(subnet)
+ err = netlink.AddrAdd(br, &netlink.Addr{
+ IPNet: &net.IPNet{IP: gwIP, Mask: subnet.Mask},
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ cmdAddDelTest(originalNS, targetNS, tc, dataDir)
+
+ // The MAC must have been pinned nevertheless: it neither
+ // got zeroed when the container veth was removed, nor
+ // does it change with port churn.
+ link, err = netlinksafe.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().HardwareAddr).To(Equal(originalMAC))
+
+ cleanupVethPort, err := attachVethPort(br, "02:00:00:00:00:01")
+ Expect(err).NotTo(HaveOccurred())
+ defer func() { Expect(cleanupVethPort()).To(Succeed()) }()
+
+ link, err = netlinksafe.LinkByName(BRNAME)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(link.Attrs().HardwareAddr).To(Equal(originalMAC))
+ return nil
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
}

It(fmt.Sprintf("[%s] uses an explicit MAC addresses for the container iface (from CNI_ARGS)", ver), func() {
--
2.54.0

Loading
Loading