summaryrefslogtreecommitdiff
path: root/libpod/network/netavark
diff options
context:
space:
mode:
authorPaul Holzinger <pholzing@redhat.com>2021-10-14 10:33:18 +0200
committerPaul Holzinger <pholzing@redhat.com>2021-11-11 15:54:02 +0100
commiteaae29462880aa0fb17e8d448cc79519e070e64f (patch)
tree7da8ee0d642bb34f0f7c97a64e95f93661966050 /libpod/network/netavark
parent12c62b92ff2f63cb34dcb9c0555b96983e6aad94 (diff)
downloadpodman-eaae29462880aa0fb17e8d448cc79519e070e64f.tar.gz
podman-eaae29462880aa0fb17e8d448cc79519e070e64f.tar.bz2
podman-eaae29462880aa0fb17e8d448cc79519e070e64f.zip
netavark network interface
Implement a new network interface for netavark. For now only bridge networking is supported. The interface can create/list/inspect/remove networks. For setup and teardown netavark will be invoked. Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Diffstat (limited to 'libpod/network/netavark')
-rw-r--r--libpod/network/netavark/config.go210
-rw-r--r--libpod/network/netavark/config_test.go1123
-rw-r--r--libpod/network/netavark/const.go5
-rw-r--r--libpod/network/netavark/exec.go116
-rw-r--r--libpod/network/netavark/netavark_suite_test.go37
-rw-r--r--libpod/network/netavark/network.go275
-rw-r--r--libpod/network/netavark/run.go90
-rw-r--r--libpod/network/netavark/run_test.go363
-rw-r--r--libpod/network/netavark/testfiles/invalid/broken.json16
-rw-r--r--libpod/network/netavark/testfiles/invalid/invalid name.json19
-rw-r--r--libpod/network/netavark/testfiles/invalid/invalid_gateway.json19
-rw-r--r--libpod/network/netavark/testfiles/invalid/name_missmatch.json19
-rw-r--r--libpod/network/netavark/testfiles/invalid/wrongID.json19
-rw-r--r--libpod/network/netavark/testfiles/valid/bridge.json23
-rw-r--r--libpod/network/netavark/testfiles/valid/dualstack.json23
-rw-r--r--libpod/network/netavark/testfiles/valid/internal.json18
-rw-r--r--libpod/network/netavark/testfiles/valid/label.json22
-rw-r--r--libpod/network/netavark/testfiles/valid/mtu.json22
-rw-r--r--libpod/network/netavark/testfiles/valid/podman.json19
-rw-r--r--libpod/network/netavark/testfiles/valid/vlan.json22
20 files changed, 2460 insertions, 0 deletions
diff --git a/libpod/network/netavark/config.go b/libpod/network/netavark/config.go
new file mode 100644
index 000000000..5cab76710
--- /dev/null
+++ b/libpod/network/netavark/config.go
@@ -0,0 +1,210 @@
+// +build linux
+
+package netavark
+
+import (
+ "encoding/json"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/containers/podman/v3/libpod/define"
+ internalutil "github.com/containers/podman/v3/libpod/network/internal/util"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/storage/pkg/stringid"
+ "github.com/pkg/errors"
+)
+
+// NetworkCreate will take a partial filled Network and fill the
+// missing fields. It creates the Network and returns the full Network.
+func (n *netavarkNetwork) NetworkCreate(net types.Network) (types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return types.Network{}, err
+ }
+ network, err := n.networkCreate(net, false)
+ if err != nil {
+ return types.Network{}, err
+ }
+ // add the new network to the map
+ n.networks[network.Name] = network
+ return *network, nil
+}
+
+func (n *netavarkNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*types.Network, error) {
+ // if no driver is set use the default one
+ if newNetwork.Driver == "" {
+ newNetwork.Driver = types.DefaultNetworkDriver
+ }
+ if !defaultNet {
+ // FIXME: Should we use a different type for network create without the ID field?
+ // the caller is not allowed to set a specific ID
+ if newNetwork.ID != "" {
+ return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create")
+ }
+
+ // generate random network ID
+ var i int
+ for i = 0; i < 1000; i++ {
+ id := stringid.GenerateNonCryptoID()
+ if _, err := n.getNetwork(id); err != nil {
+ newNetwork.ID = id
+ break
+ }
+ }
+ if i == 1000 {
+ return nil, errors.New("failed to create random network ID")
+ }
+ }
+
+ err := internalutil.CommonNetworkCreate(n, &newNetwork)
+ if err != nil {
+ return nil, err
+ }
+
+ // Only get the used networks for validation if we do not create the default network.
+ // The default network should not be validated against used subnets, we have to ensure
+ // that this network can always be created even when a subnet is already used on the host.
+ // This could happen if you run a container on this net, then the cni interface will be
+ // created on the host and "block" this subnet from being used again.
+ // Therefore the next podman command tries to create the default net again and it would
+ // fail because it thinks the network is used on the host.
+ var usedNetworks []*net.IPNet
+ if !defaultNet {
+ usedNetworks, err = internalutil.GetUsedSubnets(n)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ switch newNetwork.Driver {
+ case types.BridgeNetworkDriver:
+ err = internalutil.CreateBridge(n, &newNetwork, usedNetworks)
+ if err != nil {
+ return nil, err
+ }
+ // validate the given options, we do not need them but just check to make sure they are valid
+ for key, value := range newNetwork.Options {
+ switch key {
+ case "mtu":
+ _, err = internalutil.ParseMTU(value)
+ if err != nil {
+ return nil, err
+ }
+
+ case "vlan":
+ _, err = internalutil.ParseVlan(value)
+ if err != nil {
+ return nil, err
+ }
+
+ default:
+ return nil, errors.Errorf("unsupported network option %s", key)
+ }
+ }
+
+ default:
+ return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver)
+ }
+
+ err = internalutil.ValidateSubnets(&newNetwork, usedNetworks)
+ if err != nil {
+ return nil, err
+ }
+
+ // FIXME: If we have a working solution for internal networks with dns this check should be removed.
+ if newNetwork.DNSEnabled && newNetwork.Internal {
+ return nil, errors.New("cannot set internal and dns enabled")
+ }
+
+ newNetwork.Created = time.Now()
+
+ if !defaultNet {
+ confPath := filepath.Join(n.networkConfigDir, newNetwork.Name+".json")
+ f, err := os.Create(confPath)
+ if err != nil {
+ return nil, err
+ }
+ enc := json.NewEncoder(f)
+ enc.SetIndent("", " ")
+ err = enc.Encode(newNetwork)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &newNetwork, nil
+}
+
+// NetworkRemove will remove the Network with the given name or ID.
+// It does not ensure that the network is unused.
+func (n *netavarkNetwork) NetworkRemove(nameOrID string) error {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return err
+ }
+
+ network, err := n.getNetwork(nameOrID)
+ if err != nil {
+ return err
+ }
+
+ // Removing the default network is not allowed.
+ if network.Name == n.defaultNetwork {
+ return errors.Errorf("default network %s cannot be removed", n.defaultNetwork)
+ }
+
+ file := filepath.Join(n.networkConfigDir, network.Name+".json")
+ // make sure to not error for ErrNotExist
+ if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ delete(n.networks, network.Name)
+ return nil
+}
+
+// NetworkList will return all known Networks. Optionally you can
+// supply a list of filter functions. Only if a network matches all
+// functions it is returned.
+func (n *netavarkNetwork) NetworkList(filters ...types.FilterFunc) ([]types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ networks := make([]types.Network, 0, len(n.networks))
+outer:
+ for _, net := range n.networks {
+ for _, filter := range filters {
+ // All filters have to match, if one does not match we can skip to the next network.
+ if !filter(*net) {
+ continue outer
+ }
+ }
+ networks = append(networks, *net)
+ }
+ return networks, nil
+}
+
+// NetworkInspect will return the Network with the given name or ID.
+func (n *netavarkNetwork) NetworkInspect(nameOrID string) (types.Network, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return types.Network{}, err
+ }
+
+ network, err := n.getNetwork(nameOrID)
+ if err != nil {
+ return types.Network{}, err
+ }
+ return *network, nil
+}
diff --git a/libpod/network/netavark/config_test.go b/libpod/network/netavark/config_test.go
new file mode 100644
index 000000000..ee4a825f1
--- /dev/null
+++ b/libpod/network/netavark/config_test.go
@@ -0,0 +1,1123 @@
+// +build linux
+
+package netavark_test
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ gomegaTypes "github.com/onsi/gomega/types"
+ "github.com/sirupsen/logrus"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+)
+
+var _ = Describe("Config", func() {
+ var (
+ libpodNet types.ContainerNetwork
+ networkConfDir string
+ logBuffer bytes.Buffer
+ )
+
+ BeforeEach(func() {
+ var err error
+ networkConfDir, err = ioutil.TempDir("", "podman_netavark_test")
+ if err != nil {
+ Fail("Failed to create tmpdir")
+
+ }
+ logBuffer = bytes.Buffer{}
+ logrus.SetOutput(&logBuffer)
+ })
+
+ JustBeforeEach(func() {
+ var err error
+ libpodNet, err = getNetworkInterface(networkConfDir, false)
+ if err != nil {
+ Fail("Failed to create NewCNINetworkInterface")
+ }
+ })
+
+ AfterEach(func() {
+ os.RemoveAll(networkConfDir)
+ })
+
+ Context("basic network config tests", func() {
+
+ It("check default network config exists", func() {
+ networks, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks[0].Name).To(Equal("podman"))
+ Expect(networks[0].Driver).To(Equal("bridge"))
+ Expect(networks[0].ID).To(Equal("2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9"))
+ Expect(networks[0].NetworkInterface).To(Equal("podman0"))
+ Expect(networks[0].Created.Before(time.Now())).To(BeTrue())
+ Expect(networks[0].Subnets).To(HaveLen(1))
+ Expect(networks[0].Subnets[0].Subnet.String()).To(Equal("10.88.0.0/16"))
+ Expect(networks[0].Subnets[0].Gateway.String()).To(Equal("10.88.0.1"))
+ Expect(networks[0].Subnets[0].LeaseRange).To(BeNil())
+ Expect(networks[0].IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(networks[0].Options).To(BeEmpty())
+ Expect(networks[0].Labels).To(BeEmpty())
+ Expect(networks[0].DNSEnabled).To(BeFalse())
+ Expect(networks[0].Internal).To(BeFalse())
+ })
+
+ It("basic network create, inspect and remove", func() {
+ // Because we get the time from the file create timestamp there is small precision
+ // loss so lets remove 500 milliseconds to make sure this test does not flake.
+ now := time.Now().Add(-500 * time.Millisecond)
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ path := filepath.Join(networkConfDir, network1.Name+".json")
+ Expect(path).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.IPAMOptions).ToNot(BeEmpty())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(network1.Created.After(now)).To(BeTrue())
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+
+ // inspect by name
+ network2, err := libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(BeNil())
+ EqualNetwork(network2, network1)
+
+ // inspect by ID
+ network2, err = libpodNet.NetworkInspect(network1.ID)
+ Expect(err).To(BeNil())
+ EqualNetwork(network2, network1)
+
+ // inspect by partial ID
+ network2, err = libpodNet.NetworkInspect(network1.ID[:10])
+ Expect(err).To(BeNil())
+ EqualNetwork(network2, network1)
+
+ // create a new interface to force a config load from disk
+ libpodNet, err = getNetworkInterface(networkConfDir, false)
+ Expect(err).To(BeNil())
+
+ network2, err = libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(BeNil())
+ EqualNetwork(network2, network1)
+
+ err = libpodNet.NetworkRemove(network1.Name)
+ Expect(err).To(BeNil())
+ Expect(path).ToNot(BeARegularFile())
+
+ _, err = libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network not found"))
+ })
+
+ It("create two networks", func() {
+ network := types.Network{}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.Subnets).To(HaveLen(1))
+
+ network = types.Network{}
+ network2, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network2.Name).ToNot(Equal(network1.Name))
+ Expect(network2.ID).ToNot(Equal(network1.ID))
+ Expect(network2.NetworkInterface).ToNot(Equal(network1.NetworkInterface))
+ Expect(network2.Subnets).To(HaveLen(1))
+ Expect(network2.Subnets[0].Subnet.Contains(network1.Subnets[0].Subnet.IP)).To(BeFalse())
+ })
+
+ It("create bridge config", func() {
+ network := types.Network{Driver: "bridge"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).To(BeEmpty())
+ Expect(network1.Options).To(BeEmpty())
+ Expect(network1.IPAMOptions).ToNot(BeEmpty())
+ Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.DNSEnabled).To(BeFalse())
+ Expect(network1.Internal).To(BeFalse())
+ })
+
+ It("create bridge with same name should fail", func() {
+ network := types.Network{
+ Driver: "bridge",
+ NetworkInterface: "podman2",
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).To(Equal("podman2"))
+ Expect(network1.Driver).To(Equal("bridge"))
+
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("bridge name podman2 already in use"))
+ })
+
+ It("create bridge with subnet", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 subnet", func() {
+ subnet := "fdcc::/64"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.IPv6Enabled).To(BeTrue())
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("fdcc::1"))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+
+ // reload configs from disk
+ libpodNet, err = getNetworkInterface(networkConfDir, false)
+ Expect(err).To(BeNil())
+ // check the the networks are identical
+ network2, err := libpodNet.NetworkInspect(network1.Name)
+ Expect(err).To(BeNil())
+ EqualNetwork(network2, network1)
+ })
+
+ It("create bridge with ipv6 enabled", func() {
+ network := types.Network{
+ Driver: "bridge",
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(ContainSubstring(".0/24"))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv4 subnet", func() {
+ subnet := "10.100.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv6 subnet", func() {
+ subnet := "fd66::/64"
+ n, _ := types.ParseCIDR(subnet)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring(".0/24"))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and ipv4+ipv6 subnet", func() {
+ subnet1 := "10.100.0.0/24"
+ n1, _ := types.ParseCIDR(subnet1)
+ subnet2 := "fd66::/64"
+ n2, _ := types.ParseCIDR(subnet2)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n1}, {Subnet: n2},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(2))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with ipv6 enabled and two ipv4 subnets", func() {
+ subnet1 := "10.100.0.0/24"
+ n1, _ := types.ParseCIDR(subnet1)
+ subnet2 := "10.200.0.0/24"
+ n2, _ := types.ParseCIDR(subnet2)
+
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n1}, {Subnet: n2},
+ },
+ IPv6Enabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(3))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1))
+ Expect(network1.Subnets[0].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2))
+ Expect(network1.Subnets[1].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[1].LeaseRange).To(BeNil())
+ Expect(network1.Subnets[2].Subnet.String()).To(ContainSubstring("::/64"))
+ Expect(network1.Subnets[2].Gateway).ToNot(BeNil())
+ Expect(network1.Subnets[2].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with subnet and gateway", func() {
+ subnet := "10.0.0.5/24"
+ n, _ := types.ParseCIDR(subnet)
+ gateway := "10.0.0.50"
+ g := net.ParseIP(gateway)
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, Gateway: g},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.0.0.0/24"))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal(gateway))
+ Expect(network1.Subnets[0].LeaseRange).To(BeNil())
+ })
+
+ It("create bridge with subnet and gateway not in the same subnet", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ gateway := "10.10.0.50"
+ g := net.ParseIP(gateway)
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, Gateway: g},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+ })
+
+ It("create bridge with subnet and lease range", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ startIP := "10.0.0.10"
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ }},
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP))
+
+ err = libpodNet.NetworkRemove(network1.Name)
+ Expect(err).To(BeNil())
+
+ endIP := "10.0.0.10"
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ network1, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP))
+
+ err = libpodNet.NetworkRemove(network1.Name)
+ Expect(err).To(BeNil())
+
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ network1, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(BeEmpty())
+ Expect(network1.ID).ToNot(BeEmpty())
+ Expect(network1.NetworkInterface).ToNot(BeEmpty())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet))
+ Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1"))
+ Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP))
+ Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP))
+ })
+
+ It("create bridge with subnet and invalid lease range", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ startIP := "10.0.1.2"
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP(startIP),
+ }},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+
+ endIP := "10.1.1.1"
+ network = types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: n, LeaseRange: &types.LeaseRange{
+ EndIP: net.ParseIP(endIP),
+ }},
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("not in subnet"))
+ })
+
+ It("create bridge with broken subnet", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Subnets: []types.Subnet{
+ {Subnet: types.IPNet{}},
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("subnet ip is nil"))
+ })
+
+ It("create network with name", func() {
+ name := "myname"
+ network := types.Network{
+ Name: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal(name))
+ Expect(network1.NetworkInterface).ToNot(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid name", func() {
+ name := "myname@some"
+ network := types.Network{
+ Name: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with name", func() {
+ name := "myname"
+ network := types.Network{
+ Name: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal(name))
+ Expect(network1.NetworkInterface).ToNot(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid name", func() {
+ name := "myname@some"
+ network := types.Network{
+ Name: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with interface name", func() {
+ name := "myname"
+ network := types.Network{
+ NetworkInterface: name,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).ToNot(Equal(name))
+ Expect(network1.NetworkInterface).To(Equal(name))
+ Expect(network1.Driver).To(Equal("bridge"))
+ })
+
+ It("create network with invalid interface name", func() {
+ name := "myname@some"
+ network := types.Network{
+ NetworkInterface: name,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ })
+
+ It("create network with ID should fail", func() {
+ id := "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"
+ network := types.Network{
+ ID: id,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("ID can not be set for network create"))
+ })
+
+ It("create bridge with dns", func() {
+ network := types.Network{
+ Driver: "bridge",
+ DNSEnabled: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.DNSEnabled).To(BeTrue())
+ path := filepath.Join(networkConfDir, network1.Name+".json")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"dns_enabled": true`)
+ })
+
+ It("create bridge with internal", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Internal: true,
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Subnets).To(HaveLen(1))
+ Expect(network1.Subnets[0].Subnet.String()).ToNot(BeEmpty())
+ Expect(network1.Subnets[0].Gateway).To(BeNil())
+ Expect(network1.Internal).To(BeTrue())
+ })
+
+ It("create network with labels", func() {
+ network := types.Network{
+ Labels: map[string]string{
+ "key": "value",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Labels).ToNot(BeNil())
+ Expect(network1.Labels).To(ContainElement("value"))
+ })
+
+ It("create network with mtu option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "mtu": "1500",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Options).ToNot(BeNil())
+ path := filepath.Join(networkConfDir, network1.Name+".json")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"mtu": "1500"`)
+ Expect(network1.Options).To(HaveKeyWithValue("mtu", "1500"))
+ })
+
+ It("create network with invalid mtu option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "mtu": "abc",
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`))
+
+ network = types.Network{
+ Options: map[string]string{
+ "mtu": "-1",
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`mtu -1 is less than zero`))
+ })
+
+ It("create network with vlan option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "vlan": "5",
+ },
+ }
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Driver).To(Equal("bridge"))
+ Expect(network1.Options).ToNot(BeNil())
+ path := filepath.Join(networkConfDir, network1.Name+".json")
+ Expect(path).To(BeARegularFile())
+ grepInFile(path, `"vlan": "5"`)
+ Expect(network1.Options).To(HaveKeyWithValue("vlan", "5"))
+ })
+
+ It("create network with invalid vlan option", func() {
+ network := types.Network{
+ Options: map[string]string{
+ "vlan": "abc",
+ },
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`))
+
+ network = types.Network{
+ Options: map[string]string{
+ "vlan": "-1",
+ },
+ }
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring(`vlan ID -1 must be between 0 and 4094`))
+ })
+
+ It("network create unsupported option", func() {
+ network := types.Network{Options: map[string]string{
+ "someopt": "",
+ }}
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("unsupported network option someopt"))
+ })
+
+ It("network create unsupported driver", func() {
+ network := types.Network{
+ Driver: "someDriver",
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("unsupported driver someDriver"))
+ })
+
+ It("network create internal and dns", func() {
+ network := types.Network{
+ Driver: "bridge",
+ Internal: true,
+ DNSEnabled: true,
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("cannot set internal and dns enabled"))
+ })
+
+ It("network inspect partial ID", func() {
+ network := types.Network{Name: "net4"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.ID).To(HaveLen(64))
+
+ network2, err := libpodNet.NetworkInspect(network1.ID[:10])
+ Expect(err).ToNot(HaveOccurred())
+ EqualNetwork(network2, network1)
+ })
+
+ It("network create two with same name", func() {
+ network := types.Network{Name: "net"}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Name).To(Equal("net"))
+ network = types.Network{Name: "net"}
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("network name net already used"))
+ })
+
+ It("remove default network config should fail", func() {
+ err := libpodNet.NetworkRemove("podman")
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("default network podman cannot be removed"))
+
+ network, err := libpodNet.NetworkInspect("podman")
+ Expect(err).To(BeNil())
+ err = libpodNet.NetworkRemove(network.ID)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("default network podman cannot be removed"))
+ })
+
+ It("network create with same subnet", func() {
+ subnet := "10.0.0.0/24"
+ n, _ := types.ParseCIDR(subnet)
+ subnet2 := "10.10.0.0/24"
+ n2, _ := types.ParseCIDR(subnet2)
+ network := types.Network{Subnets: []types.Subnet{{Subnet: n}, {Subnet: n2}}}
+ network1, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(BeNil())
+ Expect(network1.Subnets).To(HaveLen(2))
+ network = types.Network{Subnets: []types.Subnet{{Subnet: n}}}
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("subnet 10.0.0.0/24 is already used on the host or by another config"))
+ network = types.Network{Subnets: []types.Subnet{{Subnet: n2}}}
+ _, err = libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("subnet 10.10.0.0/24 is already used on the host or by another config"))
+ })
+ })
+
+ Context("network load valid existing ones", func() {
+
+ BeforeEach(func() {
+ dir := "testfiles/valid"
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ Fail("Failed to read test directory")
+ }
+ for _, file := range files {
+ filename := file.Name()
+ data, err := ioutil.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700)
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ }
+ })
+
+ It("load networks from disk", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(7))
+ // test the we do not show logrus warnings/errors
+ logString := logBuffer.String()
+ Expect(logString).To(BeEmpty())
+ })
+
+ It("change network struct fields should not affect network struct in the backend", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(7))
+
+ nets[0].Name = "myname"
+ nets, err = libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(7))
+ Expect(nets).ToNot(ContainElement(HaveNetworkName("myname")))
+
+ network, err := libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ network.NetworkInterface = "abc"
+
+ network, err = libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ Expect(network.NetworkInterface).ToNot(Equal("abc"))
+ })
+
+ It("bridge network", func() {
+ network, err := libpodNet.NetworkInspect("bridge")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("bridge"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman9"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.8.0/24"))
+ Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.8.1"))
+ Expect(network.Subnets[0].LeaseRange).ToNot(BeNil())
+ Expect(network.Subnets[0].LeaseRange.StartIP.String()).To(Equal("10.89.8.20"))
+ Expect(network.Subnets[0].LeaseRange.EndIP.String()).To(Equal("10.89.8.50"))
+ Expect(network.Internal).To(BeFalse())
+ })
+
+ It("internal network", func() {
+ network, err := libpodNet.NetworkInspect("internal")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("internal"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman8"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.7.0/24"))
+ Expect(network.Subnets[0].Gateway).To(BeNil())
+ Expect(network.Internal).To(BeTrue())
+ })
+
+ It("bridge network with mtu", func() {
+ network, err := libpodNet.NetworkInspect("mtu")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("mtu"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman13"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.11.0/24"))
+ Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.11.1"))
+ Expect(network.Internal).To(BeFalse())
+ Expect(network.Options).To(HaveLen(1))
+ Expect(network.Options).To(HaveKeyWithValue("mtu", "1500"))
+ })
+
+ It("bridge network with vlan", func() {
+ network, err := libpodNet.NetworkInspect("vlan")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("vlan"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman14"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Options).To(HaveLen(1))
+ Expect(network.Options).To(HaveKeyWithValue("vlan", "5"))
+ })
+
+ It("bridge network with labels", func() {
+ network, err := libpodNet.NetworkInspect("label")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("label"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman15"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(1))
+ Expect(network.Labels).To(HaveLen(1))
+ Expect(network.Labels).To(HaveKeyWithValue("mykey", "value"))
+ })
+
+ It("dual stack network", func() {
+ network, err := libpodNet.NetworkInspect("dualstack")
+ Expect(err).To(BeNil())
+ Expect(network.Name).To(Equal("dualstack"))
+ Expect(network.ID).To(HaveLen(64))
+ Expect(network.NetworkInterface).To(Equal("podman21"))
+ Expect(network.Driver).To(Equal("bridge"))
+ Expect(network.Subnets).To(HaveLen(2))
+
+ sub1, _ := types.ParseCIDR("fd10:88:a::/64")
+ sub2, _ := types.ParseCIDR("10.89.19.0/24")
+ Expect(network.Subnets).To(ContainElements(
+ types.Subnet{Subnet: sub1, Gateway: net.ParseIP("fd10:88:a::1")},
+ types.Subnet{Subnet: sub2, Gateway: net.ParseIP("10.89.19.10").To4()},
+ ))
+ })
+
+ It("network list with filters (name)", func() {
+ filters := map[string][]string{
+ "name": {"internal", "bridge"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (partial name)", func() {
+ filters := map[string][]string{
+ "name": {"inte", "bri"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (partial id)", func() {
+ filters := map[string][]string{
+ "id": {"3bed2cb3a3acf7b6a8ef408420", "17f29b073143d8cd97b5bbe492bde"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(2))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge")))
+ })
+
+ It("network list with filters (driver)", func() {
+ filters := map[string][]string{
+ "driver": {"bridge"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(7))
+ Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"),
+ HaveNetworkName("mtu"), HaveNetworkName("vlan"), HaveNetworkName("podman"),
+ HaveNetworkName("label"), HaveNetworkName("dualstack")))
+ })
+
+ It("network list with filters (label)", func() {
+ filters := map[string][]string{
+ "label": {"mykey"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+
+ filters = map[string][]string{
+ "label": {"mykey=value"},
+ }
+ filterFuncs, err = util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err = libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+ })
+
+ It("network list with filters", func() {
+ filters := map[string][]string{
+ "driver": {"bridge"},
+ "label": {"mykey"},
+ }
+ filterFuncs, err := util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+ Expect(filterFuncs).To(HaveLen(2))
+
+ networks, err := libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(1))
+ Expect(networks).To(ConsistOf(HaveNetworkName("label")))
+
+ filters = map[string][]string{
+ "driver": {"macvlan"},
+ "label": {"mykey"},
+ }
+ filterFuncs, err = util.GenerateNetworkFilters(filters)
+ Expect(err).To(BeNil())
+
+ networks, err = libpodNet.NetworkList(filterFuncs...)
+ Expect(err).To(BeNil())
+ Expect(networks).To(HaveLen(0))
+ })
+
+ It("create bridge network with used interface name", func() {
+ network := types.Network{
+ NetworkInterface: "podman9",
+ }
+ _, err := libpodNet.NetworkCreate(network)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(ContainSubstring("bridge name podman9 already in use"))
+ })
+ })
+
+ Context("network load invalid existing ones", func() {
+
+ BeforeEach(func() {
+ dir := "testfiles/invalid"
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ Fail("Failed to read test directory")
+ }
+ for _, file := range files {
+ filename := file.Name()
+ data, err := ioutil.ReadFile(filepath.Join(dir, filename))
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700)
+ if err != nil {
+ Fail("Failed to copy test files")
+ }
+ }
+ })
+
+ It("load invalid networks from disk", func() {
+ nets, err := libpodNet.NetworkList()
+ Expect(err).To(BeNil())
+ Expect(nets).To(HaveLen(1))
+ logString := logBuffer.String()
+ Expect(logString).To(ContainSubstring("Error reading network config file \\\"%s/broken.json\\\": unexpected EOF", networkConfDir))
+ Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid name.json\\\" has invalid name: \\\"invalid name\\\", skipping: names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*: invalid argument", networkConfDir))
+ Expect(logString).To(ContainSubstring("Network config name \\\"name_miss\\\" does not match file name \\\"name_missmatch.json\\\", skipping"))
+ Expect(logString).To(ContainSubstring("Network config \\\"%s/wrongID.json\\\" could not be parsed, skipping: invalid network ID \\\"someID\\\"", networkConfDir))
+ Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid_gateway.json\\\" could not be parsed, skipping: gateway 10.89.100.1 not in subnet 10.89.9.0/24", networkConfDir))
+ })
+
+ })
+
+})
+
+func grepInFile(path string, match string) {
+ data, err := ioutil.ReadFile(path)
+ ExpectWithOffset(1, err).To(BeNil())
+ ExpectWithOffset(1, string(data)).To(ContainSubstring(match))
+}
+
+// HaveNetworkName is a custom GomegaMatcher to match a network name
+func HaveNetworkName(name string) gomegaTypes.GomegaMatcher {
+ return WithTransform(func(e types.Network) string {
+ return e.Name
+ }, Equal(name))
+}
+
+// EqualNetwork must be used because comparing the time with deep equal does not work
+func EqualNetwork(net1, net2 types.Network) {
+ ExpectWithOffset(1, net1.Created.Equal(net2.Created)).To(BeTrue(), "net1 created: %v is not equal net2 created: %v", net1.Created, net2.Created)
+ net1.Created = time.Time{}
+ net2.Created = time.Time{}
+ ExpectWithOffset(1, net1).To(Equal(net2))
+}
diff --git a/libpod/network/netavark/const.go b/libpod/network/netavark/const.go
new file mode 100644
index 000000000..9709315c6
--- /dev/null
+++ b/libpod/network/netavark/const.go
@@ -0,0 +1,5 @@
+// +build linux
+
+package netavark
+
+const defaultBridgeName = "podman"
diff --git a/libpod/network/netavark/exec.go b/libpod/network/netavark/exec.go
new file mode 100644
index 000000000..700df9a69
--- /dev/null
+++ b/libpod/network/netavark/exec.go
@@ -0,0 +1,116 @@
+package netavark
+
+import (
+ "encoding/json"
+ "errors"
+ "os"
+ "os/exec"
+ "strconv"
+
+ "github.com/sirupsen/logrus"
+)
+
+type netavarkError struct {
+ exitCode int
+ // Set the json key to "error" so we can directly unmarshal into this struct
+ Msg string `json:"error"`
+ err error
+}
+
+func (e *netavarkError) Error() string {
+ ec := ""
+ // only add the exit code the the error message if we have at least info log level
+ // the normal user does not need to care about the number
+ if e.exitCode > 0 && logrus.IsLevelEnabled(logrus.InfoLevel) {
+ ec = " (exit code " + strconv.Itoa(e.exitCode) + ")"
+ }
+ msg := "netavark" + ec
+ if len(msg) > 0 {
+ msg += ": " + e.Msg
+ }
+ if e.err != nil {
+ msg += ": " + e.err.Error()
+ }
+ return msg
+}
+
+func (e *netavarkError) Unwrap() error {
+ return e.err
+}
+
+func newNetavarkError(msg string, err error) error {
+ return &netavarkError{
+ Msg: msg,
+ err: err,
+ }
+}
+
+// execNetavark will execute netavark with the following arguments
+// It takes the path to the binary, the list of args and an interface which is
+// marshaled to json and send via stdin to netavark. The result interface is
+// used to marshal the netavark output into it. This can be nil.
+// All errors return by this function should be of the type netavarkError
+// to provide a helpful error message.
+func execNetavark(binary string, args []string, stdin, result interface{}) error {
+ stdinR, stdinW, err := os.Pipe()
+ if err != nil {
+ return newNetavarkError("failed to create stdin pipe", err)
+ }
+ defer stdinR.Close()
+
+ stdoutR, stdoutW, err := os.Pipe()
+ if err != nil {
+ return newNetavarkError("failed to create stdout pipe", err)
+ }
+ defer stdoutR.Close()
+ defer stdoutW.Close()
+
+ cmd := exec.Command(binary, args...)
+ // connect the pipes to stdin and stdout
+ cmd.Stdin = stdinR
+ cmd.Stdout = stdoutW
+ // connect stderr to the podman stderr for logging
+ cmd.Stderr = os.Stderr
+ // set the netavark log level to the same as the podman
+ cmd.Env = append(os.Environ(), "RUST_LOG="+logrus.GetLevel().String())
+ // if we run with debug log level lets also set RUST_BACKTRACE=1 so we can get the full stack trace in case of panics
+ if logrus.IsLevelEnabled(logrus.DebugLevel) {
+ cmd.Env = append(cmd.Env, "RUST_BACKTRACE=1")
+ }
+
+ err = cmd.Start()
+ if err != nil {
+ return newNetavarkError("failed to start process", err)
+ }
+ err = json.NewEncoder(stdinW).Encode(stdin)
+ stdinW.Close()
+ if err != nil {
+ return newNetavarkError("failed to encode stdin data", err)
+ }
+
+ dec := json.NewDecoder(stdoutR)
+
+ err = cmd.Wait()
+ stdoutW.Close()
+ if err != nil {
+ exitError := &exec.ExitError{}
+ if errors.As(err, &exitError) {
+ ne := &netavarkError{}
+ // lets disallow unknown fields to make sure we do not get some unexpected stuff
+ dec.DisallowUnknownFields()
+ // this will unmarshal the error message into the error struct
+ ne.err = dec.Decode(ne)
+ ne.exitCode = exitError.ExitCode()
+ return ne
+ }
+ return newNetavarkError("unexpected failure during execution", err)
+ }
+
+ if result != nil {
+ err = dec.Decode(result)
+ if err != nil {
+ return newNetavarkError("failed to decode result", err)
+ }
+ }
+ return nil
+}
diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go
new file mode 100644
index 000000000..b24f880e0
--- /dev/null
+++ b/libpod/network/netavark/netavark_suite_test.go
@@ -0,0 +1,37 @@
+// +build linux
+
+package netavark_test
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/netavark"
+ "github.com/containers/podman/v3/libpod/network/types"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+)
+
+func TestNetavark(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "Netavark Suite")
+}
+
+var netavarkBinary string
+
+func init() {
+ netavarkBinary = os.Getenv("NETAVARK_BINARY")
+ if netavarkBinary == "" {
+ netavarkBinary = "/usr/libexec/podman/netavark"
+ }
+}
+
+func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, error) {
+ return netavark.NewNetworkInterface(netavark.InitConfig{
+ NetworkConfigDir: confDir,
+ IsMachine: machine,
+ NetavarkBinary: netavarkBinary,
+ LockFile: filepath.Join(confDir, "netavark.lock"),
+ })
+}
diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go
new file mode 100644
index 000000000..aa7dc8183
--- /dev/null
+++ b/libpod/network/netavark/network.go
@@ -0,0 +1,275 @@
+// +build linux
+
+package netavark
+
+import (
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/containers/podman/v3/libpod/define"
+ "github.com/containers/podman/v3/libpod/network/internal/util"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/storage/pkg/lockfile"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+type netavarkNetwork struct {
+ // networkConfigDir is directory where the network config files are stored.
+ networkConfigDir string
+
+ // netavarkBinary is the path to the netavark binary.
+ netavarkBinary string
+
+ // defaultNetwork is the name for the default network.
+ defaultNetwork string
+ // defaultSubnet is the default subnet for the default network.
+ defaultSubnet types.IPNet
+
+ // isMachine describes whenever podman runs in a podman machine environment.
+ isMachine bool
+
+ // lock is a internal lock for critical operations
+ lock lockfile.Locker
+
+ // modTime is the timestamp when the config dir was modified
+ modTime time.Time
+
+ // networks is a map with loaded networks, the key is the network name
+ networks map[string]*types.Network
+}
+
+type InitConfig struct {
+ // NetworkConfigDir is directory where the network config files are stored.
+ NetworkConfigDir string
+
+ // NetavarkBinary is the path to the netavark binary.
+ NetavarkBinary string
+
+ // DefaultNetwork is the name for the default network.
+ DefaultNetwork string
+ // DefaultSubnet is the default subnet for the default network.
+ DefaultSubnet string
+
+ // IsMachine describes whenever podman runs in a podman machine environment.
+ IsMachine bool
+
+ // LockFile is the path to lock file.
+ LockFile string
+}
+
+// NewNetworkInterface creates the ContainerNetwork interface for the netavark backend.
+// Note: The networks are not loaded from disk until a method is called.
+func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) {
+ // TODO: consider using a shared memory lock
+ lock, err := lockfile.GetLockfile(conf.LockFile)
+ if err != nil {
+ return nil, err
+ }
+
+ defaultNetworkName := conf.DefaultNetwork
+ if defaultNetworkName == "" {
+ defaultNetworkName = types.DefaultNetworkName
+ }
+
+ defaultSubnet := conf.DefaultSubnet
+ if defaultSubnet == "" {
+ defaultSubnet = types.DefaultSubnet
+ }
+ defaultNet, err := types.ParseCIDR(defaultSubnet)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to parse default subnet")
+ }
+
+ n := &netavarkNetwork{
+ networkConfigDir: conf.NetworkConfigDir,
+ netavarkBinary: conf.NetavarkBinary,
+ defaultNetwork: defaultNetworkName,
+ defaultSubnet: defaultNet,
+ isMachine: conf.IsMachine,
+ lock: lock,
+ }
+
+ return n, nil
+}
+
+// Drivers will return the list of supported network drivers
+// for this interface.
+func (n *netavarkNetwork) Drivers() []string {
+ return []string{types.BridgeNetworkDriver}
+}
+
+func (n *netavarkNetwork) loadNetworks() error {
+ // check the mod time of the config dir
+ f, err := os.Stat(n.networkConfigDir)
+ if err != nil {
+ return err
+ }
+ modTime := f.ModTime()
+
+ // skip loading networks if they are already loaded and
+ // if the config dir was not modified since the last call
+ if n.networks != nil && modTime.Equal(n.modTime) {
+ return nil
+ }
+ // make sure the remove all networks before we reload them
+ n.networks = nil
+ n.modTime = modTime
+
+ files, err := ioutil.ReadDir(n.networkConfigDir)
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+
+ networks := make(map[string]*types.Network, len(files))
+ for _, f := range files {
+ if f.IsDir() {
+ continue
+ }
+ if filepath.Ext(f.Name()) != ".json" {
+ continue
+ }
+
+ path := filepath.Join(n.networkConfigDir, f.Name())
+ file, err := os.Open(path)
+ if err != nil {
+ // do not log ENOENT errors
+ if !errors.Is(err, os.ErrNotExist) {
+ logrus.Warnf("Error loading network config file %q: %v", path, err)
+ }
+ continue
+ }
+ network := new(types.Network)
+ err = json.NewDecoder(file).Decode(network)
+ if err != nil {
+ logrus.Warnf("Error reading network config file %q: %v", path, err)
+ continue
+ }
+
+ // check that the filename matches the network name
+ if network.Name+".json" != f.Name() {
+ logrus.Warnf("Network config name %q does not match file name %q, skipping", network.Name, f.Name())
+ continue
+ }
+
+ if !define.NameRegex.MatchString(network.Name) {
+ logrus.Warnf("Network config %q has invalid name: %q, skipping: %v", path, network.Name, define.RegexError)
+ continue
+ }
+
+ err = parseNetwork(network)
+ if err != nil {
+ logrus.Warnf("Network config %q could not be parsed, skipping: %v", path, err)
+ continue
+ }
+
+ logrus.Debugf("Successfully loaded network %s: %v", network.Name, network)
+ networks[network.Name] = network
+ }
+
+ // create the default network in memory if it did not exists on disk
+ if networks[n.defaultNetwork] == nil {
+ networkInfo, err := n.createDefaultNetwork()
+ if err != nil {
+ return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork)
+ }
+ networks[n.defaultNetwork] = networkInfo
+ }
+ logrus.Debugf("Successfully loaded %d networks", len(networks))
+ n.networks = networks
+ return nil
+}
+
+func parseNetwork(network *types.Network) error {
+ if network.Labels == nil {
+ network.Labels = map[string]string{}
+ }
+ if network.Options == nil {
+ network.Options = map[string]string{}
+ }
+ if network.IPAMOptions == nil {
+ network.IPAMOptions = map[string]string{}
+ }
+
+ if len(network.ID) != 64 {
+ return errors.Errorf("invalid network ID %q", network.ID)
+ }
+
+ return util.ValidateSubnets(network, nil)
+}
+
+func (n *netavarkNetwork) createDefaultNetwork() (*types.Network, error) {
+ net := types.Network{
+ Name: n.defaultNetwork,
+ NetworkInterface: defaultBridgeName + "0",
+ // Important do not change this ID
+ ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
+ Driver: types.BridgeNetworkDriver,
+ Subnets: []types.Subnet{
+ {Subnet: n.defaultSubnet},
+ },
+ }
+ return n.networkCreate(net, true)
+}
+
+// getNetwork will lookup a network by name or ID. It returns an
+// error when no network was found or when more than one network
+// with the given (partial) ID exists.
+// getNetwork will read from the networks map, therefore the caller
+// must ensure that n.lock is locked before using it.
+func (n *netavarkNetwork) getNetwork(nameOrID string) (*types.Network, error) {
+ // fast path check the map key, this will only work for names
+ if val, ok := n.networks[nameOrID]; ok {
+ return val, nil
+ }
+ // If there was no match we might got a full or partial ID.
+ var net *types.Network
+ for _, val := range n.networks {
+ // This should not happen because we already looked up the map by name but check anyway.
+ if val.Name == nameOrID {
+ return val, nil
+ }
+
+ if strings.HasPrefix(val.ID, nameOrID) {
+ if net != nil {
+ return nil, errors.Errorf("more than one result for network ID %s", nameOrID)
+ }
+ net = val
+ }
+ }
+ if net != nil {
+ return net, nil
+ }
+ return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID)
+}
+
+// Implement the NetUtil interface for easy code sharing with other network interfaces.
+
+// ForEach call the given function for each network
+func (n *netavarkNetwork) ForEach(run func(types.Network)) {
+ for _, val := range n.networks {
+ run(*val)
+ }
+}
+
+// Len return the number of networks
+func (n *netavarkNetwork) Len() int {
+ return len(n.networks)
+}
+
+// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number.
+func (n *netavarkNetwork) DefaultInterfaceName() string {
+ return defaultBridgeName
+}
+
+func (n *netavarkNetwork) Network(nameOrID string) (*types.Network, error) {
+ network, err := n.getNetwork(nameOrID)
+ if err != nil {
+ return nil, err
+ }
+ return network, nil
+}
diff --git a/libpod/network/netavark/run.go b/libpod/network/netavark/run.go
new file mode 100644
index 000000000..bd26e957e
--- /dev/null
+++ b/libpod/network/netavark/run.go
@@ -0,0 +1,90 @@
+// +build linux
+
+package netavark
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/containers/podman/v3/libpod/network/internal/util"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+type netavarkOptions struct {
+ types.NetworkOptions
+ Networks map[string]*types.Network `json:"network_info"`
+}
+
+// Setup will setup the container network namespace. It returns
+// a map of StatusBlocks, the key is the network name.
+func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions) (map[string]types.StatusBlock, error) {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ err = util.ValidateSetupOptions(n, namespacePath, options)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO IP address assignment
+
+ netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to convert net opts")
+ }
+
+ b, err := json.Marshal(&netavarkOpts)
+ if err != nil {
+ return nil, err
+ }
+ fmt.Println(string(b))
+
+ result := map[string]types.StatusBlock{}
+ err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, result)
+
+ if len(result) != len(options.Networks) {
+ logrus.Errorf("unexpected netavark result: %v", result)
+ return nil, fmt.Errorf("unexpected netavark result length, want (%d), got (%d) networks", len(options.Networks), len(result))
+ }
+
+ return result, err
+}
+
+// Teardown will teardown the container network namespace.
+func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownOptions) error {
+ n.lock.Lock()
+ defer n.lock.Unlock()
+ err := n.loadNetworks()
+ if err != nil {
+ return err
+ }
+
+ netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
+ if err != nil {
+ return errors.Wrap(err, "failed to convert net opts")
+ }
+
+ return execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil)
+}
+
+func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, error) {
+ netavarkOptions := netavarkOptions{
+ NetworkOptions: opts,
+ Networks: make(map[string]*types.Network, len(opts.Networks)),
+ }
+
+ for network := range opts.Networks {
+ net, err := n.getNetwork(network)
+ if err != nil {
+ return nil, err
+ }
+ netavarkOptions.Networks[network] = net
+ }
+ return &netavarkOptions, nil
+}
diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go
new file mode 100644
index 000000000..6c3b4d970
--- /dev/null
+++ b/libpod/network/netavark/run_test.go
@@ -0,0 +1,363 @@
+// +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 (
+ "bytes"
+ "fmt"
+ "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/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
+ logBuffer bytes.Buffer
+ 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() {
+ logrus.SetLevel(logrus.TraceLevel)
+ logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true})
+ // 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")
+ }
+ logBuffer = bytes.Buffer{}
+ logrus.SetOutput(&logBuffer)
+
+ netNSTest, err = netns.NewNS()
+ if err != nil {
+ Fail("Failed to create netns")
+ }
+
+ netNSContainer, err = netns.NewNS()
+ if err != nil {
+ Fail("Failed to create netns")
+ }
+ })
+
+ 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()
+
+ fmt.Println(logBuffer.String())
+ })
+
+ 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].Networks).To(HaveLen(1))
+ ip := res[defNet].Interfaces[intName].Networks[0].Subnet.IP
+ Expect(ip.String()).To(ContainSubstring("10.88.0."))
+ gw := res[defNet].Interfaces[intName].Networks[0].Gateway
+ 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(macAddress))
+ addrs, err := i.Addrs()
+ Expect(err).To(BeNil())
+ subnet := &net.IPNet{
+ IP: ip,
+ Mask: net.CIDRMask(16, 32),
+ }
+ Expect(addrs).To(ContainElements(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(subnet))
+
+ wg := &sync.WaitGroup{}
+ expected := stringid.GenerateNonCryptoID()
+ // now check ip connectivity
+ err = netNSContainer.Do(func(_ ns.NetNS) error {
+ 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())
+ })
+ })
+
+ 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].Networks).To(HaveLen(1))
+ Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.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].Networks).To(HaveLen(1))
+ containerIP := res[defNet].Interfaces[intName].Networks[0].Subnet.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())
+ })
+ })
+ }
+
+})
+
+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")
+ }
+}
diff --git a/libpod/network/netavark/testfiles/invalid/broken.json b/libpod/network/netavark/testfiles/invalid/broken.json
new file mode 100644
index 000000000..8968ddc73
--- /dev/null
+++ b/libpod/network/netavark/testfiles/invalid/broken.json
@@ -0,0 +1,16 @@
+{
+ "name": "bridge",
+ "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121",
+ "driver": "bridge",
+ "network_interface": "podman9",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1",
+ "lease_range": {
+ "start_ip": "10.89.8.20",
+ "end_ip": "10.89.8.50"
+ }
+ }
+ ],
diff --git a/libpod/network/netavark/testfiles/invalid/invalid name.json b/libpod/network/netavark/testfiles/invalid/invalid name.json
new file mode 100644
index 000000000..02b441279
--- /dev/null
+++ b/libpod/network/netavark/testfiles/invalid/invalid name.json
@@ -0,0 +1,19 @@
+{
+ "name": "invalid name",
+ "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889",
+ "driver": "bridge",
+ "network_interface": "podman9",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/invalid/invalid_gateway.json b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json
new file mode 100644
index 000000000..6e3a83156
--- /dev/null
+++ b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json
@@ -0,0 +1,19 @@
+{
+ "name": "invalid_gateway",
+ "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686",
+ "driver": "bridge",
+ "network_interface": "podman9",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.9.0/24",
+ "gateway": "10.89.100.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/invalid/name_missmatch.json b/libpod/network/netavark/testfiles/invalid/name_missmatch.json
new file mode 100644
index 000000000..a3142d8bb
--- /dev/null
+++ b/libpod/network/netavark/testfiles/invalid/name_missmatch.json
@@ -0,0 +1,19 @@
+{
+ "name": "name_miss",
+ "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f",
+ "driver": "bridge",
+ "network_interface": "podman8",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.7.0/24",
+ "gateway": "10.89.7.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": true,
+ "dns_enabled": false,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/invalid/wrongID.json b/libpod/network/netavark/testfiles/invalid/wrongID.json
new file mode 100644
index 000000000..7c1446306
--- /dev/null
+++ b/libpod/network/netavark/testfiles/invalid/wrongID.json
@@ -0,0 +1,19 @@
+{
+ "name": "wrongID",
+ "id": "someID",
+ "driver": "bridge",
+ "network_interface": "podman1",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.0.0/24",
+ "gateway": "10.89.0.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": false,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/bridge.json b/libpod/network/netavark/testfiles/valid/bridge.json
new file mode 100644
index 000000000..f4ec82188
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/bridge.json
@@ -0,0 +1,23 @@
+{
+ "name": "bridge",
+ "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121",
+ "driver": "bridge",
+ "network_interface": "podman9",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.8.0/24",
+ "gateway": "10.89.8.1",
+ "lease_range": {
+ "start_ip": "10.89.8.20",
+ "end_ip": "10.89.8.50"
+ }
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/dualstack.json b/libpod/network/netavark/testfiles/valid/dualstack.json
new file mode 100644
index 000000000..bb4168f3a
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/dualstack.json
@@ -0,0 +1,23 @@
+{
+ "name": "dualstack",
+ "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889",
+ "driver": "bridge",
+ "network_interface": "podman21",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "fd10:88:a::/64",
+ "gateway": "fd10:88:a::1"
+ },
+ {
+ "subnet": "10.89.19.0/24",
+ "gateway": "10.89.19.10"
+ }
+ ],
+ "ipv6_enabled": true,
+ "internal": false,
+ "dns_enabled": true,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/internal.json b/libpod/network/netavark/testfiles/valid/internal.json
new file mode 100644
index 000000000..3ccdd3889
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/internal.json
@@ -0,0 +1,18 @@
+{
+ "name": "internal",
+ "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f",
+ "driver": "bridge",
+ "network_interface": "podman8",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.7.0/24"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": true,
+ "dns_enabled": false,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/label.json b/libpod/network/netavark/testfiles/valid/label.json
new file mode 100644
index 000000000..c4ed637ec
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/label.json
@@ -0,0 +1,22 @@
+{
+ "name": "label",
+ "id": "1aca80e8b55c802f7b43740da2990e1b5735bbb323d93eb5ebda8395b04025e2",
+ "driver": "bridge",
+ "network_interface": "podman15",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.13.0/24",
+ "gateway": "10.89.13.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "labels": {
+ "mykey": "value"
+ },
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/mtu.json b/libpod/network/netavark/testfiles/valid/mtu.json
new file mode 100644
index 000000000..53fa4c9bc
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/mtu.json
@@ -0,0 +1,22 @@
+{
+ "name": "mtu",
+ "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686",
+ "driver": "bridge",
+ "network_interface": "podman13",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.11.0/24",
+ "gateway": "10.89.11.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "options": {
+ "mtu": "1500"
+ },
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/podman.json b/libpod/network/netavark/testfiles/valid/podman.json
new file mode 100644
index 000000000..19acddc83
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/podman.json
@@ -0,0 +1,19 @@
+{
+ "name": "podman",
+ "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9",
+ "driver": "bridge",
+ "network_interface": "podman0",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.88.0.0/16",
+ "gateway": "10.88.0.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": false,
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}
diff --git a/libpod/network/netavark/testfiles/valid/vlan.json b/libpod/network/netavark/testfiles/valid/vlan.json
new file mode 100644
index 000000000..30c88ec49
--- /dev/null
+++ b/libpod/network/netavark/testfiles/valid/vlan.json
@@ -0,0 +1,22 @@
+{
+ "name": "vlan",
+ "id": "c3b258168c41c0bce97616716bef315eeed33eb1142904bfe7f32eb392c7cf80",
+ "driver": "bridge",
+ "network_interface": "podman14",
+ "created": "2021-10-06T18:50:54.25770461+02:00",
+ "subnets": [
+ {
+ "subnet": "10.89.12.0/24",
+ "gateway": "10.89.12.1"
+ }
+ ],
+ "ipv6_enabled": false,
+ "internal": false,
+ "dns_enabled": true,
+ "options": {
+ "vlan": "5"
+ },
+ "ipam_options": {
+ "driver": "host-local"
+ }
+}