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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ gopath/
.vagrant
.idea
/release-*
vendor/
Binary file added bridge
Binary file not shown.
47 changes: 47 additions & 0 deletions plugins/main/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"sort"
"syscall"
Expand Down Expand Up @@ -63,6 +64,7 @@ type NetConf struct {
EnableDad bool `json:"enabledad,omitempty"`
DisableContainerInterface bool `json:"disableContainerInterface,omitempty"`
PortIsolation bool `json:"portIsolation,omitempty"`
GroupFwdMask int `json:"groupFwdMask,omitempty"`

Args struct {
Cni BridgeArgs `json:"cni,omitempty"`
Expand Down Expand Up @@ -97,6 +99,11 @@ type gwInfo struct {
defaultRouteFound bool
}

var getGroupFwdMaskPath = func(brName string) string {
safeName := filepath.Base(brName)
return fmt.Sprintf("/sys/class/net/%s/bridge/group_fwd_mask", safeName)
}

func init() {
// this ensures that main runs only on main thread (thread group leader).
// since namespace ops (unshare, setns) are done for a single thread, we
Expand Down Expand Up @@ -511,13 +518,53 @@ func calcGatewayIP(ipn *net.IPNet) net.IP {
return ip.NextIP(nid)
}

func setGroupFwdMask(brName string, val int) error {
if val == 0 {
return nil
}

if err := validateGroupFwdMask(val); err != nil {
return err
}

path := getGroupFwdMaskPath(brName)
safeName := filepath.Base(brName)
if _, err := os.Stat(path); err == nil {
if err := os.WriteFile(path, []byte(fmt.Sprintf("%d", val)), 0o644); err != nil {
return fmt.Errorf("failed to set group_fwd_mask for bridge %q: %w", safeName, err)
}
} else {
fmt.Fprintf(os.Stderr, "group_fwd_mask not supported on bridge %q\n", safeName)
}

return nil
}

func validateGroupFwdMask(val int) error {
if val < 0 || val > 65535 {
return fmt.Errorf("invalid groupFwdMask %d: must be between 0 and 65535", val)
}
return nil
}

func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
vlanFiltering := n.Vlan != 0 || n.VlanTrunk != nil

// create bridge if necessary
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering)
if err != nil {
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
}
// groupFwdMask enables forwarding of link-local multicast MAC addresses.
// By default, Linux bridge drops addresses like 01-80-C2-00-00-0E (PTP).
// When set, writes the value to /sys/class/net/<bridge>/bridge/group_fwd_mask.

// Skip default (0) to preserve kernel behavior unless explicitly set
if n.GroupFwdMask != 0 {
if err := setGroupFwdMask(n.BrName, n.GroupFwdMask); err != nil {
return nil, nil, err
}
}

return br, &current.Interface{
Name: br.Attrs().Name,
Expand Down
139 changes: 139 additions & 0 deletions plugins/main/bridge/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"strings"
"testing"

"github.com/coreos/go-iptables/iptables"
"github.com/networkplumbing/go-nft/nft"
Expand Down Expand Up @@ -1909,6 +1911,84 @@ var _ = Describe("bridge Operations", func() {
Entry("when the maxID is smaller than minID", []*VlanTrunk{{MinID: &incorrectMinID, MaxID: &incorrectMaxID}}, fmt.Errorf("minID is greater than maxID in trunk parameter")),
)

Describe("group_fwd_mask configuration", func() {

It("should set group_fwd_mask when configured", func() {
tc := testCase{
cniVersion: "1.0.0",
subnet: "10.10.0.0/16",
isGW: true,
}

// Inject custom config with groupFwdMask
conf := fmt.Sprintf(`{
"cniVersion": "%s",
"name": "testConfig",
"type": "bridge",
"bridge": "%s",
"groupFwdMask": 16384,
"ipam": {
"type": "host-local",
"subnet": "10.10.0.0/16"
}
}`, tc.cniVersion, BRNAME)

args := &skel.CmdArgs{
ContainerID: "test-groupfwdmask",
Netns: targetNS.Path(),
IfName: IFNAME,
StdinData: []byte(conf),
}

err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()

err := cmdAdd(args)
Expect(err).NotTo(HaveOccurred())

path := fmt.Sprintf("/sys/class/net/%s/bridge/group_fwd_mask", BRNAME)

if _, err := os.Stat(path); err == nil {
data, err := os.ReadFile(path)
Expect(err).NotTo(HaveOccurred())

Expect(strings.TrimSpace(string(data))).To(Equal("16384"))
} else {
Skip("group_fwd_mask not supported on this system")
}
return nil
})

Expect(err).NotTo(HaveOccurred())
})

It("should not modify group_fwd_mask when not set", func() {
tc := testCase{
cniVersion: "1.0.0",
subnet: "10.10.0.0/16",
isGW: true,
}

args := tc.createCmdArgs(targetNS, dataDir)

err := originalNS.Do(func(ns.NetNS) error {
defer GinkgoRecover()

err := cmdAdd(args)
Expect(err).NotTo(HaveOccurred())

// Just ensure file exists (default behavior preserved)
_, err = os.ReadFile(fmt.Sprintf("/sys/class/net/%s/bridge/group_fwd_mask", BRNAME))
Expect(err).NotTo(HaveOccurred())

return nil
})

Expect(err).NotTo(HaveOccurred())
})

})

for _, ver := range testutils.AllSpecVersions {
// Redefine ver inside for scope so real value is picked up by each dynamically defined It()
// See Gingkgo's "Patterns for dynamically generating tests" documentation.
Expand Down Expand Up @@ -2755,3 +2835,62 @@ func assertMacSpoofCheckRules(assert func(actual interface{}, expectedLen int))
"macspoofchk-dummy-0-eth0",
)), 2)
}

func TestValidateGroupFwdMask(t *testing.T) {
tests := []struct {
name string
value int
wantErr bool
}{
{"valid zero", 0, false},
{"valid mid", 100, false},
{"valid max", 65535, false},
{"invalid negative", -1, true},
{"invalid too large", 70000, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGroupFwdMask(tt.value)
if (err != nil) != tt.wantErr {
t.Fatalf("expected error=%v, got %v", tt.wantErr, err)
}
})
}
}

func TestSetGroupFwdMask(t *testing.T) {
tmpDir := t.TempDir()

brName := "testbr0"
bridgeDir := filepath.Join(tmpDir, brName, "bridge")
err := os.MkdirAll(bridgeDir, 0755)
if err != nil {
t.Fatal(err)
}

filePath := filepath.Join(bridgeDir, "group_fwd_mask")

// Create fake file
err = os.WriteFile(filePath, []byte("0"), 0644)
if err != nil {
t.Fatal(err)
}

// Override path logic temporarily
oldPathFunc := getGroupFwdMaskPath
getGroupFwdMaskPath = func(name string) string {
return filePath
}
defer func() { getGroupFwdMaskPath = oldPathFunc }()

err = setGroupFwdMask(brName, 100)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

data, _ := os.ReadFile(filePath)
if string(data) != "100" {
t.Fatalf("expected 100, got %s", string(data))
}
}
43 changes: 0 additions & 43 deletions vendor/cyphar.com/go-pathrs/.golangci.yml

This file was deleted.

Loading