diff --git a/plugins/linux_only.txt b/plugins/linux_only.txt index b789fb4c..3819a696 100644 --- a/plugins/linux_only.txt +++ b/plugins/linux_only.txt @@ -10,3 +10,4 @@ plugins/meta/portmap plugins/meta/tuning plugins/meta/bandwidth plugins/meta/firewall +plugins/meta/vrf diff --git a/plugins/meta/vrf/README.md b/plugins/meta/vrf/README.md new file mode 100644 index 00000000..d2dc64cc --- /dev/null +++ b/plugins/meta/vrf/README.md @@ -0,0 +1,53 @@ +# vrf plugin + +## Overview + +This plugin creates a [VRF](https://www.kernel.org/doc/Documentation/networking/vrf.txt) in the network namespace and assigns it the interface passed in the arguments. If the VRF is already present in the namespace, it only adds the interface to it. + +As a table id is mandatory, the plugin generates a new one for each different VRF that is added to the namespace. + +It does not create any network interfaces and therefore does not bring connectivity by itself. +It is only useful when used in addition to other plugins. + +## Operation + +The following network configuration file + +```json +{ + "cniVersion": "0.3.1", + "name": "macvlan-vrf", + "plugins": [ + { + "type": "macvlan", + "master": "eth0", + "ipam": { + "type": "dhcp" + } + }, + { + "type": "vrf", + "vrfname": "blue", + } + ] +} +``` + +will create a VRF named blue inside the target namespace (if not existing), and set it as master of the interface created by the previous plugin. + +## Configuration + +The only configuration is the name of the VRF, as per the following example: + +```json +{ + "type": "vrf", + "vrfname": "blue" +} +``` + +## Supported arguments + +The following [args conventions](https://github.com/containernetworking/cni/blob/master/CONVENTIONS.md#args-in-network-config) are supported: + +* `vrfname` (string, optional): The name of the VRF to be created and to be set as master of the interface diff --git a/plugins/meta/vrf/main.go b/plugins/meta/vrf/main.go new file mode 100644 index 00000000..e6f52c3f --- /dev/null +++ b/plugins/meta/vrf/main.go @@ -0,0 +1,186 @@ +// Copyright 2020 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "encoding/json" + "fmt" + + "github.com/vishvananda/netlink" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/cni/pkg/version" + + "github.com/containernetworking/plugins/pkg/ns" + bv "github.com/containernetworking/plugins/pkg/utils/buildversion" +) + +// VRFNetConf represents the vrf configuration. +type VRFNetConf struct { + types.NetConf + + // VRFName is the name of the vrf to add the interface to. + VRFName string `json:"vrfname"` +} + +func main() { + skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.PluginSupports("0.4.0"), bv.BuildString("vrf")) +} + +func cmdAdd(args *skel.CmdArgs) error { + conf, result, err := parseConf(args.StdinData) + if err != nil { + return err + } + + if conf.PrevResult == nil { + return fmt.Errorf("missing prevResult from earlier plugin") + } + + err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { + vrf, err := findVRF(conf.VRFName) + + if _, ok := err.(netlink.LinkNotFoundError); ok { + vrf, err = createVRF(conf.VRFName) + } + + if err != nil { + return err + } + + err = addInterface(vrf, args.IfName) + if err != nil { + return err + } + return nil + }) + + if err != nil { + return fmt.Errorf("cmdAdd failed: %v", err) + } + + if result == nil { + result = ¤t.Result{} + } + + return types.PrintResult(result, conf.CNIVersion) +} + +func cmdDel(args *skel.CmdArgs) error { + conf, _, err := parseConf(args.StdinData) + if err != nil { + return err + } + err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { + vrf, err := findVRF(conf.VRFName) + if _, ok := err.(netlink.LinkNotFoundError); ok { + return nil + } + + if err != nil { + return err + } + + err = resetMaster(args.IfName) + if err != nil { + return err + } + + interfaces, err := assignedInterfaces(vrf) + if err != nil { + return err + } + + // Meaning, we are deleting the last interface assigned to the VRF + if len(interfaces) == 0 { + err = netlink.LinkDel(vrf) + if err != nil { + return err + } + } + return nil + }) + + if err != nil { + return fmt.Errorf("cmdDel failed: %v", err) + } + return nil +} + +func cmdCheck(args *skel.CmdArgs) error { + conf, _, err := parseConf(args.StdinData) + if err != nil { + return err + } + + // Ensure we have previous result. + if conf.PrevResult == nil { + return fmt.Errorf("missing prevResult from earlier plugin") + } + + err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { + vrf, err := findVRF(conf.VRFName) + if err != nil { + return err + } + vrfInterfaces, err := assignedInterfaces(vrf) + + found := false + for _, intf := range vrfInterfaces { + if intf.Attrs().Name == args.IfName { + found = true + break + } + } + if !found { + return fmt.Errorf("Failed to find %s associated to vrf %s", args.IfName, conf.VRFName) + } + return nil + }) + + return nil +} + +func parseConf(data []byte) (*VRFNetConf, *current.Result, error) { + conf := VRFNetConf{} + if err := json.Unmarshal(data, &conf); err != nil { + return nil, nil, fmt.Errorf("failed to load netconf: %v", err) + } + + if conf.VRFName == "" { + return nil, nil, fmt.Errorf("configuration is expected to have a valid vrf name") + } + + if conf.RawPrevResult == nil { + // return early if there was no previous result, which is allowed for DEL calls + return &conf, ¤t.Result{}, nil + } + + // Parse previous result. + var result *current.Result + var err error + if err = version.ParsePrevResult(&conf.NetConf); err != nil { + return nil, nil, fmt.Errorf("could not parse prevResult: %v", err) + } + + result, err = current.NewResultFromResult(conf.PrevResult) + if err != nil { + return nil, nil, fmt.Errorf("could not convert result to current version: %v", err) + } + + return &conf, result, nil +} diff --git a/plugins/meta/vrf/vrf.go b/plugins/meta/vrf/vrf.go new file mode 100644 index 00000000..61a3d056 --- /dev/null +++ b/plugins/meta/vrf/vrf.go @@ -0,0 +1,158 @@ +// Copyright 2020 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "math" + + "github.com/vishvananda/netlink" +) + +// findVRF finds a VRF link with the provided name. +func findVRF(name string) (*netlink.Vrf, error) { + link, err := netlink.LinkByName(name) + if err != nil { + return nil, err + } + vrf, ok := link.(*netlink.Vrf) + if !ok { + return nil, fmt.Errorf("Netlink %s is not a VRF", name) + } + return vrf, nil +} + +// createVRF creates a new VRF and sets it up. +func createVRF(name string) (*netlink.Vrf, error) { + links, err := netlink.LinkList() + if err != nil { + return nil, fmt.Errorf("createVRF: Failed to find links %v", err) + } + tableID, err := findFreeRoutingTableID(links) + if err != nil { + return nil, err + } + vrf := &netlink.Vrf{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + }, + Table: tableID, + } + + err = netlink.LinkAdd(vrf) + if err != nil { + return nil, fmt.Errorf("could not add VRF %s: %v", name, err) + } + err = netlink.LinkSetUp(vrf) + if err != nil { + return nil, fmt.Errorf("could not set link up for VRF %s: %v", name, err) + } + + return vrf, nil +} + +// assignedInterfaces returns the list of interfaces associated to the given vrf. +func assignedInterfaces(vrf *netlink.Vrf) ([]netlink.Link, error) { + links, err := netlink.LinkList() + if err != nil { + return nil, fmt.Errorf("getAssignedInterfaces: Failed to find links %v", err) + } + res := make([]netlink.Link, 0) + for _, l := range links { + if l.Attrs().MasterIndex == vrf.Index { + res = append(res, l) + } + } + return res, nil +} + +// addInterface adds the given interface to the VRF +func addInterface(vrf *netlink.Vrf, intf string) error { + i, err := netlink.LinkByName(intf) + if err != nil { + return fmt.Errorf("could not get link by name %s", intf) + } + + if i.Attrs().MasterIndex != 0 { + master, err := netlink.LinkByIndex(i.Attrs().MasterIndex) + if err != nil { + return fmt.Errorf("interface %s has already a master set, could not retrieve the name: %v", intf, err) + } + return fmt.Errorf("interface %s has already a master set: %s", intf, master.Attrs().Name) + } + + // IPV6 addresses are not maintained unless + // sysctl -w net.ipv6.conf.all.keep_addr_on_down=1 is called + // so we save it, and restore it back. + beforeAddresses, err := netlink.AddrList(i, netlink.FAMILY_V6) + if err != nil { + return fmt.Errorf("failed getting ipv6 addresses for %s", intf) + } + err = netlink.LinkSetMaster(i, vrf) + if err != nil { + return fmt.Errorf("could not set vrf %s as master of %s: %v", vrf.Name, intf, err) + } + + afterAddresses, err := netlink.AddrList(i, netlink.FAMILY_V6) + if err != nil { + return fmt.Errorf("failed getting ipv6 new addresses for %s", intf) + } + + // Since keeping the ipv6 address depends on net.ipv6.conf.all.keep_addr_on_down , + // we check if the new interface does not have them and in case we restore them. +CONTINUE: + for _, toFind := range beforeAddresses { + for _, current := range afterAddresses { + if toFind.Equal(current) { + continue CONTINUE + } + } + // Not found, re-adding it + err = netlink.AddrAdd(i, &toFind) + if err != nil { + return fmt.Errorf("could not restore address %s to %s @ %s: %v", toFind, intf, vrf.Name, err) + } + } + + return nil +} + +func findFreeRoutingTableID(links []netlink.Link) (uint32, error) { + takenTables := make(map[uint32]struct{}, len(links)) + for _, l := range links { + if vrf, ok := l.(*netlink.Vrf); ok { + takenTables[vrf.Table] = struct{}{} + } + } + + for res := uint32(1); res < math.MaxUint32; res++ { + if _, ok := takenTables[res]; !ok { + return res, nil + } + } + return 0, fmt.Errorf("findFreeRoutingTableID: Failed to find an available routing id") +} + +func resetMaster(interfaceName string) error { + intf, err := netlink.LinkByName(interfaceName) + if err != nil { + return fmt.Errorf("resetMaster: could not get link by name %s", interfaceName) + } + err = netlink.LinkSetNoMaster(intf) + if err != nil { + return fmt.Errorf("resetMaster: could reset master to %s", interfaceName) + } + return nil +} diff --git a/plugins/meta/vrf/vrf_suite_test.go b/plugins/meta/vrf/vrf_suite_test.go new file mode 100644 index 00000000..e2c33d2c --- /dev/null +++ b/plugins/meta/vrf/vrf_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2020 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestVRF(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "plugins/meta/vrf") +} diff --git a/plugins/meta/vrf/vrf_test.go b/plugins/meta/vrf/vrf_test.go new file mode 100644 index 00000000..f1af9acb --- /dev/null +++ b/plugins/meta/vrf/vrf_test.go @@ -0,0 +1,255 @@ +// Copyright 2020 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types/current" + "github.com/containernetworking/plugins/pkg/ns" + "github.com/containernetworking/plugins/pkg/testutils" + + "github.com/vishvananda/netlink" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("vrf plugin", func() { + var originalNS ns.NetNS + var targetNS ns.NetNS + const ( + IF0Name = "dummy0" + IF1Name = "dummy1" + VRF0Name = "vrf0" + VRF1Name = "vrf1" + ) + + BeforeEach(func() { + var err error + originalNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + targetNS, err = testutils.NewNS() + Expect(err).NotTo(HaveOccurred()) + + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + err = netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: IF0Name, + }, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = netlink.LinkByName(IF0Name) + Expect(err).NotTo(HaveOccurred()) + + err = netlink.LinkAdd(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: IF1Name, + }, + }) + Expect(err).NotTo(HaveOccurred()) + _, err = netlink.LinkByName(IF0Name) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(originalNS.Close()).To(Succeed()) + Expect(targetNS.Close()).To(Succeed()) + }) + + It("passes prevResult through unchanged", func() { + conf := configFor("test", IF0Name, VRF0Name, "10.0.0.2/24") + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IF0Name, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + + r, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + result, err := current.GetResult(r) + Expect(err).NotTo(HaveOccurred()) + + Expect(len(result.Interfaces)).To(Equal(1)) + Expect(result.Interfaces[0].Name).To(Equal(IF0Name)) + Expect(len(result.IPs)).To(Equal(1)) + Expect(result.IPs[0].Address.String()).To(Equal("10.0.0.2/24")) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("configures a VRF and adds the interface to it", func() { + conf := configFor("test", IF0Name, VRF0Name, "10.0.0.2/24") + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IF0Name, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + + err = targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + checkInterfaceOnVRF(VRF0Name, IF0Name) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("fails if the interface already has a master set", func() { + conf := configFor("test", IF0Name, VRF0Name, "10.0.0.2/24") + + By("Setting the interface's master", func() { + err := targetNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + l, err := netlink.LinkByName(IF0Name) + Expect(err).NotTo(HaveOccurred()) + br := &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: "testrbridge", + }, + } + err = netlink.LinkAdd(br) + Expect(err).NotTo(HaveOccurred()) + err = netlink.LinkSetMaster(l, br) + Expect(err).NotTo(HaveOccurred()) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: targetNS.Path(), + IfName: IF0Name, + StdinData: conf, + } + + err := originalNS.Do(func(ns.NetNS) error { + defer GinkgoRecover() + _, _, err := testutils.CmdAddWithArgs(args, func() error { + return cmdAdd(args) + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("has already a master set")) + return nil + }) + Expect(err).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("unit tests", func() { + DescribeTable("When looking for a table id", + func(links []netlink.Link, expected uint32, expectFail bool) { + newID, err := findFreeRoutingTableID(links) + if expectFail { + Expect(err).To(HaveOccurred()) + return + } + Expect(err).NotTo(HaveOccurred()) + Expect(newID).To(Equal(expected)) + }, + Entry("Finds first free one", []netlink.Link{ + &netlink.Vrf{Table: 1}, + &netlink.Vrf{Table: 2}, + &netlink.Vrf{Table: 3}, + &netlink.Vrf{Table: 5}, + }, uint32(4), false), + Entry("Ignores non VRFs free one", []netlink.Link{ + &netlink.Vrf{Table: 1}, + &netlink.Vrf{Table: 2}, + &netlink.Dummy{}, + &netlink.Vrf{Table: 5}, + }, uint32(3), false), + Entry("Takes the first when no vrfs are there", []netlink.Link{}, + uint32(1), false), + Entry("Works with 999 vrfs already assigned", func() []netlink.Link { + res := []netlink.Link{} + for i := uint32(1); i < 1000; i++ { + res = append(res, &netlink.Vrf{Table: i}) + } + return res + }(), uint32(1000), false), + ) +}) + +func configFor(name, intf, vrf, ip string) []byte { + conf := fmt.Sprintf(`{ + "name": "%s", + "type": "vrf", + "cniVersion": "0.3.1", + "vrfName": "%s", + "prevResult": { + "interfaces": [ + {"name": "%s", "sandbox":"netns"} + ], + "ips": [ + { + "version": "4", + "address": "%s", + "gateway": "10.0.0.1", + "interface": 0 + } + ] + } + }`, name, vrf, intf, ip) + return []byte(conf) +} + +func checkInterfaceOnVRF(vrfName, intfName string) { + vrf, err := netlink.LinkByName(vrfName) + Expect(err).NotTo(HaveOccurred()) + Expect(vrf).To(BeAssignableToTypeOf(&netlink.Vrf{})) + + link, err := netlink.LinkByName(intfName) + Expect(err).NotTo(HaveOccurred()) + masterIndx := link.Attrs().MasterIndex + master, err := netlink.LinkByIndex(masterIndx) + Expect(err).NotTo(HaveOccurred()) + Expect(master.Attrs().Name).To(Equal(vrfName)) +} + +func checkLinkHasNoMaster(intfName string) { + link, err := netlink.LinkByName(intfName) + Expect(err).NotTo(HaveOccurred()) + masterIndx := link.Attrs().MasterIndex + Expect(masterIndx).To(Equal(0)) +}