// +build linux package netavark_test // The tests have to be run as root. // For each test there will be two network namespaces created, // netNSTest and netNSContainer. Each test must be run inside // netNSTest to prevent leakage in the host netns, therefore // it should use the following structure: // It("test name", func() { // runTest(func() { // // add test logic here // }) // }) import ( "io/ioutil" "net" "os" "strconv" "sync" "time" "github.com/containernetworking/plugins/pkg/ns" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/network/util" "github.com/containers/podman/v3/pkg/netns" "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/storage/pkg/stringid" ) var _ = Describe("run netavark", func() { var ( libpodNet types.ContainerNetwork confDir string netNSTest ns.NetNS netNSContainer ns.NetNS ) // runTest is a helper function to run a test. It ensures that each test // is run in its own netns. It also creates a mountns to mount a tmpfs to /var/lib/cni. runTest := func(run func()) { netNSTest.Do(func(_ ns.NetNS) error { defer GinkgoRecover() // we have to setup the loopback adapter in this netns to use port forwarding link, err := netlink.LinkByName("lo") Expect(err).To(BeNil(), "Failed to get loopback adapter") err = netlink.LinkSetUp(link) Expect(err).To(BeNil(), "Failed to set loopback adapter up") run() return nil }) } BeforeEach(func() { if _, ok := os.LookupEnv("NETAVARK_BINARY"); !ok { Skip("NETAVARK_BINARY not set skip run tests") } // set the logrus settings logrus.SetLevel(logrus.TraceLevel) // disable extra quotes so we can easily copy the netavark command logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true}) logrus.SetOutput(os.Stderr) // The tests need root privileges. // Technically we could work around that by using user namespaces and // the rootless cni code but this is to much work to get it right for a unit test. if rootless.IsRootless() { Skip("this test needs to be run as root") } var err error confDir, err = ioutil.TempDir("", "podman_netavark_test") if err != nil { Fail("Failed to create tmpdir") } netNSTest, err = netns.NewNS() if err != nil { Fail("Failed to create netns") } netNSContainer, err = netns.NewNS() if err != nil { Fail("Failed to create netns") } // Force iptables driver, firewalld is broken inside the extra // namespace because it still connects to firewalld on the host. _ = os.Setenv("NETAVARK_FW", "iptables") }) JustBeforeEach(func() { var err error libpodNet, err = getNetworkInterface(confDir, false) if err != nil { Fail("Failed to create NewCNINetworkInterface") } }) AfterEach(func() { logrus.SetFormatter(&logrus.TextFormatter{}) logrus.SetLevel(logrus.InfoLevel) os.RemoveAll(confDir) netns.UnmountNS(netNSTest) netNSTest.Close() netns.UnmountNS(netNSContainer) netNSContainer.Close() _ = os.Unsetenv("NETAVARK_FW") }) It("test basic setup", func() { runTest(func() { defNet := types.DefaultNetworkName intName := "eth0" opts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: "someID", ContainerName: "someName", Networks: map[string]types.PerNetworkOptions{ defNet: { InterfaceName: intName, }, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), opts) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) ip := res[defNet].Interfaces[intName].Subnets[0].IPNet.IP Expect(ip.String()).To(ContainSubstring("10.88.0.")) gw := res[defNet].Interfaces[intName].Subnets[0].Gateway util.NormalizeIP(&gw) Expect(gw.String()).To(Equal("10.88.0.1")) macAddress := res[defNet].Interfaces[intName].MacAddress Expect(macAddress).To(HaveLen(6)) // default network has no dns Expect(res[defNet].DNSServerIPs).To(BeEmpty()) Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) // check in the container namespace if the settings are applied err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() i, err := net.InterfaceByName(intName) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName)) Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macAddress))) addrs, err := i.Addrs() Expect(err).To(BeNil()) subnet := &net.IPNet{ IP: ip, Mask: net.CIDRMask(16, 32), } Expect(addrs).To(ContainElements(EqualSubnet(subnet))) // check loopback adapter i, err = net.InterfaceByName("lo") Expect(err).To(BeNil()) Expect(i.Name).To(Equal("lo")) Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") return nil }) Expect(err).To(BeNil()) // default bridge name bridgeName := "podman0" // check settings on the host side i, err := net.InterfaceByName(bridgeName) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(bridgeName)) addrs, err := i.Addrs() Expect(err).ToNot(HaveOccurred()) // test that the gateway ip is assigned to the interface subnet := &net.IPNet{ IP: gw, Mask: net.CIDRMask(16, 32), } Expect(addrs).To(ContainElements(EqualSubnet(subnet))) wg := &sync.WaitGroup{} expected := stringid.GenerateNonCryptoID() // now check ip connectivity err = netNSContainer.Do(func(_ ns.NetNS) error { wg.Add(1) runNetListener(wg, "tcp", "0.0.0.0", 5000, expected) return nil }) Expect(err).ToNot(HaveOccurred()) conn, err := net.Dial("tcp", ip.String()+":5000") Expect(err).To(BeNil()) _, err = conn.Write([]byte(expected)) Expect(err).To(BeNil()) conn.Close() err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts)) Expect(err).ToNot(HaveOccurred()) wg.Wait() }) }) It("setup two containers", func() { runTest(func() { defNet := types.DefaultNetworkName intName := "eth0" setupOpts1 := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), Networks: map[string]types.PerNetworkOptions{ defNet: {InterfaceName: intName}, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts1) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) ip1 := res[defNet].Interfaces[intName].Subnets[0].IPNet.IP Expect(ip1.String()).To(ContainSubstring("10.88.0.")) Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) setupOpts2 := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), Networks: map[string]types.PerNetworkOptions{ defNet: {InterfaceName: intName}, }, }, } netNSContainer2, err := netns.NewNS() Expect(err).ToNot(HaveOccurred()) defer netns.UnmountNS(netNSContainer2) defer netNSContainer2.Close() res, err = libpodNet.Setup(netNSContainer2.Path(), setupOpts2) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) ip2 := res[defNet].Interfaces[intName].Subnets[0].IPNet.IP Expect(ip2.String()).To(ContainSubstring("10.88.0.")) Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) Expect(ip1.Equal(ip2)).To(BeFalse(), "IP1 %s should not be equal to IP2 %s", ip1.String(), ip2.String()) err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts1)) Expect(err).ToNot(HaveOccurred()) err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts2)) Expect(err).ToNot(HaveOccurred()) }) }) It("setup dualstack network", func() { runTest(func() { s1, _ := types.ParseCIDR("10.0.0.1/24") s2, _ := types.ParseCIDR("fd10:88:a::/64") network, err := libpodNet.NetworkCreate(types.Network{ Subnets: []types.Subnet{ {Subnet: s1}, {Subnet: s2}, }, }) Expect(err).ToNot(HaveOccurred()) netName := network.Name intName := "eth0" setupOpts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), Networks: map[string]types.PerNetworkOptions{ netName: {InterfaceName: intName}, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(netName)) Expect(res[netName].Interfaces).To(HaveKey(intName)) Expect(res[netName].Interfaces[intName].Subnets).To(HaveLen(2)) ip1 := res[netName].Interfaces[intName].Subnets[0].IPNet.IP Expect(ip1.String()).To(ContainSubstring("10.0.0.")) gw1 := res[netName].Interfaces[intName].Subnets[0].Gateway Expect(gw1.String()).To(Equal("10.0.0.1")) ip2 := res[netName].Interfaces[intName].Subnets[1].IPNet.IP Expect(ip2.String()).To(ContainSubstring("fd10:88:a::")) gw2 := res[netName].Interfaces[intName].Subnets[0].Gateway Expect(gw2.String()).To(Equal("fd10:88:a::1")) Expect(res[netName].Interfaces[intName].MacAddress).To(HaveLen(6)) // check in the container namespace if the settings are applied err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() i, err := net.InterfaceByName(intName) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(intName)) addrs, err := i.Addrs() Expect(err).ToNot(HaveOccurred()) subnet1 := s1.IPNet subnet1.IP = ip1 subnet2 := s2.IPNet subnet2.IP = ip2 Expect(addrs).To(ContainElements(EqualSubnet(&subnet1), EqualSubnet(&subnet2))) // check loopback adapter i, err = net.InterfaceByName("lo") Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal("lo")) Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") return nil }) Expect(err).ToNot(HaveOccurred()) bridgeName := network.NetworkInterface // check settings on the host side i, err := net.InterfaceByName(bridgeName) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(bridgeName)) addrs, err := i.Addrs() Expect(err).ToNot(HaveOccurred()) // test that the gateway ip is assigned to the interface subnet1 := s1.IPNet subnet1.IP = gw1 subnet2 := s2.IPNet subnet2.IP = gw2 Expect(addrs).To(ContainElements(EqualSubnet(&subnet1), EqualSubnet(&subnet2))) err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) Expect(err).ToNot(HaveOccurred()) }) }) It("setup two networks", func() { runTest(func() { s1, _ := types.ParseCIDR("10.0.0.1/24") network1, err := libpodNet.NetworkCreate(types.Network{ Subnets: []types.Subnet{ {Subnet: s1}, }, }) Expect(err).ToNot(HaveOccurred()) netName1 := network1.Name intName1 := "eth0" s2, _ := types.ParseCIDR("10.1.0.0/24") network2, err := libpodNet.NetworkCreate(types.Network{ Subnets: []types.Subnet{ {Subnet: s2}, }, }) Expect(err).ToNot(HaveOccurred()) netName2 := network2.Name intName2 := "eth1" setupOpts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), Networks: map[string]types.PerNetworkOptions{ netName1: {InterfaceName: intName1}, netName2: {InterfaceName: intName2}, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(2)) Expect(res).To(HaveKey(netName1)) Expect(res).To(HaveKey(netName2)) Expect(res[netName1].Interfaces).To(HaveKey(intName1)) Expect(res[netName2].Interfaces).To(HaveKey(intName2)) Expect(res[netName1].Interfaces[intName1].Subnets).To(HaveLen(1)) ip1 := res[netName1].Interfaces[intName1].Subnets[0].IPNet.IP Expect(ip1.String()).To(ContainSubstring("10.0.0.")) gw1 := res[netName1].Interfaces[intName1].Subnets[0].Gateway Expect(gw1.String()).To(Equal("10.0.0.1")) ip2 := res[netName2].Interfaces[intName2].Subnets[0].IPNet.IP Expect(ip2.String()).To(ContainSubstring("10.1.0.")) gw2 := res[netName2].Interfaces[intName2].Subnets[0].Gateway Expect(gw2.String()).To(Equal("10.1.0.1")) mac1 := res[netName1].Interfaces[intName1].MacAddress Expect(mac1).To(HaveLen(6)) mac2 := res[netName2].Interfaces[intName2].MacAddress Expect(mac2).To(HaveLen(6)) // check in the container namespace if the settings are applied err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() i, err := net.InterfaceByName(intName1) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(intName1)) addrs, err := i.Addrs() Expect(err).ToNot(HaveOccurred()) subnet1 := s1.IPNet subnet1.IP = ip1 Expect(addrs).To(ContainElements(EqualSubnet(&subnet1))) i, err = net.InterfaceByName(intName2) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(intName2)) addrs, err = i.Addrs() Expect(err).ToNot(HaveOccurred()) subnet2 := s2.IPNet subnet2.IP = ip2 Expect(addrs).To(ContainElements(EqualSubnet(&subnet2))) // check loopback adapter i, err = net.InterfaceByName("lo") Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal("lo")) Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") return nil }) Expect(err).ToNot(HaveOccurred()) bridgeName1 := network1.NetworkInterface // check settings on the host side i, err := net.InterfaceByName(bridgeName1) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(bridgeName1)) addrs, err := i.Addrs() Expect(err).ToNot(HaveOccurred()) // test that the gateway ip is assigned to the interface subnet1 := s1.IPNet subnet1.IP = gw1 Expect(addrs).To(ContainElements(EqualSubnet(&subnet1))) bridgeName2 := network2.NetworkInterface // check settings on the host side i, err = net.InterfaceByName(bridgeName2) Expect(err).ToNot(HaveOccurred()) Expect(i.Name).To(Equal(bridgeName2)) addrs, err = i.Addrs() Expect(err).ToNot(HaveOccurred()) // test that the gateway ip is assigned to the interface subnet2 := s2.IPNet subnet2.IP = gw2 Expect(addrs).To(ContainElements(EqualSubnet(&subnet2))) err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) Expect(err).ToNot(HaveOccurred()) }) }) for _, proto := range []string{"tcp", "udp"} { // copy proto to extra var to keep correct references in the goroutines protocol := proto It("run with exposed ports protocol "+protocol, func() { runTest(func() { testdata := stringid.GenerateNonCryptoID() defNet := types.DefaultNetworkName intName := "eth0" setupOpts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), PortMappings: []types.PortMapping{{ Protocol: protocol, HostIP: "127.0.0.1", HostPort: 5000, ContainerPort: 5000, }}, Networks: map[string]types.PerNetworkOptions{ defNet: {InterfaceName: intName}, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).To(BeNil()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) Expect(res[defNet].Interfaces[intName].Subnets[0].IPNet.IP.String()).To(ContainSubstring("10.88.0.")) Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) // default network has no dns Expect(res[defNet].DNSServerIPs).To(BeEmpty()) Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) var wg sync.WaitGroup wg.Add(1) // start a listener in the container ns err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata) return nil }) Expect(err).To(BeNil()) conn, err := net.Dial(protocol, "127.0.0.1:5000") Expect(err).To(BeNil()) _, err = conn.Write([]byte(testdata)) Expect(err).To(BeNil()) conn.Close() // wait for the listener to finish wg.Wait() err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) Expect(err).To(BeNil()) }) }) It("run with range ports protocol "+protocol, func() { runTest(func() { defNet := types.DefaultNetworkName intName := "eth0" setupOpts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: stringid.GenerateNonCryptoID(), PortMappings: []types.PortMapping{{ Protocol: protocol, HostIP: "127.0.0.1", HostPort: 5001, ContainerPort: 5000, Range: 3, }}, Networks: map[string]types.PerNetworkOptions{ defNet: {InterfaceName: intName}, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).To(BeNil()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) containerIP := res[defNet].Interfaces[intName].Subnets[0].IPNet.IP.String() Expect(containerIP).To(ContainSubstring("10.88.0.")) Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) // default network has no dns Expect(res[defNet].DNSServerIPs).To(BeEmpty()) Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) // loop over all ports for p := 5001; p < 5004; p++ { port := p var wg sync.WaitGroup wg.Add(1) testdata := stringid.GenerateNonCryptoID() // start a listener in the container ns err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() runNetListener(&wg, protocol, containerIP, port-1, testdata) return nil }) Expect(err).To(BeNil()) conn, err := net.Dial(protocol, net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) Expect(err).To(BeNil()) _, err = conn.Write([]byte(testdata)) Expect(err).To(BeNil()) conn.Close() // wait for the listener to finish wg.Wait() } err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) Expect(err).To(BeNil()) }) }) } It("simple teardown", func() { runTest(func() { defNet := types.DefaultNetworkName intName := "eth0" opts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: "someID", ContainerName: "someName", Networks: map[string]types.PerNetworkOptions{ defNet: { InterfaceName: intName, }, }, }, } res, err := libpodNet.Setup(netNSContainer.Path(), opts) Expect(err).ToNot(HaveOccurred()) Expect(res).To(HaveLen(1)) Expect(res).To(HaveKey(defNet)) Expect(res[defNet].Interfaces).To(HaveKey(intName)) Expect(res[defNet].Interfaces[intName].Subnets).To(HaveLen(1)) ip := res[defNet].Interfaces[intName].Subnets[0].IPNet.IP Expect(ip.String()).To(ContainSubstring("10.88.0.")) gw := res[defNet].Interfaces[intName].Subnets[0].Gateway Expect(gw.String()).To(Equal("10.88.0.1")) macAddress := res[defNet].Interfaces[intName].MacAddress Expect(macAddress).To(HaveLen(6)) err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts)) Expect(err).ToNot(HaveOccurred()) err = netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() // check that the container interface is removed _, err := net.InterfaceByName(intName) Expect(err).To(HaveOccurred()) return nil }) Expect(err).ToNot(HaveOccurred()) // default bridge name bridgeName := "podman0" // check that bridge interface was removed _, err = net.InterfaceByName(bridgeName) Expect(err).To(HaveOccurred()) }) }) It("test netavark error", func() { runTest(func() { intName := "eth0" err := netNSContainer.Do(func(_ ns.NetNS) error { defer GinkgoRecover() attr := netlink.NewLinkAttrs() attr.Name = "eth0" err := netlink.LinkAdd(&netlink.Bridge{LinkAttrs: attr}) Expect(err).ToNot(HaveOccurred()) return nil }) Expect(err).ToNot(HaveOccurred()) defNet := types.DefaultNetworkName opts := types.SetupOptions{ NetworkOptions: types.NetworkOptions{ ContainerID: "someID", ContainerName: "someName", Networks: map[string]types.PerNetworkOptions{ defNet: { InterfaceName: intName, }, }, }, } _, err = libpodNet.Setup(netNSContainer.Path(), opts) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("interface eth0 already exists on container namespace")) }) }) }) func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) { switch protocol { case "tcp": ln, err := net.Listen(protocol, net.JoinHostPort(ip, strconv.Itoa(port))) Expect(err).To(BeNil()) // make sure to read in a separate goroutine to not block go func() { defer GinkgoRecover() defer wg.Done() defer ln.Close() conn, err := ln.Accept() Expect(err).To(BeNil()) defer conn.Close() conn.SetDeadline(time.Now().Add(1 * time.Second)) data, err := ioutil.ReadAll(conn) Expect(err).To(BeNil()) Expect(string(data)).To(Equal(expectedData)) }() case "udp": conn, err := net.ListenUDP("udp", &net.UDPAddr{ IP: net.ParseIP(ip), Port: port, }) Expect(err).To(BeNil()) conn.SetDeadline(time.Now().Add(1 * time.Second)) go func() { defer GinkgoRecover() defer wg.Done() defer conn.Close() data := make([]byte, len(expectedData)) i, err := conn.Read(data) Expect(err).To(BeNil()) Expect(i).To(Equal(len(expectedData))) Expect(string(data)).To(Equal(expectedData)) }() default: Fail("unsupported protocol") } }