summaryrefslogtreecommitdiff
path: root/libpod/network/netavark
diff options
context:
space:
mode:
authorOpenShift Merge Robot <openshift-merge-robot@users.noreply.github.com>2021-11-11 19:12:43 +0100
committerGitHub <noreply@github.com>2021-11-11 19:12:43 +0100
commit375ff223f430301edf25ef5a5f03a1ae1e029bef (patch)
tree96993f98fa27d39a93589a93179e7c914746b876 /libpod/network/netavark
parentd5b411c484f9e45c71ad3552b796bd3343ec7a0c (diff)
parent3af19917a1666fc3f29d98f7f0b937eb488f706c (diff)
downloadpodman-375ff223f430301edf25ef5a5f03a1ae1e029bef.tar.gz
podman-375ff223f430301edf25ef5a5f03a1ae1e029bef.tar.bz2
podman-375ff223f430301edf25ef5a5f03a1ae1e029bef.zip
Merge pull request #12131 from Luap99/netavark-interface
Netavark interface
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.go128
-rw-r--r--libpod/network/netavark/ipam.go368
-rw-r--r--libpod/network/netavark/ipam_test.go433
-rw-r--r--libpod/network/netavark/netavark_suite_test.go75
-rw-r--r--libpod/network/netavark/network.go305
-rw-r--r--libpod/network/netavark/run.go119
-rw-r--r--libpod/network/netavark/run_test.go693
-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
22 files changed, 3700 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..d6458eeb4
--- /dev/null
+++ b/libpod/network/netavark/exec.go
@@ -0,0 +1,128 @@
+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,
+ }
+}
+
+// getRustLogEnv returns the RUST_LOG env var based on the current logrus level
+func getRustLogEnv() string {
+ level := logrus.GetLevel().String()
+ // rust env_log uses warn instead of warning
+ if level == "warning" {
+ level = "warn"
+ }
+ // the rust netlink library is very verbose
+ // make sure to only log netavark logs
+ return "RUST_LOG=netavark=" + level
+}
+
+// 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(), getRustLogEnv())
+ // 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/ipam.go b/libpod/network/netavark/ipam.go
new file mode 100644
index 000000000..db46ee652
--- /dev/null
+++ b/libpod/network/netavark/ipam.go
@@ -0,0 +1,368 @@
+package netavark
+
+import (
+ "encoding/json"
+ "fmt"
+ "net"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ "github.com/pkg/errors"
+ "go.etcd.io/bbolt"
+)
+
+// IPAM boltdb structure
+// Each network has their own bucket with the network name as bucket key.
+// Inside the network bucket there is an ID bucket which maps the container ID (key)
+// to a json array of ip addresses (value).
+// The network bucket also has a bucket for each subnet, the subnet is used as key.
+// Inside the subnet bucket an ip is used as key and the container ID as value.
+
+const (
+ idBucket = "ids"
+ // lastIP this is used as key to store the last allocated ip
+ // note that this string should not be 4 or 16 byte long
+ lastIP = "lastIP"
+)
+
+var (
+ idBucketKey = []byte(idBucket)
+ lastIPKey = []byte(lastIP)
+)
+
+type ipamError struct {
+ msg string
+ cause error
+}
+
+func (e *ipamError) Error() string {
+ msg := "IPAM error"
+ if e.msg != "" {
+ msg += ": " + e.msg
+ }
+ if e.cause != nil {
+ msg += ": " + e.cause.Error()
+ }
+ return msg
+}
+
+func newIPAMError(cause error, msg string, args ...interface{}) *ipamError {
+ return &ipamError{
+ msg: fmt.Sprintf(msg, args...),
+ cause: cause,
+ }
+}
+
+// openDB will open the ipam database
+// Note that the caller has to Close it.
+func (n *netavarkNetwork) openDB() (*bbolt.DB, error) {
+ db, err := bbolt.Open(n.ipamDBPath, 0600, nil)
+ if err != nil {
+ return nil, newIPAMError(err, "failed to open database %s", n.ipamDBPath)
+ }
+ return db, nil
+}
+
+// allocIPs will allocate ips for the the container. It will change the
+// NetworkOptions in place. When static ips are given it will validate
+// that these are free to use and will allocate them to the container.
+func (n *netavarkNetwork) allocIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.Update(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+
+ // create/get network bucket
+ netBkt, err := tx.CreateBucketIfNotExists([]byte(netName))
+ if err != nil {
+ return newIPAMError(err, "failed to create/get network bucket for network %s", netName)
+ }
+
+ // requestIPs is the list of ips which should be used for this container
+ requestIPs := make([]net.IP, 0, len(network.Subnets))
+
+ for _, subnet := range network.Subnets {
+ subnetBkt, err := netBkt.CreateBucketIfNotExists([]byte(subnet.Subnet.String()))
+ if err != nil {
+ return newIPAMError(err, "failed to create/get subnet bucket for network %s", netName)
+ }
+
+ // search for a static ip which matches the current subnet
+ // in this case the user wants this one and we should not assign a free one
+ var ip net.IP
+ for _, staticIP := range netOpts.StaticIPs {
+ if subnet.Subnet.Contains(staticIP) {
+ ip = staticIP
+ break
+ }
+ }
+
+ // when static ip is requested for this network
+ if ip != nil {
+ // convert to 4 byte ipv4 if needed
+ util.NormalizeIP(&ip)
+ id := subnetBkt.Get(ip)
+ if id != nil {
+ return newIPAMError(nil, "requested ip address %s is already allocated to container ID %s", ip.String(), string(id))
+ }
+ } else {
+ ip, err = getFreeIPFromBucket(subnetBkt, subnet)
+ if err != nil {
+ return err
+ }
+ err = subnetBkt.Put(lastIPKey, ip)
+ if err != nil {
+ return newIPAMError(err, "failed to store last ip in database")
+ }
+ }
+
+ err = subnetBkt.Put(ip, []byte(opts.ContainerID))
+ if err != nil {
+ return newIPAMError(err, "failed to store ip in database")
+ }
+
+ requestIPs = append(requestIPs, ip)
+ }
+
+ idsBucket, err := netBkt.CreateBucketIfNotExists(idBucketKey)
+ if err != nil {
+ return newIPAMError(err, "failed to create/get id bucket for network %s", netName)
+ }
+
+ ipsBytes, err := json.Marshal(requestIPs)
+ if err != nil {
+ return newIPAMError(err, "failed to marshal ips")
+ }
+
+ err = idsBucket.Put([]byte(opts.ContainerID), ipsBytes)
+ if err != nil {
+ return newIPAMError(err, "failed to store ips in database")
+ }
+
+ netOpts.StaticIPs = requestIPs
+ opts.Networks[netName] = netOpts
+ }
+ return nil
+ })
+ return err
+}
+
+func getFreeIPFromBucket(bucket *bbolt.Bucket, subnet types.Subnet) (net.IP, error) {
+ var rangeStart net.IP
+ var rangeEnd net.IP
+ if subnet.LeaseRange != nil {
+ rangeStart = subnet.LeaseRange.StartIP
+ rangeEnd = subnet.LeaseRange.EndIP
+ }
+
+ if rangeStart == nil {
+ // let start with the first ip in subnet
+ rangeStart = util.NextIP(subnet.Subnet.IP)
+ }
+
+ if rangeEnd == nil {
+ lastIP, err := util.LastIPInSubnet(&subnet.Subnet.IPNet)
+ // this error should never happen but lets check anyways to prevent panics
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get lastIP")
+ }
+ // ipv4 uses the last ip in a subnet for broadcast so we cannot use it
+ if util.IsIPv4(lastIP) {
+ lastIP = util.PrevIP(lastIP)
+ }
+ rangeEnd = lastIP
+ }
+
+ lastIPByte := bucket.Get(lastIPKey)
+ curIP := net.IP(lastIPByte)
+ if curIP == nil {
+ curIP = rangeStart
+ } else {
+ curIP = util.NextIP(curIP)
+ }
+
+ // store the start ip to make sure we know when we looped over all available ips
+ startIP := curIP
+
+ for {
+ // skip the gateway
+ if subnet.Gateway != nil {
+ if util.Cmp(curIP, subnet.Gateway) == 0 {
+ curIP = util.NextIP(curIP)
+ continue
+ }
+ }
+
+ // if we are at the end we need to jump back to the start ip
+ if util.Cmp(curIP, rangeEnd) > 0 {
+ if util.Cmp(rangeStart, startIP) == 0 {
+ return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String())
+ }
+ curIP = rangeStart
+ continue
+ }
+
+ // check if ip is already used by another container
+ // if not return it
+ if bucket.Get(curIP) == nil {
+ return curIP, nil
+ }
+
+ curIP = util.NextIP(curIP)
+
+ if util.Cmp(curIP, startIP) == 0 {
+ return nil, newIPAMError(nil, "failed to find free IP in range: %s - %s", rangeStart.String(), rangeEnd.String())
+ }
+ }
+}
+
+// getAssignedIPs will read the ipam database and will fill in the used ips for this container.
+// It will change the NetworkOptions in place.
+func (n *netavarkNetwork) getAssignedIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.View(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+ // get network bucket
+ netBkt := tx.Bucket([]byte(netName))
+ if netBkt == nil {
+ return newIPAMError(nil, "failed to get network bucket for network %s", netName)
+ }
+
+ idBkt := netBkt.Bucket(idBucketKey)
+ if idBkt == nil {
+ return newIPAMError(nil, "failed to get id bucket for network %s", netName)
+ }
+
+ ipJSON := idBkt.Get([]byte(opts.ContainerID))
+ if ipJSON == nil {
+ return newIPAMError(nil, "failed to get ips for container ID %s on network %s", opts.ContainerID, netName)
+ }
+
+ // assignedIPs is the list of ips which should be used for this container
+ assignedIPs := make([]net.IP, 0, len(network.Subnets))
+
+ err = json.Unmarshal(ipJSON, &assignedIPs)
+ if err != nil {
+ return newIPAMError(err, "failed to unmarshal ips from database")
+ }
+
+ for i := range assignedIPs {
+ util.NormalizeIP(&assignedIPs[i])
+ }
+
+ netOpts.StaticIPs = assignedIPs
+ opts.Networks[netName] = netOpts
+ }
+ return nil
+ })
+ return err
+}
+
+// deallocIPs will release the ips in the network options from the DB so that
+// they can be reused by other containers. It expects that the network options
+// are already filled with the correct ips. Use getAssignedIPs() for this.
+func (n *netavarkNetwork) deallocIPs(opts *types.NetworkOptions) error {
+ db, err := n.openDB()
+ if err != nil {
+ return err
+ }
+ defer db.Close()
+
+ err = db.Update(func(tx *bbolt.Tx) error {
+ for netName, netOpts := range opts.Networks {
+ network := n.networks[netName]
+ if network == nil {
+ return newIPAMError(nil, "could not find network %q", netName)
+ }
+
+ // check if we have to alloc ips
+ if !requiresIPAMAlloc(network) {
+ continue
+ }
+ // get network bucket
+ netBkt := tx.Bucket([]byte(netName))
+ if netBkt == nil {
+ return newIPAMError(nil, "failed to get network bucket for network %s", netName)
+ }
+
+ for _, subnet := range network.Subnets {
+ subnetBkt := netBkt.Bucket([]byte(subnet.Subnet.String()))
+ if subnetBkt == nil {
+ return newIPAMError(nil, "failed to get subnet bucket for network %s", netName)
+ }
+
+ // search for a static ip which matches the current subnet
+ // in this case the user wants this one and we should not assign a free one
+ var ip net.IP
+ for _, staticIP := range netOpts.StaticIPs {
+ if subnet.Subnet.Contains(staticIP) {
+ ip = staticIP
+ break
+ }
+ }
+ if ip == nil {
+ return newIPAMError(nil, "failed to find ip for subnet %s on network %s", subnet.Subnet.String(), netName)
+ }
+ util.NormalizeIP(&ip)
+
+ err = subnetBkt.Delete(ip)
+ if err != nil {
+ return newIPAMError(err, "failed to remove ip %s from subnet bucket for network %s", ip.String(), netName)
+ }
+ }
+
+ idBkt := netBkt.Bucket(idBucketKey)
+ if idBkt == nil {
+ return newIPAMError(nil, "failed to get id bucket for network %s", netName)
+ }
+
+ err = idBkt.Delete([]byte(opts.ContainerID))
+ if err != nil {
+ return newIPAMError(err, "failed to remove allocated ips for container ID %s on network %s", opts.ContainerID, netName)
+ }
+ }
+ return nil
+ })
+ return err
+}
+
+// requiresIPAMAlloc return true when we have to allocate ips for this network
+// it checks the ipam driver and if subnets are set
+func requiresIPAMAlloc(network *types.Network) bool {
+ // only do host allocation when driver is set to HostLocalIPAMDriver or unset
+ switch network.IPAMOptions["driver"] {
+ case "", types.HostLocalIPAMDriver:
+ default:
+ return false
+ }
+
+ // no subnets == no ips to assign
+ return len(network.Subnets) > 0
+}
diff --git a/libpod/network/netavark/ipam_test.go b/libpod/network/netavark/ipam_test.go
new file mode 100644
index 000000000..4b3947501
--- /dev/null
+++ b/libpod/network/netavark/ipam_test.go
@@ -0,0 +1,433 @@
+package netavark
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "os"
+ "path/filepath"
+
+ "github.com/containers/podman/v3/libpod/network/types"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ "github.com/sirupsen/logrus"
+)
+
+var _ = Describe("IPAM", func() {
+ var (
+ networkInterface *netavarkNetwork
+ 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() {
+ libpodNet, err := NewNetworkInterface(InitConfig{
+ NetworkConfigDir: networkConfDir,
+ IPAMDBPath: filepath.Join(networkConfDir, "ipam.db"),
+ LockFile: filepath.Join(networkConfDir, "netavark.lock"),
+ })
+ if err != nil {
+ Fail("Failed to create NewCNINetworkInterface")
+ }
+
+ networkInterface = libpodNet.(*netavarkNetwork)
+ // run network list to force a network load
+ networkInterface.NetworkList()
+ })
+
+ AfterEach(func() {
+ os.RemoveAll(networkConfDir)
+ })
+
+ It("simple ipam alloc", func() {
+ netName := types.DefaultNetworkName
+ for i := 2; i < 100; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.88.0.%d", i)).To4()))
+ }
+ })
+
+ It("ipam try to alloc same ip", func() {
+ netName := types.DefaultNetworkName
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.88.0.2").To4()))
+
+ opts = &types.NetworkOptions{
+ ContainerID: "otherID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {StaticIPs: []net.IP{net.ParseIP("10.88.0.2")}},
+ },
+ }
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID"))
+ })
+
+ It("ipam try to alloc more ips as in range", func() {
+ s, _ := types.ParseCIDR("10.0.0.1/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ LeaseRange: &types.LeaseRange{
+ StartIP: net.ParseIP("10.0.0.10"),
+ EndIP: net.ParseIP("10.0.0.20"),
+ },
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 10; i < 21; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: fmt.Sprintf("someContainerID-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+ }
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID-22",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ // now this should fail because all free ips are already assigned
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.10 - 10.0.0.20"))
+ })
+
+ It("ipam basic setup", func() {
+ netName := types.DefaultNetworkName
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ expectedIP := net.ParseIP("10.88.0.2").To4()
+
+ err := networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: requested ip address 10.88.0.2 is already allocated to container ID someContainerID"))
+
+ // dealloc the ip
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(expectedIP))
+ })
+
+ It("ipam dual stack", func() {
+ s1, _ := types.ParseCIDR("10.0.0.0/26")
+ s2, _ := types.ParseCIDR("fd80::/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s1,
+ },
+ {
+ Subnet: s2,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ // try to alloc the same again
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(2))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks[netName].StaticIPs[1]).To(Equal(net.ParseIP("fd80::2")))
+ })
+
+ It("ipam with two networks", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/24")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName1 := network.Name
+
+ s, _ = types.ParseCIDR("10.0.1.0/24")
+ network, err = networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName2 := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName1: {},
+ netName2: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+
+ // remove static ips from opts
+ netOpts := opts.Networks[netName1]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName1] = netOpts
+ netOpts = opts.Networks[netName2]
+ netOpts.StaticIPs = nil
+ opts.Networks[netName2] = netOpts
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+
+ // try to alloc the same again
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName1))
+ Expect(opts.Networks[netName1].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName1].StaticIPs[0]).To(Equal(net.ParseIP("10.0.0.2").To4()))
+ Expect(opts.Networks).To(HaveKey(netName2))
+ Expect(opts.Networks[netName2].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName2].StaticIPs[0]).To(Equal(net.ParseIP("10.0.1.2").To4()))
+ })
+
+ It("ipam alloc more ips as in subnet", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/26")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 2; i < 64; i++ {
+ opts := &types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(opts)
+ if i < 63 {
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+ } else {
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.62"))
+ }
+ }
+ })
+
+ It("ipam alloc -> dealloc -> alloc", func() {
+ s, _ := types.ParseCIDR("10.0.0.0/27")
+ network, err := networkInterface.NetworkCreate(types.Network{
+ Subnets: []types.Subnet{
+ {
+ Subnet: s,
+ },
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ for i := 2; i < 10; i++ {
+ opts := types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(&opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", i)).To4()))
+
+ err = networkInterface.deallocIPs(&opts)
+ Expect(err).ToNot(HaveOccurred())
+ }
+
+ for i := 0; i < 30; i++ {
+ opts := types.NetworkOptions{
+ ContainerID: fmt.Sprintf("id-%d", i),
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+ err = networkInterface.allocIPs(&opts)
+ if i < 29 {
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(1))
+ // The (i+8)%29+2 part looks cryptic but it is actually simple, we already have 8 ips allocated above
+ // so we expect the 8 available ip. We have 29 assignable ip addresses in this subnet because "i"+8 can
+ // be greater than 30 we have to modulo by 29 to go back to the beginning. Also the first free ip is
+ // network address + 2, so we have to add 2 to the result
+ Expect(opts.Networks[netName].StaticIPs[0]).To(Equal(net.ParseIP(fmt.Sprintf("10.0.0.%d", (i+8)%29+2)).To4()))
+ } else {
+ Expect(err).To(HaveOccurred())
+ Expect(err.Error()).To(Equal("IPAM error: failed to find free IP in range: 10.0.0.1 - 10.0.0.30"))
+ }
+ }
+ })
+
+ It("ipam with dhcp driver should not set ips", func() {
+ network, err := networkInterface.NetworkCreate(types.Network{
+ IPAMOptions: map[string]string{
+ "driver": types.DHCPIPAMDriver,
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+
+ netName := network.Name
+
+ opts := &types.NetworkOptions{
+ ContainerID: "someContainerID",
+ Networks: map[string]types.PerNetworkOptions{
+ netName: {},
+ },
+ }
+
+ err = networkInterface.allocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0))
+
+ err = networkInterface.getAssignedIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ Expect(opts.Networks).To(HaveKey(netName))
+ Expect(opts.Networks[netName].StaticIPs).To(HaveLen(0))
+
+ // dealloc the ip
+ err = networkInterface.deallocIPs(opts)
+ Expect(err).ToNot(HaveOccurred())
+ })
+
+})
diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go
new file mode 100644
index 000000000..6063a54e3
--- /dev/null
+++ b/libpod/network/netavark/netavark_suite_test.go
@@ -0,0 +1,75 @@
+// +build linux
+
+package netavark_test
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+
+ "github.com/containers/podman/v3/libpod/network/netavark"
+ "github.com/containers/podman/v3/libpod/network/types"
+ "github.com/containers/podman/v3/libpod/network/util"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ gomegaTypes "github.com/onsi/gomega/types"
+)
+
+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,
+ IPAMDBPath: filepath.Join(confDir, "ipam.db"),
+ LockFile: filepath.Join(confDir, "netavark.lock"),
+ })
+}
+
+// EqualSubnet is a custom GomegaMatcher to match a subnet
+// This makes sure to not use the 16 bytes ip representation.
+func EqualSubnet(subnet *net.IPNet) gomegaTypes.GomegaMatcher {
+ return &equalSubnetMatcher{
+ expected: subnet,
+ }
+}
+
+type equalSubnetMatcher struct {
+ expected *net.IPNet
+}
+
+func (m *equalSubnetMatcher) Match(actual interface{}) (bool, error) {
+ util.NormalizeIP(&m.expected.IP)
+
+ subnet, ok := actual.(*net.IPNet)
+ if !ok {
+ return false, fmt.Errorf("EqualSubnet expects a *net.IPNet")
+ }
+ util.NormalizeIP(&subnet.IP)
+
+ return reflect.DeepEqual(subnet, m.expected), nil
+}
+
+func (m *equalSubnetMatcher) FailureMessage(actual interface{}) string {
+ return fmt.Sprintf("Expected subnet %#v to equal subnet %#v", actual, m.expected)
+}
+
+func (m *equalSubnetMatcher) NegatedFailureMessage(actual interface{}) string {
+ return fmt.Sprintf("Expected subnet %#v not to equal subnet %#v", actual, m.expected)
+}
diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go
new file mode 100644
index 000000000..cc6fb423c
--- /dev/null
+++ b/libpod/network/netavark/network.go
@@ -0,0 +1,305 @@
+// +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"
+ pkgutil "github.com/containers/podman/v3/pkg/util"
+ "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
+
+ // ipamDBPath is the path to the ip allocation bolt db
+ ipamDBPath string
+
+ // 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
+
+ // IPAMDBPath is the path to the ipam database. This should be on a tmpfs.
+ // If empty defaults to XDG_RUNTIME_DIR/netavark/ipam.db or /run/netavark/ipam.db as root.
+ IPAMDBPath 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")
+ }
+
+ ipamdbPath := conf.IPAMDBPath
+ if ipamdbPath == "" {
+ runDir, err := pkgutil.GetRuntimeDir()
+ if err != nil {
+ return nil, err
+ }
+ // as root runtimeDir is empty so use /run
+ if runDir == "" {
+ runDir = "/run"
+ }
+ ipamdbPath = filepath.Join(runDir, "netavark")
+ if err := os.MkdirAll(ipamdbPath, 0700); err != nil {
+ return nil, errors.Wrap(err, "failed to create ipam db path")
+ }
+ ipamdbPath = filepath.Join(ipamdbPath, "ipam.db")
+ }
+
+ if err := os.MkdirAll(conf.NetworkConfigDir, 0755); err != nil {
+ return nil, err
+ }
+
+ n := &netavarkNetwork{
+ networkConfigDir: conf.NetworkConfigDir,
+ netavarkBinary: conf.NetavarkBinary,
+ ipamDBPath: ipamdbPath,
+ 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..2f839151e
--- /dev/null
+++ b/libpod/network/netavark/run.go
@@ -0,0 +1,119 @@
+// +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
+ }
+
+ // allocate IPs in the IPAM db
+ err = n.allocIPs(&options.NetworkOptions)
+ if err != nil {
+ return nil, err
+ }
+
+ netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to convert net opts")
+ }
+
+ // trace output to get the json
+ if logrus.IsLevelEnabled(logrus.TraceLevel) {
+ b, err := json.Marshal(&netavarkOpts)
+ if err != nil {
+ return nil, err
+ }
+ // show the full netavark command so we can easily reproduce errors from the cli
+ logrus.Tracef("netavark command: printf '%s' | %s setup %s", string(b), n.netavarkBinary, namespacePath)
+ }
+
+ 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
+ }
+
+ // get IPs from the IPAM db
+ err = n.getAssignedIPs(&options.NetworkOptions)
+ if err != nil {
+ // when there is an error getting the ips we should still continue
+ // to call teardown for netavark to prevent leaking network interfaces
+ logrus.Error(err)
+ }
+
+ netavarkOpts, err := n.convertNetOpts(options.NetworkOptions)
+ if err != nil {
+ return errors.Wrap(err, "failed to convert net opts")
+ }
+
+ retErr := execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil)
+
+ // when netavark returned an error we still free the used ips
+ // otherwise we could end up in a state where block the ips forever
+ err = n.deallocIPs(&netavarkOpts.NetworkOptions)
+ if err != nil {
+ if retErr != nil {
+ logrus.Error(err)
+ } else {
+ retErr = err
+ }
+ }
+
+ return retErr
+}
+
+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..3279203cc
--- /dev/null
+++ b/libpod/network/netavark/run_test.go
@@ -0,0 +1,693 @@
+// +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")
+ }
+ })
+
+ 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()
+ })
+
+ 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
+ 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].Networks).To(HaveLen(1))
+ ip1 := res[defNet].Interfaces[intName].Networks[0].Subnet.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].Networks).To(HaveLen(1))
+ ip2 := res[defNet].Interfaces[intName].Networks[0].Subnet.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].Networks).To(HaveLen(2))
+ ip1 := res[netName].Interfaces[intName].Networks[0].Subnet.IP
+ Expect(ip1.String()).To(ContainSubstring("10.0.0."))
+ gw1 := res[netName].Interfaces[intName].Networks[0].Gateway
+ Expect(gw1.String()).To(Equal("10.0.0.1"))
+ ip2 := res[netName].Interfaces[intName].Networks[1].Subnet.IP
+ Expect(ip2.String()).To(ContainSubstring("fd10:88:a::"))
+ gw2 := res[netName].Interfaces[intName].Networks[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].Networks).To(HaveLen(1))
+ ip1 := res[netName1].Interfaces[intName1].Networks[0].Subnet.IP
+ Expect(ip1.String()).To(ContainSubstring("10.0.0."))
+ gw1 := res[netName1].Interfaces[intName1].Networks[0].Gateway
+ Expect(gw1.String()).To(Equal("10.0.0.1"))
+ ip2 := res[netName2].Interfaces[intName2].Networks[0].Subnet.IP
+ Expect(ip2.String()).To(ContainSubstring("10.1.0."))
+ gw2 := res[netName2].Interfaces[intName2].Networks[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].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())
+ })
+ })
+ }
+
+ 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].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))
+
+ 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")
+ }
+}
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"
+ }
+}