Skip to content

Commit

Permalink
Added vlan tag to the bridge cni plugin.
Browse files Browse the repository at this point in the history
With the VLAN filter, the Linux bridge acts more like a real switch, Allow to tag and untag
vlan id's on every interface connected to the bridge.

Related to https://developers.redhat.com/blog/2017/09/14/vlan-filter-support-on-bridge/ post.

Note: This feature was introduced in Linux kernel 3.8 and was added to RHEL in version 7.0.
  • Loading branch information
SchSeba committed Dec 13, 2018
1 parent 3b56c4c commit a1438fc
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 7 deletions.
5 changes: 5 additions & 0 deletions plugins/main/bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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```
28 changes: 24 additions & 4 deletions plugins/main/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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 := &current.Interface{}
hostIface := &current.Interface{}

Expand Down Expand Up @@ -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
}

Expand All @@ -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, &current.Interface{
Name: br.Attrs().Name,
Mac: br.Attrs().HardwareAddr.String(),
Expand Down Expand Up @@ -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
}
Expand Down
104 changes: 101 additions & 3 deletions plugins/main/bridge/bridge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package main

import (
"fmt"
"github.com/vishvananda/netlink/nl"
"io/ioutil"
"net"
"os"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -78,9 +80,12 @@ const (
"cniVersion": "%s",
"name": "testConfig",
"type": "bridge",
"bridge": "%s",`
"bridge": "%s"`

netDefault = `
vlan = `,
"vlan": %d`

netDefault = `,
"isDefaultGateway": true,
"ipMasq": false`

Expand Down Expand Up @@ -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 {
Expand All @@ -136,7 +145,7 @@ func (tc testCase) netConfJSON(dataDir string) string {
conf += ipamEndStr
}
} else {
conf += `
conf += `,
"ipam": {}`
}
return "{" + conf + "\n}"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
{
Expand Down

0 comments on commit a1438fc

Please sign in to comment.