diff --git a/plugins/main/bridge/README.md b/plugins/main/bridge/README.md index 75f8ede0e..85ae242e6 100644 --- a/plugins/main/bridge/README.md +++ b/plugins/main/bridge/README.md @@ -52,3 +52,8 @@ If the bridge is missing, the plugin will create one on first use and, if gatewa * `hairpinMode` (boolean, optional): set hairpin mode for interfaces on the bridge. Defaults to false. * `ipam` (dictionary, required): IPAM configuration to be used for this network. For L2-only network, create empty dictionary. * `promiscMode` (boolean, optional): set promiscuous mode on the bridge. Defaults to false. +* `vlan` (int, optional): assign VLAN tag. Defaults to none. + +*Note:* The VLAN parameter config the VLAN tag on the host end of the veth and also enable the vlan_filtering feature on the bridge interface. + +*Note:* To configure up link for L2 network you need to allow the vlan on the up link interface by using the follow command ``` bridge vlan add vid VLAN_ID dev DEV``` \ No newline at end of file diff --git a/plugins/main/bridge/bridge.go b/plugins/main/bridge/bridge.go index b66f81f09..e1de031b8 100644 --- a/plugins/main/bridge/bridge.go +++ b/plugins/main/bridge/bridge.go @@ -52,6 +52,7 @@ type NetConf struct { MTU int `json:"mtu"` HairpinMode bool `json:"hairpinMode"` PromiscMode bool `json:"promiscMode"` + Vlan int `json:"vlan"` } type gwInfo struct { @@ -209,7 +210,7 @@ func bridgeByName(name string) (*netlink.Bridge, error) { return br, nil } -func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) { +func ensureBridge(brName string, mtu int, promiscMode, vlanFiltering bool) (*netlink.Bridge, error) { br := &netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: brName, @@ -220,6 +221,7 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er // default packet limit TxQLen: -1, }, + VlanFiltering: &vlanFiltering, } err := netlink.LinkAdd(br) @@ -247,7 +249,7 @@ func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, er return br, nil } -func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) { +func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool, vlanID int) (*current.Interface, *current.Interface, error) { contIface := ¤t.Interface{} hostIface := ¤t.Interface{} @@ -284,6 +286,13 @@ func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairp return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err) } + if vlanID != 0 { + err = netlink.BridgeVlanAdd(hostVeth, uint16(vlanID), true, true, false, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to setup vlan tag on interface %q: %v", hostIface.Name, err) + } + } + return hostIface, contIface, nil } @@ -293,12 +302,23 @@ func calcGatewayIP(ipn *net.IPNet) net.IP { } func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { + vlanFiltering := false + if n.Vlan != 0 { + vlanFiltering = true + } // create bridge if necessary - br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode) + 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) } + if n.Vlan != 0 { + err = netlink.BridgeVlanAdd(br, uint16(n.Vlan), false, true, true, false) + if err != nil { + return nil, nil, fmt.Errorf("failed to setup vlan tag on the bridge interface %q: %v", br.Name, err) + } + } + return br, ¤t.Interface{ Name: br.Attrs().Name, Mac: br.Attrs().HardwareAddr.String(), @@ -355,7 +375,7 @@ func cmdAdd(args *skel.CmdArgs) error { } defer netns.Close() - hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode) + hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode, n.Vlan) if err != nil { return err } diff --git a/plugins/main/bridge/bridge_test.go b/plugins/main/bridge/bridge_test.go index 420773bf3..b8a33d520 100644 --- a/plugins/main/bridge/bridge_test.go +++ b/plugins/main/bridge/bridge_test.go @@ -16,6 +16,7 @@ package main import ( "fmt" + "github.com/vishvananda/netlink/nl" "io/ioutil" "net" "os" @@ -49,6 +50,7 @@ type testCase struct { isGW bool isLayer2 bool expGWCIDRs []string // Expected gateway addresses in CIDR form + vlan int } // Range definition for each entry in the ranges list @@ -78,9 +80,12 @@ const ( "cniVersion": "%s", "name": "testConfig", "type": "bridge", - "bridge": "%s",` + "bridge": "%s"` - netDefault = ` + vlan = `, + "vlan": %d` + + netDefault = `, "isDefaultGateway": true, "ipMasq": false` @@ -120,6 +125,10 @@ const ( // for a test case. func (tc testCase) netConfJSON(dataDir string) string { conf := fmt.Sprintf(netConfStr, tc.cniVersion, BRNAME) + if tc.vlan != 0 { + conf += fmt.Sprintf(vlan, tc.vlan) + } + if !tc.isLayer2 { conf += netDefault if tc.subnet != "" || tc.ranges != nil { @@ -136,7 +145,7 @@ func (tc testCase) netConfJSON(dataDir string) string { conf += ipamEndStr } } else { - conf += ` + conf += `, "ipam": {}` } return "{" + conf + "\n}" @@ -248,6 +257,16 @@ func countIPAMIPs(path string) (int, error) { return count, nil } +func checkVlan(vlanId int, bridgeVlanInfo []*nl.BridgeVlanInfo) bool { + for _, vlan := range bridgeVlanInfo { + if vlan.Vid == uint16(vlanId) { + return true + } + } + + return false +} + type cmdAddDelTester interface { setNS(testNS ns.NetNS, targetNS ns.NetNS) cmdAddTest(tc testCase, dataDir string) @@ -312,6 +331,19 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { Expect(link.Attrs().HardwareAddr.String()).To(Equal(result.Interfaces[0].Mac)) bridgeMAC := link.Attrs().HardwareAddr.String() + // Check the bridge vlan filtering equals true + if tc.vlan != 0 { + Expect(*link.(*netlink.Bridge).VlanFiltering).To(Equal(true)) + + interfaceMap, err := netlink.BridgeVlanList() + Expect(err).NotTo(HaveOccurred()) + vlans, isExist := interfaceMap[int32(link.Attrs().Index)] + Expect(isExist).To(BeTrue()) + Expect(checkVlan(tc.vlan, vlans)).To(BeTrue()) + } else { + Expect(*link.(*netlink.Bridge).VlanFiltering).To(Equal(false)) + } + // Ensure bridge has expected gateway address(es) addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) Expect(err).NotTo(HaveOccurred()) @@ -342,6 +374,15 @@ func (tester *testerV03x) cmdAddTest(tc testCase, dataDir string) { Expect(link).To(BeAssignableToTypeOf(&netlink.Veth{})) tester.vethName = result.Interfaces[1].Name + // check vlan exist on the veth interface + if tc.vlan != 0 { + interfaceMap, err := netlink.BridgeVlanList() + Expect(err).NotTo(HaveOccurred()) + vlans, isExist := interfaceMap[int32(link.Attrs().Index)] + Expect(isExist).To(BeTrue()) + Expect(checkVlan(tc.vlan, vlans)).To(BeTrue()) + } + // Check that the bridge has a different mac from the veth // If not, it means the bridge has an unstable mac and will change // as ifs are added and removed @@ -722,6 +763,63 @@ var _ = Describe("bridge Operations", func() { cmdAddDelTest(originalNS, tc, dataDir) }) + It("configures and deconfigures a l2 bridge with vlan id 100 using ADD/DEL for 0.3.1 config", func() { + tc := testCase{cniVersion: "0.3.0", isLayer2: true, vlan: 100} + cmdAddDelTest(originalNS, tc, dataDir) + }) + + It("configures and deconfigures a l2 bridge with vlan id 100 using ADD/DEL for 0.3.1 config", func() { + tc := testCase{cniVersion: "0.3.1", isLayer2: true, vlan: 100} + cmdAddDelTest(originalNS, tc, dataDir) + }) + + It("configures and deconfigures a bridge and veth with default route and vlanID 100 with ADD/DEL for 0.3.0 config", func() { + testCases := []testCase{ + { + // IPv4 only + subnet: "10.1.2.0/24", + expGWCIDRs: []string{"10.1.2.1/24"}, + vlan: 100, + }, + { + // IPv6 only + subnet: "2001:db8::0/64", + expGWCIDRs: []string{"2001:db8::1/64"}, + vlan: 100, + }, + { + // Dual-Stack + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + }, + vlan: 100, + }, + { + // 3 Subnets (1 IPv4 and 2 IPv6 subnets) + ranges: []rangeInfo{ + {subnet: "192.168.0.0/24"}, + {subnet: "fd00::0/64"}, + {subnet: "2001:db8::0/64"}, + }, + expGWCIDRs: []string{ + "192.168.0.1/24", + "fd00::1/64", + "2001:db8::1/64", + }, + vlan: 100, + }, + } + for _, tc := range testCases { + tc.cniVersion = "0.3.0" + cmdAddDelTest(originalNS, tc, dataDir) + } + }) + It("configures and deconfigures a bridge and veth with default route with ADD/DEL for 0.3.1 config", func() { testCases := []testCase{ {