From 12c62b92ff2f63cb34dcb9c0555b96983e6aad94 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 13 Oct 2021 21:52:55 +0200 Subject: Make networking code reusable To prevent code duplication when creating new network backends move reusable code into a separate internal package. This allows all network backends to use the same code as long as they implement the new NetUtil interface. Signed-off-by: Paul Holzinger --- libpod/network/cni/config.go | 152 ++--------------------------- libpod/network/cni/network.go | 116 +++------------------- libpod/network/cni/run.go | 39 +------- libpod/network/cni/run_test.go | 2 +- libpod/network/internal/util/bridge.go | 67 +++++++++++++ libpod/network/internal/util/create.go | 48 +++++++++ libpod/network/internal/util/interface.go | 19 ++++ libpod/network/internal/util/interfaces.go | 34 +++++++ libpod/network/internal/util/ip.go | 70 +++++++++++++ libpod/network/internal/util/ip_test.go | 63 ++++++++++++ libpod/network/internal/util/util.go | 123 +++++++++++++++++++++++ libpod/network/internal/util/validate.go | 97 ++++++++++++++++++ libpod/network/util/interfaces.go | 34 ------- libpod/network/util/ip.go | 69 +------------ libpod/network/util/ip_test.go | 52 ---------- 15 files changed, 550 insertions(+), 435 deletions(-) create mode 100644 libpod/network/internal/util/bridge.go create mode 100644 libpod/network/internal/util/create.go create mode 100644 libpod/network/internal/util/interface.go create mode 100644 libpod/network/internal/util/interfaces.go create mode 100644 libpod/network/internal/util/ip.go create mode 100644 libpod/network/internal/util/ip_test.go create mode 100644 libpod/network/internal/util/util.go create mode 100644 libpod/network/internal/util/validate.go delete mode 100644 libpod/network/util/interfaces.go diff --git a/libpod/network/cni/config.go b/libpod/network/cni/config.go index 3df155637..a63574f25 100644 --- a/libpod/network/cni/config.go +++ b/libpod/network/cni/config.go @@ -7,6 +7,7 @@ import ( "os" "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/podman/v3/libpod/network/util" pkgutil "github.com/containers/podman/v3/pkg/util" @@ -41,38 +42,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* newNetwork.Driver = types.DefaultNetworkDriver } - // 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") - } - - if newNetwork.Labels == nil { - newNetwork.Labels = map[string]string{} - } - if newNetwork.Options == nil { - newNetwork.Options = map[string]string{} - } - if newNetwork.IPAMOptions == nil { - newNetwork.IPAMOptions = map[string]string{} - } - - var name string - var err error - // validate the name when given - if newNetwork.Name != "" { - if !define.NameRegex.MatchString(newNetwork.Name) { - return nil, errors.Wrapf(define.RegexError, "network name %s invalid", newNetwork.Name) - } - if _, ok := n.networks[newNetwork.Name]; ok { - return nil, errors.Wrapf(define.ErrNetworkExists, "network name %s already used", newNetwork.Name) - } - } else { - name, err = n.getFreeDeviceName() - if err != nil { - return nil, err - } - newNetwork.Name = name + 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. @@ -84,7 +56,7 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* // fail because it thinks the network is used on the host. var usedNetworks []*net.IPNet if !defaultNet { - usedNetworks, err = n.getUsedSubnets() + usedNetworks, err = internalutil.GetUsedSubnets(n) if err != nil { return nil, err } @@ -92,11 +64,7 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* switch newNetwork.Driver { case types.BridgeNetworkDriver: - // if the name was created with getFreeDeviceName set the interface to it as well - if name != "" && newNetwork.NetworkInterface == "" { - newNetwork.NetworkInterface = name - } - err = n.createBridge(&newNetwork, usedNetworks) + err = internalutil.CreateBridge(n, &newNetwork, usedNetworks) if err != nil { return nil, err } @@ -110,7 +78,7 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* } for i := range newNetwork.Subnets { - err := validateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks) + err := internalutil.ValidateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks) if err != nil { return nil, err } @@ -223,7 +191,7 @@ func createIPMACVLAN(network *types.Network) error { return errors.New("internal is not supported with macvlan") } if network.NetworkInterface != "" { - interfaceNames, err := util.GetLiveNetworkNames() + interfaceNames, err := internalutil.GetLiveNetworkNames() if err != nil { return err } @@ -238,107 +206,3 @@ func createIPMACVLAN(network *types.Network) error { } return nil } - -func (n *cniNetwork) createBridge(network *types.Network, usedNetworks []*net.IPNet) error { - if network.NetworkInterface != "" { - bridges := n.getBridgeInterfaceNames() - if pkgutil.StringInSlice(network.NetworkInterface, bridges) { - return errors.Errorf("bridge name %s already in use", network.NetworkInterface) - } - if !define.NameRegex.MatchString(network.NetworkInterface) { - return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface) - } - } else { - var err error - network.NetworkInterface, err = n.getFreeDeviceName() - if err != nil { - return err - } - } - - if len(network.Subnets) == 0 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - // ipv6 enabled means dual stack, check if we already have - // a ipv4 or ipv6 subnet and add one if not. - if network.IPv6Enabled { - ipv4 := false - ipv6 := false - for _, subnet := range network.Subnets { - if util.IsIPv6(subnet.Subnet.IP) { - ipv6 = true - } - if util.IsIPv4(subnet.Subnet.IP) { - ipv4 = true - } - } - if !ipv4 { - freeSubnet, err := n.getFreeIPv4NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - if !ipv6 { - freeSubnet, err := n.getFreeIPv6NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - } - network.IPAMOptions["driver"] = types.HostLocalIPAMDriver - return nil -} - -// validateSubnet will validate a given Subnet. It checks if the -// given gateway and lease range are part of this subnet. If the -// gateway is empty and addGateway is true it will get the first -// available ip in the subnet assigned. -func validateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) error { - if s == nil { - return errors.New("subnet is nil") - } - if s.Subnet.IP == nil { - return errors.New("subnet ip is nil") - } - - // Reparse to ensure subnet is valid. - // Do not use types.ParseCIDR() because we want the ip to be - // the network address and not a random ip in the subnet. - _, net, err := net.ParseCIDR(s.Subnet.String()) - if err != nil { - return errors.Wrap(err, "subnet invalid") - } - - // check that the new subnet does not conflict with existing ones - if util.NetworkIntersectsWithNetworks(net, usedNetworks) { - return errors.Errorf("subnet %s is already used on the host or by another config", net.String()) - } - - s.Subnet = types.IPNet{IPNet: *net} - if s.Gateway != nil { - if !s.Subnet.Contains(s.Gateway) { - return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) - } - } else if addGateway { - ip, err := util.FirstIPInSubnet(net) - if err != nil { - return err - } - s.Gateway = ip - } - if s.LeaseRange != nil { - if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) { - return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) - } - if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) { - return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) - } - } - return nil -} diff --git a/libpod/network/cni/network.go b/libpod/network/cni/network.go index a37a84373..3e9cdaa47 100644 --- a/libpod/network/cni/network.go +++ b/libpod/network/cni/network.go @@ -6,8 +6,6 @@ import ( "context" "crypto/sha256" "encoding/hex" - "fmt" - "net" "os" "strings" "time" @@ -15,8 +13,6 @@ import ( "github.com/containernetworking/cni/libcni" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/network/types" - "github.com/containers/podman/v3/libpod/network/util" - pkgutil "github.com/containers/podman/v3/pkg/util" "github.com/containers/storage/pkg/lockfile" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -242,111 +238,29 @@ func getNetworkIDFromName(name string) string { return hex.EncodeToString(hash[:]) } -// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet -func (n *cniNetwork) getFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { - // the default podman network is 10.88.0.0/16 - // start locking for free /24 networks - network := &net.IPNet{ - IP: net.IP{10, 89, 0, 0}, - Mask: net.IPMask{255, 255, 255, 0}, - } - - // TODO: make sure to not use public subnets - for { - if intersectsConfig := util.NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig { - logrus.Debugf("found free ipv4 network subnet %s", network.String()) - return &types.Subnet{ - Subnet: types.IPNet{IPNet: *network}, - }, nil - } - var err error - network, err = util.NextSubnet(network) - if err != nil { - return nil, err - } - } -} +// Implement the NetUtil interface for easy code sharing with other network interfaces. -// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet -func (n *cniNetwork) getFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { - // FIXME: Is 10000 fine as limit? We should prevent an endless loop. - for i := 0; i < 10000; i++ { - // RFC4193: Choose the ipv6 subnet random and NOT sequentially. - network, err := util.GetRandomIPv6Subnet() - if err != nil { - return nil, err - } - if intersectsConfig := util.NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig { - logrus.Debugf("found free ipv6 network subnet %s", network.String()) - return &types.Subnet{ - Subnet: types.IPNet{IPNet: network}, - }, nil - } - } - return nil, errors.New("failed to get random ipv6 subnet") -} - -// getUsedSubnets returns a list of all used subnets by network -// configs and interfaces on the host. -func (n *cniNetwork) getUsedSubnets() ([]*net.IPNet, error) { - // first, load all used subnets from network configs - subnets := make([]*net.IPNet, 0, len(n.networks)) +// ForEach call the given function for each network +func (n *cniNetwork) ForEach(run func(types.Network)) { for _, val := range n.networks { - for i := range val.libpodNet.Subnets { - subnets = append(subnets, &val.libpodNet.Subnets[i].Subnet.IPNet) - } + run(*val.libpodNet) } - // second, load networks from the current system - liveSubnets, err := util.GetLiveNetworkSubnets() - if err != nil { - return nil, err - } - return append(subnets, liveSubnets...), nil } -// getFreeDeviceName returns a free device name which can -// be used for new configs as name and bridge interface name -func (n *cniNetwork) getFreeDeviceName() (string, error) { - bridgeNames := n.getBridgeInterfaceNames() - netNames := n.getUsedNetworkNames() - liveInterfaces, err := util.GetLiveNetworkNames() - if err != nil { - return "", nil - } - names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces)) - names = append(names, bridgeNames...) - names = append(names, netNames...) - names = append(names, liveInterfaces...) - // FIXME: Is a limit fine? - // Start by 1, 0 is reserved for the default network - for i := 1; i < 1000000; i++ { - deviceName := fmt.Sprintf("%s%d", cniDeviceName, i) - if !pkgutil.StringInSlice(deviceName, names) { - logrus.Debugf("found free device name %s", deviceName) - return deviceName, nil - } - } - return "", errors.New("could not find free device name, to many iterations") +// Len return the number of networks +func (n *cniNetwork) Len() int { + return len(n.networks) } -// getUsedNetworkNames returns all network names already used -// by network configs -func (n *cniNetwork) getUsedNetworkNames() []string { - names := make([]string, 0, len(n.networks)) - for _, val := range n.networks { - names = append(names, val.libpodNet.Name) - } - return names +// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number. +func (n *cniNetwork) DefaultInterfaceName() string { + return cniDeviceName } -// getUsedNetworkNames returns all bridge device names already used -// by network configs -func (n *cniNetwork) getBridgeInterfaceNames() []string { - names := make([]string, 0, len(n.networks)) - for _, val := range n.networks { - if val.libpodNet.Driver == types.BridgeNetworkDriver { - names = append(names, val.libpodNet.NetworkInterface) - } +func (n *cniNetwork) Network(nameOrID string) (*types.Network, error) { + network, err := n.getNetwork(nameOrID) + if err != nil { + return nil, err } - return names + return network.libpodNet, err } diff --git a/libpod/network/cni/run.go b/libpod/network/cni/run.go index 7795dfeeb..667ed3ab1 100644 --- a/libpod/network/cni/run.go +++ b/libpod/network/cni/run.go @@ -13,6 +13,7 @@ import ( types040 "github.com/containernetworking/cni/pkg/types/040" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/internal/util" "github.com/containers/podman/v3/libpod/network/types" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -30,24 +31,9 @@ func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (ma return nil, err } - if namespacePath == "" { - return nil, errors.New("namespacePath is empty") - } - if options.ContainerID == "" { - return nil, errors.New("ContainerID is empty") - } - if len(options.Networks) == 0 { - return nil, errors.New("must specify at least one network") - } - for name, netOpts := range options.Networks { - network := n.networks[name] - if network == nil { - return nil, errors.Wrapf(define.ErrNoSuchNetwork, "network %s", name) - } - err := validatePerNetworkOpts(network, netOpts) - if err != nil { - return nil, err - } + err = util.ValidateSetupOptions(n, namespacePath, options) + if err != nil { + return nil, err } // set the loopback adapter up in the container netns @@ -172,23 +158,6 @@ func CNIResultToStatus(res cnitypes.Result) (types.StatusBlock, error) { return result, nil } -// validatePerNetworkOpts checks that all given static ips are in a subnet on this network -func validatePerNetworkOpts(network *network, netOpts types.PerNetworkOptions) error { - if netOpts.InterfaceName == "" { - return errors.Errorf("interface name on network %s is empty", network.libpodNet.Name) - } -outer: - for _, ip := range netOpts.StaticIPs { - for _, s := range network.libpodNet.Subnets { - if s.Subnet.Contains(ip) { - continue outer - } - } - return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.libpodNet.Name) - } - return nil -} - func getRuntimeConfig(netns, conName, conID, networkName string, ports []cniPortMapEntry, opts types.PerNetworkOptions) *libcni.RuntimeConf { rt := &libcni.RuntimeConf{ ContainerID: conID, diff --git a/libpod/network/cni/run_test.go b/libpod/network/cni/run_test.go index 3169cd0eb..6c54f82ef 100644 --- a/libpod/network/cni/run_test.go +++ b/libpod/network/cni/run_test.go @@ -1232,7 +1232,7 @@ var _ = Describe("run CNI", func() { } _, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("network somenet: network not found")) + Expect(err.Error()).To(ContainSubstring("unable to find network with name or ID somenet: network not found")) }) }) diff --git a/libpod/network/internal/util/bridge.go b/libpod/network/internal/util/bridge.go new file mode 100644 index 000000000..c054c7d4e --- /dev/null +++ b/libpod/network/internal/util/bridge.go @@ -0,0 +1,67 @@ +package util + +import ( + "net" + + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + pkgutil "github.com/containers/podman/v3/pkg/util" + "github.com/pkg/errors" +) + +func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet) error { + if network.NetworkInterface != "" { + bridges := GetBridgeInterfaceNames(n) + if pkgutil.StringInSlice(network.NetworkInterface, bridges) { + return errors.Errorf("bridge name %s already in use", network.NetworkInterface) + } + if !define.NameRegex.MatchString(network.NetworkInterface) { + return errors.Wrapf(define.RegexError, "bridge name %s invalid", network.NetworkInterface) + } + } else { + var err error + network.NetworkInterface, err = GetFreeDeviceName(n) + if err != nil { + return err + } + } + + if len(network.Subnets) == 0 { + freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + // ipv6 enabled means dual stack, check if we already have + // a ipv4 or ipv6 subnet and add one if not. + if network.IPv6Enabled { + ipv4 := false + ipv6 := false + for _, subnet := range network.Subnets { + if util.IsIPv6(subnet.Subnet.IP) { + ipv6 = true + } + if util.IsIPv4(subnet.Subnet.IP) { + ipv4 = true + } + } + if !ipv4 { + freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + if !ipv6 { + freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + } + network.IPAMOptions["driver"] = types.HostLocalIPAMDriver + return nil +} diff --git a/libpod/network/internal/util/create.go b/libpod/network/internal/util/create.go new file mode 100644 index 000000000..ca716f913 --- /dev/null +++ b/libpod/network/internal/util/create.go @@ -0,0 +1,48 @@ +package util + +import ( + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/pkg/errors" +) + +func CommonNetworkCreate(n NetUtil, network *types.Network) error { + // 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 network.ID != "" { + return errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") + } + + 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{} + } + + var name string + var err error + // validate the name when given + if network.Name != "" { + if !define.NameRegex.MatchString(network.Name) { + return errors.Wrapf(define.RegexError, "network name %s invalid", network.Name) + } + if _, err := n.Network(network.Name); err == nil { + return errors.Wrapf(define.ErrNetworkExists, "network name %s already used", network.Name) + } + } else { + name, err = GetFreeDeviceName(n) + if err != nil { + return err + } + network.Name = name + // also use the name as interface name when we create a bridge network + if network.Driver == types.BridgeNetworkDriver && network.NetworkInterface == "" { + network.NetworkInterface = name + } + } + return nil +} diff --git a/libpod/network/internal/util/interface.go b/libpod/network/internal/util/interface.go new file mode 100644 index 000000000..4b01a09b8 --- /dev/null +++ b/libpod/network/internal/util/interface.go @@ -0,0 +1,19 @@ +package util + +import "github.com/containers/podman/v3/libpod/network/types" + +// This is a helper package to allow code sharing between the different +// network interfaces. + +// NetUtil is a helper interface which all network interfaces should implement to allow easy code sharing +type NetUtil interface { + // ForEach eaxecutes the given function for each network + ForEach(func(types.Network)) + // Len returns the number of networks + Len() int + // DefaultInterfaceName return the default interface name, this will be suffixed by a number + DefaultInterfaceName() string + // Network returns the network with the given name or ID. + // It returns an error if the network is not found + Network(nameOrID string) (*types.Network, error) +} diff --git a/libpod/network/internal/util/interfaces.go b/libpod/network/internal/util/interfaces.go new file mode 100644 index 000000000..20819f756 --- /dev/null +++ b/libpod/network/internal/util/interfaces.go @@ -0,0 +1,34 @@ +package util + +import "net" + +// getLiveNetworkSubnets returns a slice of subnets representing what the system +// has defined as network interfaces +func getLiveNetworkSubnets() ([]*net.IPNet, error) { + addrs, err := net.InterfaceAddrs() + if err != nil { + return nil, err + } + nets := make([]*net.IPNet, 0, len(addrs)) + for _, address := range addrs { + _, n, err := net.ParseCIDR(address.String()) + if err != nil { + return nil, err + } + nets = append(nets, n) + } + return nets, nil +} + +// GetLiveNetworkNames returns a list of network interface names on the system +func GetLiveNetworkNames() ([]string, error) { + liveInterfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + interfaceNames := make([]string, 0, len(liveInterfaces)) + for _, i := range liveInterfaces { + interfaceNames = append(interfaceNames, i.Name) + } + return interfaceNames, nil +} diff --git a/libpod/network/internal/util/ip.go b/libpod/network/internal/util/ip.go new file mode 100644 index 000000000..7fe35d3d4 --- /dev/null +++ b/libpod/network/internal/util/ip.go @@ -0,0 +1,70 @@ +package util + +import ( + "crypto/rand" + "net" + + "github.com/pkg/errors" +) + +func incByte(subnet *net.IPNet, idx int, shift uint) error { + if idx < 0 { + return errors.New("no more subnets left") + } + if subnet.IP[idx] == 255 { + subnet.IP[idx] = 0 + return incByte(subnet, idx-1, 0) + } + subnet.IP[idx] += 1 << shift + return nil +} + +// NextSubnet returns subnet incremented by 1 +func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { + newSubnet := &net.IPNet{ + IP: subnet.IP, + Mask: subnet.Mask, + } + ones, bits := newSubnet.Mask.Size() + if ones == 0 { + return nil, errors.Errorf("%s has only one subnet", subnet.String()) + } + zeroes := uint(bits - ones) + shift := zeroes % 8 + idx := ones/8 - 1 + if idx < 0 { + idx = 0 + } + if err := incByte(newSubnet, idx, shift); err != nil { + return nil, err + } + return newSubnet, nil +} + +func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool { + for _, nw := range networklist { + if networkIntersect(n, nw) { + return true + } + } + return false +} + +func networkIntersect(n1, n2 *net.IPNet) bool { + return n2.Contains(n1.IP) || n1.Contains(n2.IP) +} + +// getRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879. +func getRandomIPv6Subnet() (net.IPNet, error) { + ip := make(net.IP, 8, net.IPv6len) + // read 8 random bytes + _, err := rand.Read(ip) + if err != nil { + return net.IPNet{}, nil + } + // first byte must be FD as per RFC3879 + ip[0] = 0xfd + // add 8 zero bytes + ip = append(ip, make([]byte, 8)...) + return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil +} diff --git a/libpod/network/internal/util/ip_test.go b/libpod/network/internal/util/ip_test.go new file mode 100644 index 000000000..eaed769d7 --- /dev/null +++ b/libpod/network/internal/util/ip_test.go @@ -0,0 +1,63 @@ +package util + +import ( + "fmt" + "net" + "reflect" + "testing" +) + +func parseCIDR(n string) *net.IPNet { + _, parsedNet, _ := net.ParseCIDR(n) + return parsedNet +} + +func TestNextSubnet(t *testing.T) { + type args struct { + subnet *net.IPNet + } + tests := []struct { + name string + args args + want *net.IPNet + wantErr bool + }{ + {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, + {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + got, err := NextSubnet(test.args.subnet) + if (err != nil) != test.wantErr { + t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) + return + } + if !reflect.DeepEqual(got, test.want) { + t.Errorf("NextSubnet() got = %v, want %v", got, test.want) + } + }) + } +} + +func TestGetRandomIPv6Subnet(t *testing.T) { + for i := 0; i < 1000; i++ { + t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) { + sub, err := getRandomIPv6Subnet() + if err != nil { + t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err) + return + } + if sub.IP.To4() != nil { + t.Errorf("ip %s is not an ipv6 address", sub.IP) + } + if sub.IP[0] != 0xfd { + t.Errorf("ipv6 %s does not start with fd", sub.IP) + } + ones, bytes := sub.Mask.Size() + if ones != 64 || bytes != 128 { + t.Errorf("wrong network mask %v, it should be /64", sub.Mask) + } + }) + } +} diff --git a/libpod/network/internal/util/util.go b/libpod/network/internal/util/util.go new file mode 100644 index 000000000..bf9d70aba --- /dev/null +++ b/libpod/network/internal/util/util.go @@ -0,0 +1,123 @@ +package util + +import ( + "errors" + "fmt" + "net" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/pkg/util" + "github.com/sirupsen/logrus" +) + +// GetBridgeInterfaceNames returns all bridge interface names +// already used by network configs +func GetBridgeInterfaceNames(n NetUtil) []string { + names := make([]string, 0, n.Len()) + n.ForEach(func(net types.Network) { + if net.Driver == types.BridgeNetworkDriver { + names = append(names, net.NetworkInterface) + } + }) + return names +} + +// GetUsedNetworkNames returns all network names already used +// by network configs +func GetUsedNetworkNames(n NetUtil) []string { + names := make([]string, 0, n.Len()) + n.ForEach(func(net types.Network) { + if net.Driver == types.BridgeNetworkDriver { + names = append(names, net.NetworkInterface) + } + }) + return names +} + +// GetFreeDeviceName returns a free device name which can +// be used for new configs as name and bridge interface name. +// The base name is suffixed by a number +func GetFreeDeviceName(n NetUtil) (string, error) { + bridgeNames := GetBridgeInterfaceNames(n) + netNames := GetUsedNetworkNames(n) + liveInterfaces, err := GetLiveNetworkNames() + if err != nil { + return "", nil + } + names := make([]string, 0, len(bridgeNames)+len(netNames)+len(liveInterfaces)) + names = append(names, bridgeNames...) + names = append(names, netNames...) + names = append(names, liveInterfaces...) + // FIXME: Is a limit fine? + // Start by 1, 0 is reserved for the default network + for i := 1; i < 1000000; i++ { + deviceName := fmt.Sprintf("%s%d", n.DefaultInterfaceName(), i) + if !util.StringInSlice(deviceName, names) { + logrus.Debugf("found free device name %s", deviceName) + return deviceName, nil + } + } + return "", errors.New("could not find free device name, to many iterations") +} + +// GetUsedSubnets returns a list of all used subnets by network +// configs and interfaces on the host. +func GetUsedSubnets(n NetUtil) ([]*net.IPNet, error) { + // first, load all used subnets from network configs + subnets := make([]*net.IPNet, 0, n.Len()) + n.ForEach(func(n types.Network) { + for i := range n.Subnets { + subnets = append(subnets, &n.Subnets[i].Subnet.IPNet) + } + }) + // second, load networks from the current system + liveSubnets, err := getLiveNetworkSubnets() + if err != nil { + return nil, err + } + return append(subnets, liveSubnets...), nil +} + +// GetFreeIPv6NetworkSubnet returns a unused ipv4 subnet +func GetFreeIPv4NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { + // the default podman network is 10.88.0.0/16 + // start locking for free /24 networks + network := &net.IPNet{ + IP: net.IP{10, 89, 0, 0}, + Mask: net.IPMask{255, 255, 255, 0}, + } + + // TODO: make sure to not use public subnets + for { + if intersectsConfig := NetworkIntersectsWithNetworks(network, usedNetworks); !intersectsConfig { + logrus.Debugf("found free ipv4 network subnet %s", network.String()) + return &types.Subnet{ + Subnet: types.IPNet{IPNet: *network}, + }, nil + } + var err error + network, err = NextSubnet(network) + if err != nil { + return nil, err + } + } +} + +// GetFreeIPv6NetworkSubnet returns a unused ipv6 subnet +func GetFreeIPv6NetworkSubnet(usedNetworks []*net.IPNet) (*types.Subnet, error) { + // FIXME: Is 10000 fine as limit? We should prevent an endless loop. + for i := 0; i < 10000; i++ { + // RFC4193: Choose the ipv6 subnet random and NOT sequentially. + network, err := getRandomIPv6Subnet() + if err != nil { + return nil, err + } + if intersectsConfig := NetworkIntersectsWithNetworks(&network, usedNetworks); !intersectsConfig { + logrus.Debugf("found free ipv6 network subnet %s", network.String()) + return &types.Subnet{ + Subnet: types.IPNet{IPNet: network}, + }, nil + } + } + return nil, errors.New("failed to get random ipv6 subnet") +} diff --git a/libpod/network/internal/util/validate.go b/libpod/network/internal/util/validate.go new file mode 100644 index 000000000..03a985043 --- /dev/null +++ b/libpod/network/internal/util/validate.go @@ -0,0 +1,97 @@ +package util + +import ( + "net" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" + "github.com/pkg/errors" +) + +// ValidateSubnet will validate a given Subnet. It checks if the +// given gateway and lease range are part of this subnet. If the +// gateway is empty and addGateway is true it will get the first +// available ip in the subnet assigned. +func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) error { + if s == nil { + return errors.New("subnet is nil") + } + if s.Subnet.IP == nil { + return errors.New("subnet ip is nil") + } + + // Reparse to ensure subnet is valid. + // Do not use types.ParseCIDR() because we want the ip to be + // the network address and not a random ip in the subnet. + _, net, err := net.ParseCIDR(s.Subnet.String()) + if err != nil { + return errors.Wrap(err, "subnet invalid") + } + + // check that the new subnet does not conflict with existing ones + if NetworkIntersectsWithNetworks(net, usedNetworks) { + return errors.Errorf("subnet %s is already used on the host or by another config", net.String()) + } + + s.Subnet = types.IPNet{IPNet: *net} + if s.Gateway != nil { + if !s.Subnet.Contains(s.Gateway) { + return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) + } + } else if addGateway { + ip, err := util.FirstIPInSubnet(net) + if err != nil { + return err + } + s.Gateway = ip + } + if s.LeaseRange != nil { + if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) { + return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) + } + if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) { + return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) + } + } + return nil +} + +func ValidateSetupOptions(n NetUtil, namespacePath string, options types.SetupOptions) error { + if namespacePath == "" { + return errors.New("namespacePath is empty") + } + if options.ContainerID == "" { + return errors.New("ContainerID is empty") + } + if len(options.Networks) == 0 { + return errors.New("must specify at least one network") + } + for name, netOpts := range options.Networks { + network, err := n.Network(name) + if err != nil { + return err + } + err = validatePerNetworkOpts(network, netOpts) + if err != nil { + return err + } + } + return nil +} + +// validatePerNetworkOpts checks that all given static ips are in a subnet on this network +func validatePerNetworkOpts(network *types.Network, netOpts types.PerNetworkOptions) error { + if netOpts.InterfaceName == "" { + return errors.Errorf("interface name on network %s is empty", network.Name) + } +outer: + for _, ip := range netOpts.StaticIPs { + for _, s := range network.Subnets { + if s.Subnet.Contains(ip) { + continue outer + } + } + return errors.Errorf("requested static ip %s not in any subnet on network %s", ip.String(), network.Name) + } + return nil +} diff --git a/libpod/network/util/interfaces.go b/libpod/network/util/interfaces.go deleted file mode 100644 index dc2bd601d..000000000 --- a/libpod/network/util/interfaces.go +++ /dev/null @@ -1,34 +0,0 @@ -package util - -import "net" - -// GetLiveNetworkSubnets returns a slice of subnets representing what the system -// has defined as network interfaces -func GetLiveNetworkSubnets() ([]*net.IPNet, error) { - addrs, err := net.InterfaceAddrs() - if err != nil { - return nil, err - } - nets := make([]*net.IPNet, 0, len(addrs)) - for _, address := range addrs { - _, n, err := net.ParseCIDR(address.String()) - if err != nil { - return nil, err - } - nets = append(nets, n) - } - return nets, nil -} - -// GetLiveNetworkNames returns a list of network interface names on the system -func GetLiveNetworkNames() ([]string, error) { - liveInterfaces, err := net.Interfaces() - if err != nil { - return nil, err - } - interfaceNames := make([]string, 0, len(liveInterfaces)) - for _, i := range liveInterfaces { - interfaceNames = append(interfaceNames, i.Name) - } - return interfaceNames, nil -} diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go index b2ba92735..e75107a1c 100644 --- a/libpod/network/util/ip.go +++ b/libpod/network/util/ip.go @@ -1,11 +1,6 @@ package util -import ( - "crypto/rand" - "net" - - "github.com/pkg/errors" -) +import "net" // IsIPv6 returns true if netIP is IPv6. func IsIPv6(netIP net.IP) bool { @@ -17,40 +12,6 @@ func IsIPv4(netIP net.IP) bool { return netIP != nil && netIP.To4() != nil } -func incByte(subnet *net.IPNet, idx int, shift uint) error { - if idx < 0 { - return errors.New("no more subnets left") - } - if subnet.IP[idx] == 255 { - subnet.IP[idx] = 0 - return incByte(subnet, idx-1, 0) - } - subnet.IP[idx] += 1 << shift - return nil -} - -// NextSubnet returns subnet incremented by 1 -func NextSubnet(subnet *net.IPNet) (*net.IPNet, error) { - newSubnet := &net.IPNet{ - IP: subnet.IP, - Mask: subnet.Mask, - } - ones, bits := newSubnet.Mask.Size() - if ones == 0 { - return nil, errors.Errorf("%s has only one subnet", subnet.String()) - } - zeroes := uint(bits - ones) - shift := zeroes % 8 - idx := ones/8 - 1 - if idx < 0 { - idx = 0 - } - if err := incByte(newSubnet, idx, shift); err != nil { - return nil, err - } - return newSubnet, nil -} - // LastIPInSubnet gets the last IP in a subnet func LastIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer // re-parse to ensure clean network address @@ -83,31 +44,3 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer cidr.IP[len(cidr.IP)-1]++ return cidr.IP, nil } - -func NetworkIntersectsWithNetworks(n *net.IPNet, networklist []*net.IPNet) bool { - for _, nw := range networklist { - if networkIntersect(n, nw) { - return true - } - } - return false -} - -func networkIntersect(n1, n2 *net.IPNet) bool { - return n2.Contains(n1.IP) || n1.Contains(n2.IP) -} - -// GetRandomIPv6Subnet returns a random internal ipv6 subnet as described in RFC3879. -func GetRandomIPv6Subnet() (net.IPNet, error) { - ip := make(net.IP, 8, net.IPv6len) - // read 8 random bytes - _, err := rand.Read(ip) - if err != nil { - return net.IPNet{}, nil - } - // first byte must be FD as per RFC3879 - ip[0] = 0xfd - // add 8 zero bytes - ip = append(ip, make([]byte, 8)...) - return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil -} diff --git a/libpod/network/util/ip_test.go b/libpod/network/util/ip_test.go index c26ad140a..63ac555f0 100644 --- a/libpod/network/util/ip_test.go +++ b/libpod/network/util/ip_test.go @@ -1,9 +1,7 @@ package util import ( - "fmt" "net" - "reflect" "testing" ) @@ -12,34 +10,6 @@ func parseCIDR(n string) *net.IPNet { return parsedNet } -func TestNextSubnet(t *testing.T) { - type args struct { - subnet *net.IPNet - } - tests := []struct { - name string - args args - want *net.IPNet - wantErr bool - }{ - {"class b", args{subnet: parseCIDR("192.168.0.0/16")}, parseCIDR("192.169.0.0/16"), false}, - {"class c", args{subnet: parseCIDR("192.168.1.0/24")}, parseCIDR("192.168.2.0/24"), false}, - } - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - got, err := NextSubnet(test.args.subnet) - if (err != nil) != test.wantErr { - t.Errorf("NextSubnet() error = %v, wantErr %v", err, test.wantErr) - return - } - if !reflect.DeepEqual(got, test.want) { - t.Errorf("NextSubnet() got = %v, want %v", got, test.want) - } - }) - } -} - func TestFirstIPInSubnet(t *testing.T) { tests := []struct { name string @@ -101,25 +71,3 @@ func TestLastIPInSubnet(t *testing.T) { }) } } - -func TestGetRandomIPv6Subnet(t *testing.T) { - for i := 0; i < 1000; i++ { - t.Run(fmt.Sprintf("GetRandomIPv6Subnet %d", i), func(t *testing.T) { - sub, err := GetRandomIPv6Subnet() - if err != nil { - t.Errorf("GetRandomIPv6Subnet() error should be nil: %v", err) - return - } - if sub.IP.To4() != nil { - t.Errorf("ip %s is not an ipv6 address", sub.IP) - } - if sub.IP[0] != 0xfd { - t.Errorf("ipv6 %s does not start with fd", sub.IP) - } - ones, bytes := sub.Mask.Size() - if ones != 64 || bytes != 128 { - t.Errorf("wrong network mask %v, it should be /64", sub.Mask) - } - }) - } -} -- cgit v1.2.3-54-g00ecf From eaae29462880aa0fb17e8d448cc79519e070e64f Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 14 Oct 2021 10:33:18 +0200 Subject: netavark network interface Implement a new network interface for netavark. For now only bridge networking is supported. The interface can create/list/inspect/remove networks. For setup and teardown netavark will be invoked. Signed-off-by: Paul Holzinger --- libpod/network/cni/cni_conversion.go | 40 +- libpod/network/cni/config.go | 18 +- libpod/network/internal/util/create.go | 6 - libpod/network/internal/util/ip.go | 8 + libpod/network/internal/util/parse.go | 37 + libpod/network/internal/util/validate.go | 32 +- libpod/network/netavark/config.go | 210 ++++ libpod/network/netavark/config_test.go | 1123 ++++++++++++++++++++ libpod/network/netavark/const.go | 5 + libpod/network/netavark/exec.go | 116 ++ libpod/network/netavark/netavark_suite_test.go | 37 + libpod/network/netavark/network.go | 275 +++++ libpod/network/netavark/run.go | 90 ++ libpod/network/netavark/run_test.go | 363 +++++++ .../network/netavark/testfiles/invalid/broken.json | 16 + .../netavark/testfiles/invalid/invalid name.json | 19 + .../testfiles/invalid/invalid_gateway.json | 19 + .../netavark/testfiles/invalid/name_missmatch.json | 19 + .../netavark/testfiles/invalid/wrongID.json | 19 + .../network/netavark/testfiles/valid/bridge.json | 23 + .../netavark/testfiles/valid/dualstack.json | 23 + .../network/netavark/testfiles/valid/internal.json | 18 + libpod/network/netavark/testfiles/valid/label.json | 22 + libpod/network/netavark/testfiles/valid/mtu.json | 22 + .../network/netavark/testfiles/valid/podman.json | 19 + libpod/network/netavark/testfiles/valid/vlan.json | 22 + 26 files changed, 2546 insertions(+), 55 deletions(-) create mode 100644 libpod/network/internal/util/parse.go create mode 100644 libpod/network/netavark/config.go create mode 100644 libpod/network/netavark/config_test.go create mode 100644 libpod/network/netavark/const.go create mode 100644 libpod/network/netavark/exec.go create mode 100644 libpod/network/netavark/netavark_suite_test.go create mode 100644 libpod/network/netavark/network.go create mode 100644 libpod/network/netavark/run.go create mode 100644 libpod/network/netavark/run_test.go create mode 100644 libpod/network/netavark/testfiles/invalid/broken.json create mode 100644 libpod/network/netavark/testfiles/invalid/invalid name.json create mode 100644 libpod/network/netavark/testfiles/invalid/invalid_gateway.json create mode 100644 libpod/network/netavark/testfiles/invalid/name_missmatch.json create mode 100644 libpod/network/netavark/testfiles/invalid/wrongID.json create mode 100644 libpod/network/netavark/testfiles/valid/bridge.json create mode 100644 libpod/network/netavark/testfiles/valid/dualstack.json create mode 100644 libpod/network/netavark/testfiles/valid/internal.json create mode 100644 libpod/network/netavark/testfiles/valid/label.json create mode 100644 libpod/network/netavark/testfiles/valid/mtu.json create mode 100644 libpod/network/netavark/testfiles/valid/podman.json create mode 100644 libpod/network/netavark/testfiles/valid/vlan.json diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go index 01e149114..14cab2573 100644 --- a/libpod/network/cni/cni_conversion.go +++ b/libpod/network/cni/cni_conversion.go @@ -14,6 +14,7 @@ import ( "time" "github.com/containernetworking/cni/libcni" + internalutil "github.com/containers/podman/v3/libpod/network/internal/util" "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/network/util" pkgutil "github.com/containers/podman/v3/pkg/util" @@ -156,10 +157,7 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway) } // convert to 4 byte if ipv4 - ipv4 := gateway.To4() - if ipv4 != nil { - gateway = ipv4 - } + internalutil.NormalizeIP(&gateway) } else if !network.Internal { // only add a gateway address if the network is not internal gateway, err = util.FirstIPInSubnet(sub) @@ -244,13 +242,13 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ for k, v := range network.Options { switch k { case "mtu": - mtu, err = parseMTU(v) + mtu, err = internalutil.ParseMTU(v) if err != nil { return nil, "", err } case "vlan": - vlan, err = parseVlan(v) + vlan, err = internalutil.ParseVlan(v) if err != nil { return nil, "", err } @@ -339,36 +337,6 @@ func (n *cniNetwork) createCNIConfigListFromNetwork(network *types.Network, writ return config, cniPathName, nil } -// parseMTU parses the mtu option -func parseMTU(mtu string) (int, error) { - if mtu == "" { - return 0, nil // default - } - m, err := strconv.Atoi(mtu) - if err != nil { - return 0, err - } - if m < 0 { - return 0, errors.Errorf("mtu %d is less than zero", m) - } - return m, nil -} - -// parseVlan parses the vlan option -func parseVlan(vlan string) (int, error) { - if vlan == "" { - return 0, nil // default - } - v, err := strconv.Atoi(vlan) - if err != nil { - return 0, err - } - if v < 0 || v > 4094 { - return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v) - } - return v, nil -} - func convertSpecgenPortsToCNIPorts(ports []types.PortMapping) ([]cniPortMapEntry, error) { cniPorts := make([]cniPortMapEntry, 0, len(ports)) for _, port := range ports { diff --git a/libpod/network/cni/config.go b/libpod/network/cni/config.go index a63574f25..5d587da23 100644 --- a/libpod/network/cni/config.go +++ b/libpod/network/cni/config.go @@ -9,7 +9,6 @@ import ( "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/podman/v3/libpod/network/util" pkgutil "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -42,6 +41,12 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* newNetwork.Driver = types.DefaultNetworkDriver } + // 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") + } + err := internalutil.CommonNetworkCreate(n, &newNetwork) if err != nil { return nil, err @@ -77,14 +82,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver) } - for i := range newNetwork.Subnets { - err := internalutil.ValidateSubnet(&newNetwork.Subnets[i], !newNetwork.Internal, usedNetworks) - if err != nil { - return nil, err - } - if util.IsIPv6(newNetwork.Subnets[i].Subnet.IP) { - newNetwork.IPv6Enabled = true - } + err = internalutil.ValidateSubnets(&newNetwork, usedNetworks) + if err != nil { + return nil, err } // generate the network ID diff --git a/libpod/network/internal/util/create.go b/libpod/network/internal/util/create.go index ca716f913..cecfd7133 100644 --- a/libpod/network/internal/util/create.go +++ b/libpod/network/internal/util/create.go @@ -7,12 +7,6 @@ import ( ) func CommonNetworkCreate(n NetUtil, network *types.Network) error { - // 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 network.ID != "" { - return errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") - } - if network.Labels == nil { network.Labels = map[string]string{} } diff --git a/libpod/network/internal/util/ip.go b/libpod/network/internal/util/ip.go index 7fe35d3d4..ee759fd65 100644 --- a/libpod/network/internal/util/ip.go +++ b/libpod/network/internal/util/ip.go @@ -68,3 +68,11 @@ func getRandomIPv6Subnet() (net.IPNet, error) { ip = append(ip, make([]byte, 8)...) return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil } + +// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible +func NormalizeIP(ip *net.IP) { + ipv4 := ip.To4() + if ipv4 != nil { + *ip = ipv4 + } +} diff --git a/libpod/network/internal/util/parse.go b/libpod/network/internal/util/parse.go new file mode 100644 index 000000000..1f68df0bb --- /dev/null +++ b/libpod/network/internal/util/parse.go @@ -0,0 +1,37 @@ +package util + +import ( + "strconv" + + "github.com/pkg/errors" +) + +// ParseMTU parses the mtu option +func ParseMTU(mtu string) (int, error) { + if mtu == "" { + return 0, nil // default + } + m, err := strconv.Atoi(mtu) + if err != nil { + return 0, err + } + if m < 0 { + return 0, errors.Errorf("mtu %d is less than zero", m) + } + return m, nil +} + +// ParseVlan parses the vlan option +func ParseVlan(vlan string) (int, error) { + if vlan == "" { + return 0, nil // default + } + v, err := strconv.Atoi(vlan) + if err != nil { + return 0, err + } + if v < 0 || v > 4094 { + return 0, errors.Errorf("vlan ID %d must be between 0 and 4094", v) + } + return v, nil +} diff --git a/libpod/network/internal/util/validate.go b/libpod/network/internal/util/validate.go index 03a985043..4dced8631 100644 --- a/libpod/network/internal/util/validate.go +++ b/libpod/network/internal/util/validate.go @@ -38,6 +38,7 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) if !s.Subnet.Contains(s.Gateway) { return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) } + NormalizeIP(&s.Gateway) } else if addGateway { ip, err := util.FirstIPInSubnet(net) if err != nil { @@ -45,12 +46,35 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) } s.Gateway = ip } + if s.LeaseRange != nil { - if s.LeaseRange.StartIP != nil && !s.Subnet.Contains(s.LeaseRange.StartIP) { - return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) + if s.LeaseRange.StartIP != nil { + if !s.Subnet.Contains(s.LeaseRange.StartIP) { + return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) + } + NormalizeIP(&s.LeaseRange.StartIP) + } + if s.LeaseRange.EndIP != nil { + if !s.Subnet.Contains(s.LeaseRange.EndIP) { + return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) + } + NormalizeIP(&s.LeaseRange.EndIP) + } + } + return nil +} + +// ValidateSubnets will validate the subnets for this network. +// It also sets the gateway if the gateway is empty and it sets +// IPv6Enabled to true if at least one subnet is ipv6. +func ValidateSubnets(network *types.Network, usedNetworks []*net.IPNet) error { + for i := range network.Subnets { + err := ValidateSubnet(&network.Subnets[i], !network.Internal, usedNetworks) + if err != nil { + return err } - if s.LeaseRange.EndIP != nil && !s.Subnet.Contains(s.LeaseRange.EndIP) { - return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) + if util.IsIPv6(network.Subnets[i].Subnet.IP) { + network.IPv6Enabled = true } } return nil diff --git a/libpod/network/netavark/config.go b/libpod/network/netavark/config.go new file mode 100644 index 000000000..5cab76710 --- /dev/null +++ b/libpod/network/netavark/config.go @@ -0,0 +1,210 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "net" + "os" + "path/filepath" + "time" + + "github.com/containers/podman/v3/libpod/define" + internalutil "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/storage/pkg/stringid" + "github.com/pkg/errors" +) + +// NetworkCreate will take a partial filled Network and fill the +// missing fields. It creates the Network and returns the full Network. +func (n *netavarkNetwork) NetworkCreate(net types.Network) (types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return types.Network{}, err + } + network, err := n.networkCreate(net, false) + if err != nil { + return types.Network{}, err + } + // add the new network to the map + n.networks[network.Name] = network + return *network, nil +} + +func (n *netavarkNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (*types.Network, error) { + // if no driver is set use the default one + if newNetwork.Driver == "" { + newNetwork.Driver = types.DefaultNetworkDriver + } + if !defaultNet { + // FIXME: Should we use a different type for network create without the ID field? + // the caller is not allowed to set a specific ID + if newNetwork.ID != "" { + return nil, errors.Wrap(define.ErrInvalidArg, "ID can not be set for network create") + } + + // generate random network ID + var i int + for i = 0; i < 1000; i++ { + id := stringid.GenerateNonCryptoID() + if _, err := n.getNetwork(id); err != nil { + newNetwork.ID = id + break + } + } + if i == 1000 { + return nil, errors.New("failed to create random network ID") + } + } + + err := internalutil.CommonNetworkCreate(n, &newNetwork) + if err != nil { + return nil, err + } + + // Only get the used networks for validation if we do not create the default network. + // The default network should not be validated against used subnets, we have to ensure + // that this network can always be created even when a subnet is already used on the host. + // This could happen if you run a container on this net, then the cni interface will be + // created on the host and "block" this subnet from being used again. + // Therefore the next podman command tries to create the default net again and it would + // fail because it thinks the network is used on the host. + var usedNetworks []*net.IPNet + if !defaultNet { + usedNetworks, err = internalutil.GetUsedSubnets(n) + if err != nil { + return nil, err + } + } + + switch newNetwork.Driver { + case types.BridgeNetworkDriver: + err = internalutil.CreateBridge(n, &newNetwork, usedNetworks) + if err != nil { + return nil, err + } + // validate the given options, we do not need them but just check to make sure they are valid + for key, value := range newNetwork.Options { + switch key { + case "mtu": + _, err = internalutil.ParseMTU(value) + if err != nil { + return nil, err + } + + case "vlan": + _, err = internalutil.ParseVlan(value) + if err != nil { + return nil, err + } + + default: + return nil, errors.Errorf("unsupported network option %s", key) + } + } + + default: + return nil, errors.Wrapf(define.ErrInvalidArg, "unsupported driver %s", newNetwork.Driver) + } + + err = internalutil.ValidateSubnets(&newNetwork, usedNetworks) + if err != nil { + return nil, err + } + + // FIXME: If we have a working solution for internal networks with dns this check should be removed. + if newNetwork.DNSEnabled && newNetwork.Internal { + return nil, errors.New("cannot set internal and dns enabled") + } + + newNetwork.Created = time.Now() + + if !defaultNet { + confPath := filepath.Join(n.networkConfigDir, newNetwork.Name+".json") + f, err := os.Create(confPath) + if err != nil { + return nil, err + } + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + err = enc.Encode(newNetwork) + if err != nil { + return nil, err + } + } + + return &newNetwork, nil +} + +// NetworkRemove will remove the Network with the given name or ID. +// It does not ensure that the network is unused. +func (n *netavarkNetwork) NetworkRemove(nameOrID string) error { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return err + } + + network, err := n.getNetwork(nameOrID) + if err != nil { + return err + } + + // Removing the default network is not allowed. + if network.Name == n.defaultNetwork { + return errors.Errorf("default network %s cannot be removed", n.defaultNetwork) + } + + file := filepath.Join(n.networkConfigDir, network.Name+".json") + // make sure to not error for ErrNotExist + if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + delete(n.networks, network.Name) + return nil +} + +// NetworkList will return all known Networks. Optionally you can +// supply a list of filter functions. Only if a network matches all +// functions it is returned. +func (n *netavarkNetwork) NetworkList(filters ...types.FilterFunc) ([]types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return nil, err + } + + networks := make([]types.Network, 0, len(n.networks)) +outer: + for _, net := range n.networks { + for _, filter := range filters { + // All filters have to match, if one does not match we can skip to the next network. + if !filter(*net) { + continue outer + } + } + networks = append(networks, *net) + } + return networks, nil +} + +// NetworkInspect will return the Network with the given name or ID. +func (n *netavarkNetwork) NetworkInspect(nameOrID string) (types.Network, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return types.Network{}, err + } + + network, err := n.getNetwork(nameOrID) + if err != nil { + return types.Network{}, err + } + return *network, nil +} diff --git a/libpod/network/netavark/config_test.go b/libpod/network/netavark/config_test.go new file mode 100644 index 000000000..ee4a825f1 --- /dev/null +++ b/libpod/network/netavark/config_test.go @@ -0,0 +1,1123 @@ +// +build linux + +package netavark_test + +import ( + "bytes" + "io/ioutil" + "net" + "os" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + gomegaTypes "github.com/onsi/gomega/types" + "github.com/sirupsen/logrus" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/libpod/network/util" +) + +var _ = Describe("Config", func() { + var ( + libpodNet types.ContainerNetwork + networkConfDir string + logBuffer bytes.Buffer + ) + + BeforeEach(func() { + var err error + networkConfDir, err = ioutil.TempDir("", "podman_netavark_test") + if err != nil { + Fail("Failed to create tmpdir") + + } + logBuffer = bytes.Buffer{} + logrus.SetOutput(&logBuffer) + }) + + JustBeforeEach(func() { + var err error + libpodNet, err = getNetworkInterface(networkConfDir, false) + if err != nil { + Fail("Failed to create NewCNINetworkInterface") + } + }) + + AfterEach(func() { + os.RemoveAll(networkConfDir) + }) + + Context("basic network config tests", func() { + + It("check default network config exists", func() { + networks, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks[0].Name).To(Equal("podman")) + Expect(networks[0].Driver).To(Equal("bridge")) + Expect(networks[0].ID).To(Equal("2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9")) + Expect(networks[0].NetworkInterface).To(Equal("podman0")) + Expect(networks[0].Created.Before(time.Now())).To(BeTrue()) + Expect(networks[0].Subnets).To(HaveLen(1)) + Expect(networks[0].Subnets[0].Subnet.String()).To(Equal("10.88.0.0/16")) + Expect(networks[0].Subnets[0].Gateway.String()).To(Equal("10.88.0.1")) + Expect(networks[0].Subnets[0].LeaseRange).To(BeNil()) + Expect(networks[0].IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(networks[0].Options).To(BeEmpty()) + Expect(networks[0].Labels).To(BeEmpty()) + Expect(networks[0].DNSEnabled).To(BeFalse()) + Expect(networks[0].Internal).To(BeFalse()) + }) + + It("basic network create, inspect and remove", func() { + // Because we get the time from the file create timestamp there is small precision + // loss so lets remove 500 milliseconds to make sure this test does not flake. + now := time.Now().Add(-500 * time.Millisecond) + network := types.Network{} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).To(BeEmpty()) + Expect(network1.Options).To(BeEmpty()) + Expect(network1.IPAMOptions).ToNot(BeEmpty()) + Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(network1.Created.After(now)).To(BeTrue()) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.DNSEnabled).To(BeFalse()) + Expect(network1.Internal).To(BeFalse()) + + // inspect by name + network2, err := libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // inspect by ID + network2, err = libpodNet.NetworkInspect(network1.ID) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // inspect by partial ID + network2, err = libpodNet.NetworkInspect(network1.ID[:10]) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + // create a new interface to force a config load from disk + libpodNet, err = getNetworkInterface(networkConfDir, false) + Expect(err).To(BeNil()) + + network2, err = libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + Expect(path).ToNot(BeARegularFile()) + + _, err = libpodNet.NetworkInspect(network1.Name) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network not found")) + }) + + It("create two networks", func() { + network := types.Network{} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.Subnets).To(HaveLen(1)) + + network = types.Network{} + network2, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network2.Name).ToNot(Equal(network1.Name)) + Expect(network2.ID).ToNot(Equal(network1.ID)) + Expect(network2.NetworkInterface).ToNot(Equal(network1.NetworkInterface)) + Expect(network2.Subnets).To(HaveLen(1)) + Expect(network2.Subnets[0].Subnet.Contains(network1.Subnets[0].Subnet.IP)).To(BeFalse()) + }) + + It("create bridge config", func() { + network := types.Network{Driver: "bridge"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).To(BeEmpty()) + Expect(network1.Options).To(BeEmpty()) + Expect(network1.IPAMOptions).ToNot(BeEmpty()) + Expect(network1.IPAMOptions).To(HaveKeyWithValue("driver", "host-local")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.89.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.89.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.DNSEnabled).To(BeFalse()) + Expect(network1.Internal).To(BeFalse()) + }) + + It("create bridge with same name should fail", func() { + network := types.Network{ + Driver: "bridge", + NetworkInterface: "podman2", + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).To(Equal("podman2")) + Expect(network1.Driver).To(Equal("bridge")) + + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("bridge name podman2 already in use")) + }) + + It("create bridge with subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 subnet", func() { + subnet := "fdcc::/64" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.IPv6Enabled).To(BeTrue()) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("fdcc::1")) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + + // reload configs from disk + libpodNet, err = getNetworkInterface(networkConfDir, false) + Expect(err).To(BeNil()) + // check the the networks are identical + network2, err := libpodNet.NetworkInspect(network1.Name) + Expect(err).To(BeNil()) + EqualNetwork(network2, network1) + }) + + It("create bridge with ipv6 enabled", func() { + network := types.Network{ + Driver: "bridge", + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(ContainSubstring(".0/24")) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv4 subnet", func() { + subnet := "10.100.0.0/24" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv6 subnet", func() { + subnet := "fd66::/64" + n, _ := types.ParseCIDR(subnet) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(ContainSubstring(".0/24")) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and ipv4+ipv6 subnet", func() { + subnet1 := "10.100.0.0/24" + n1, _ := types.ParseCIDR(subnet1) + subnet2 := "fd66::/64" + n2, _ := types.ParseCIDR(subnet2) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n1}, {Subnet: n2}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(2)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2)) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + }) + + It("create bridge with ipv6 enabled and two ipv4 subnets", func() { + subnet1 := "10.100.0.0/24" + n1, _ := types.ParseCIDR(subnet1) + subnet2 := "10.200.0.0/24" + n2, _ := types.ParseCIDR(subnet2) + + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n1}, {Subnet: n2}, + }, + IPv6Enabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(3)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet1)) + Expect(network1.Subnets[0].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + Expect(network1.Subnets[1].Subnet.String()).To(Equal(subnet2)) + Expect(network1.Subnets[1].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[1].LeaseRange).To(BeNil()) + Expect(network1.Subnets[2].Subnet.String()).To(ContainSubstring("::/64")) + Expect(network1.Subnets[2].Gateway).ToNot(BeNil()) + Expect(network1.Subnets[2].LeaseRange).To(BeNil()) + }) + + It("create bridge with subnet and gateway", func() { + subnet := "10.0.0.5/24" + n, _ := types.ParseCIDR(subnet) + gateway := "10.0.0.50" + g := net.ParseIP(gateway) + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, Gateway: g}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal("10.0.0.0/24")) + Expect(network1.Subnets[0].Gateway.String()).To(Equal(gateway)) + Expect(network1.Subnets[0].LeaseRange).To(BeNil()) + }) + + It("create bridge with subnet and gateway not in the same subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + gateway := "10.10.0.50" + g := net.ParseIP(gateway) + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, Gateway: g}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + }) + + It("create bridge with subnet and lease range", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + startIP := "10.0.0.10" + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + }}, + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP)) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + + endIP := "10.0.0.10" + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + EndIP: net.ParseIP(endIP), + }}, + }, + } + network1, err = libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(filepath.Join(networkConfDir, network1.Name+".json")).To(BeARegularFile()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP)) + + err = libpodNet.NetworkRemove(network1.Name) + Expect(err).To(BeNil()) + + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + EndIP: net.ParseIP(endIP), + }}, + }, + } + network1, err = libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(BeEmpty()) + Expect(network1.ID).ToNot(BeEmpty()) + Expect(network1.NetworkInterface).ToNot(BeEmpty()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).To(Equal(subnet)) + Expect(network1.Subnets[0].Gateway.String()).To(Equal("10.0.0.1")) + Expect(network1.Subnets[0].LeaseRange.StartIP.String()).To(Equal(startIP)) + Expect(network1.Subnets[0].LeaseRange.EndIP.String()).To(Equal(endIP)) + }) + + It("create bridge with subnet and invalid lease range", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + startIP := "10.0.1.2" + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + StartIP: net.ParseIP(startIP), + }}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + + endIP := "10.1.1.1" + network = types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: n, LeaseRange: &types.LeaseRange{ + EndIP: net.ParseIP(endIP), + }}, + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not in subnet")) + }) + + It("create bridge with broken subnet", func() { + network := types.Network{ + Driver: "bridge", + Subnets: []types.Subnet{ + {Subnet: types.IPNet{}}, + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet ip is nil")) + }) + + It("create network with name", func() { + name := "myname" + network := types.Network{ + Name: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal(name)) + Expect(network1.NetworkInterface).ToNot(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid name", func() { + name := "myname@some" + network := types.Network{ + Name: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with name", func() { + name := "myname" + network := types.Network{ + Name: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal(name)) + Expect(network1.NetworkInterface).ToNot(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid name", func() { + name := "myname@some" + network := types.Network{ + Name: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with interface name", func() { + name := "myname" + network := types.Network{ + NetworkInterface: name, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).ToNot(Equal(name)) + Expect(network1.NetworkInterface).To(Equal(name)) + Expect(network1.Driver).To(Equal("bridge")) + }) + + It("create network with invalid interface name", func() { + name := "myname@some" + network := types.Network{ + NetworkInterface: name, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + }) + + It("create network with ID should fail", func() { + id := "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121" + network := types.Network{ + ID: id, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("ID can not be set for network create")) + }) + + It("create bridge with dns", func() { + network := types.Network{ + Driver: "bridge", + DNSEnabled: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.DNSEnabled).To(BeTrue()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"dns_enabled": true`) + }) + + It("create bridge with internal", func() { + network := types.Network{ + Driver: "bridge", + Internal: true, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Subnets).To(HaveLen(1)) + Expect(network1.Subnets[0].Subnet.String()).ToNot(BeEmpty()) + Expect(network1.Subnets[0].Gateway).To(BeNil()) + Expect(network1.Internal).To(BeTrue()) + }) + + It("create network with labels", func() { + network := types.Network{ + Labels: map[string]string{ + "key": "value", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Labels).ToNot(BeNil()) + Expect(network1.Labels).To(ContainElement("value")) + }) + + It("create network with mtu option", func() { + network := types.Network{ + Options: map[string]string{ + "mtu": "1500", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Options).ToNot(BeNil()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"mtu": "1500"`) + Expect(network1.Options).To(HaveKeyWithValue("mtu", "1500")) + }) + + It("create network with invalid mtu option", func() { + network := types.Network{ + Options: map[string]string{ + "mtu": "abc", + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`)) + + network = types.Network{ + Options: map[string]string{ + "mtu": "-1", + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`mtu -1 is less than zero`)) + }) + + It("create network with vlan option", func() { + network := types.Network{ + Options: map[string]string{ + "vlan": "5", + }, + } + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Driver).To(Equal("bridge")) + Expect(network1.Options).ToNot(BeNil()) + path := filepath.Join(networkConfDir, network1.Name+".json") + Expect(path).To(BeARegularFile()) + grepInFile(path, `"vlan": "5"`) + Expect(network1.Options).To(HaveKeyWithValue("vlan", "5")) + }) + + It("create network with invalid vlan option", func() { + network := types.Network{ + Options: map[string]string{ + "vlan": "abc", + }, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`parsing "abc": invalid syntax`)) + + network = types.Network{ + Options: map[string]string{ + "vlan": "-1", + }, + } + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(`vlan ID -1 must be between 0 and 4094`)) + }) + + It("network create unsupported option", func() { + network := types.Network{Options: map[string]string{ + "someopt": "", + }} + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported network option someopt")) + }) + + It("network create unsupported driver", func() { + network := types.Network{ + Driver: "someDriver", + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported driver someDriver")) + }) + + It("network create internal and dns", func() { + network := types.Network{ + Driver: "bridge", + Internal: true, + DNSEnabled: true, + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("cannot set internal and dns enabled")) + }) + + It("network inspect partial ID", func() { + network := types.Network{Name: "net4"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.ID).To(HaveLen(64)) + + network2, err := libpodNet.NetworkInspect(network1.ID[:10]) + Expect(err).ToNot(HaveOccurred()) + EqualNetwork(network2, network1) + }) + + It("network create two with same name", func() { + network := types.Network{Name: "net"} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Name).To(Equal("net")) + network = types.Network{Name: "net"} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("network name net already used")) + }) + + It("remove default network config should fail", func() { + err := libpodNet.NetworkRemove("podman") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("default network podman cannot be removed")) + + network, err := libpodNet.NetworkInspect("podman") + Expect(err).To(BeNil()) + err = libpodNet.NetworkRemove(network.ID) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("default network podman cannot be removed")) + }) + + It("network create with same subnet", func() { + subnet := "10.0.0.0/24" + n, _ := types.ParseCIDR(subnet) + subnet2 := "10.10.0.0/24" + n2, _ := types.ParseCIDR(subnet2) + network := types.Network{Subnets: []types.Subnet{{Subnet: n}, {Subnet: n2}}} + network1, err := libpodNet.NetworkCreate(network) + Expect(err).To(BeNil()) + Expect(network1.Subnets).To(HaveLen(2)) + network = types.Network{Subnets: []types.Subnet{{Subnet: n}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.0.0.0/24 is already used on the host or by another config")) + network = types.Network{Subnets: []types.Subnet{{Subnet: n2}}} + _, err = libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("subnet 10.10.0.0/24 is already used on the host or by another config")) + }) + }) + + Context("network load valid existing ones", func() { + + BeforeEach(func() { + dir := "testfiles/valid" + files, err := ioutil.ReadDir(dir) + if err != nil { + Fail("Failed to read test directory") + } + for _, file := range files { + filename := file.Name() + data, err := ioutil.ReadFile(filepath.Join(dir, filename)) + if err != nil { + Fail("Failed to copy test files") + } + err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700) + if err != nil { + Fail("Failed to copy test files") + } + } + }) + + It("load networks from disk", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + // test the we do not show logrus warnings/errors + logString := logBuffer.String() + Expect(logString).To(BeEmpty()) + }) + + It("change network struct fields should not affect network struct in the backend", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + + nets[0].Name = "myname" + nets, err = libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(7)) + Expect(nets).ToNot(ContainElement(HaveNetworkName("myname"))) + + network, err := libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + network.NetworkInterface = "abc" + + network, err = libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + Expect(network.NetworkInterface).ToNot(Equal("abc")) + }) + + It("bridge network", func() { + network, err := libpodNet.NetworkInspect("bridge") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("bridge")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman9")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.8.0/24")) + Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.8.1")) + Expect(network.Subnets[0].LeaseRange).ToNot(BeNil()) + Expect(network.Subnets[0].LeaseRange.StartIP.String()).To(Equal("10.89.8.20")) + Expect(network.Subnets[0].LeaseRange.EndIP.String()).To(Equal("10.89.8.50")) + Expect(network.Internal).To(BeFalse()) + }) + + It("internal network", func() { + network, err := libpodNet.NetworkInspect("internal") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("internal")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman8")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.7.0/24")) + Expect(network.Subnets[0].Gateway).To(BeNil()) + Expect(network.Internal).To(BeTrue()) + }) + + It("bridge network with mtu", func() { + network, err := libpodNet.NetworkInspect("mtu") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("mtu")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman13")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Subnets[0].Subnet.String()).To(Equal("10.89.11.0/24")) + Expect(network.Subnets[0].Gateway.String()).To(Equal("10.89.11.1")) + Expect(network.Internal).To(BeFalse()) + Expect(network.Options).To(HaveLen(1)) + Expect(network.Options).To(HaveKeyWithValue("mtu", "1500")) + }) + + It("bridge network with vlan", func() { + network, err := libpodNet.NetworkInspect("vlan") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("vlan")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman14")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Options).To(HaveLen(1)) + Expect(network.Options).To(HaveKeyWithValue("vlan", "5")) + }) + + It("bridge network with labels", func() { + network, err := libpodNet.NetworkInspect("label") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("label")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman15")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(1)) + Expect(network.Labels).To(HaveLen(1)) + Expect(network.Labels).To(HaveKeyWithValue("mykey", "value")) + }) + + It("dual stack network", func() { + network, err := libpodNet.NetworkInspect("dualstack") + Expect(err).To(BeNil()) + Expect(network.Name).To(Equal("dualstack")) + Expect(network.ID).To(HaveLen(64)) + Expect(network.NetworkInterface).To(Equal("podman21")) + Expect(network.Driver).To(Equal("bridge")) + Expect(network.Subnets).To(HaveLen(2)) + + sub1, _ := types.ParseCIDR("fd10:88:a::/64") + sub2, _ := types.ParseCIDR("10.89.19.0/24") + Expect(network.Subnets).To(ContainElements( + types.Subnet{Subnet: sub1, Gateway: net.ParseIP("fd10:88:a::1")}, + types.Subnet{Subnet: sub2, Gateway: net.ParseIP("10.89.19.10").To4()}, + )) + }) + + It("network list with filters (name)", func() { + filters := map[string][]string{ + "name": {"internal", "bridge"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (partial name)", func() { + filters := map[string][]string{ + "name": {"inte", "bri"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (partial id)", func() { + filters := map[string][]string{ + "id": {"3bed2cb3a3acf7b6a8ef408420", "17f29b073143d8cd97b5bbe492bde"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(2)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"))) + }) + + It("network list with filters (driver)", func() { + filters := map[string][]string{ + "driver": {"bridge"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(7)) + Expect(networks).To(ConsistOf(HaveNetworkName("internal"), HaveNetworkName("bridge"), + HaveNetworkName("mtu"), HaveNetworkName("vlan"), HaveNetworkName("podman"), + HaveNetworkName("label"), HaveNetworkName("dualstack"))) + }) + + It("network list with filters (label)", func() { + filters := map[string][]string{ + "label": {"mykey"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + + filters = map[string][]string{ + "label": {"mykey=value"}, + } + filterFuncs, err = util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err = libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + }) + + It("network list with filters", func() { + filters := map[string][]string{ + "driver": {"bridge"}, + "label": {"mykey"}, + } + filterFuncs, err := util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + Expect(filterFuncs).To(HaveLen(2)) + + networks, err := libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(1)) + Expect(networks).To(ConsistOf(HaveNetworkName("label"))) + + filters = map[string][]string{ + "driver": {"macvlan"}, + "label": {"mykey"}, + } + filterFuncs, err = util.GenerateNetworkFilters(filters) + Expect(err).To(BeNil()) + + networks, err = libpodNet.NetworkList(filterFuncs...) + Expect(err).To(BeNil()) + Expect(networks).To(HaveLen(0)) + }) + + It("create bridge network with used interface name", func() { + network := types.Network{ + NetworkInterface: "podman9", + } + _, err := libpodNet.NetworkCreate(network) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("bridge name podman9 already in use")) + }) + }) + + Context("network load invalid existing ones", func() { + + BeforeEach(func() { + dir := "testfiles/invalid" + files, err := ioutil.ReadDir(dir) + if err != nil { + Fail("Failed to read test directory") + } + for _, file := range files { + filename := file.Name() + data, err := ioutil.ReadFile(filepath.Join(dir, filename)) + if err != nil { + Fail("Failed to copy test files") + } + err = ioutil.WriteFile(filepath.Join(networkConfDir, filename), data, 0700) + if err != nil { + Fail("Failed to copy test files") + } + } + }) + + It("load invalid networks from disk", func() { + nets, err := libpodNet.NetworkList() + Expect(err).To(BeNil()) + Expect(nets).To(HaveLen(1)) + logString := logBuffer.String() + Expect(logString).To(ContainSubstring("Error reading network config file \\\"%s/broken.json\\\": unexpected EOF", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid name.json\\\" has invalid name: \\\"invalid name\\\", skipping: names must match [a-zA-Z0-9][a-zA-Z0-9_.-]*: invalid argument", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config name \\\"name_miss\\\" does not match file name \\\"name_missmatch.json\\\", skipping")) + Expect(logString).To(ContainSubstring("Network config \\\"%s/wrongID.json\\\" could not be parsed, skipping: invalid network ID \\\"someID\\\"", networkConfDir)) + Expect(logString).To(ContainSubstring("Network config \\\"%s/invalid_gateway.json\\\" could not be parsed, skipping: gateway 10.89.100.1 not in subnet 10.89.9.0/24", networkConfDir)) + }) + + }) + +}) + +func grepInFile(path string, match string) { + data, err := ioutil.ReadFile(path) + ExpectWithOffset(1, err).To(BeNil()) + ExpectWithOffset(1, string(data)).To(ContainSubstring(match)) +} + +// HaveNetworkName is a custom GomegaMatcher to match a network name +func HaveNetworkName(name string) gomegaTypes.GomegaMatcher { + return WithTransform(func(e types.Network) string { + return e.Name + }, Equal(name)) +} + +// EqualNetwork must be used because comparing the time with deep equal does not work +func EqualNetwork(net1, net2 types.Network) { + ExpectWithOffset(1, net1.Created.Equal(net2.Created)).To(BeTrue(), "net1 created: %v is not equal net2 created: %v", net1.Created, net2.Created) + net1.Created = time.Time{} + net2.Created = time.Time{} + ExpectWithOffset(1, net1).To(Equal(net2)) +} diff --git a/libpod/network/netavark/const.go b/libpod/network/netavark/const.go new file mode 100644 index 000000000..9709315c6 --- /dev/null +++ b/libpod/network/netavark/const.go @@ -0,0 +1,5 @@ +// +build linux + +package netavark + +const defaultBridgeName = "podman" diff --git a/libpod/network/netavark/exec.go b/libpod/network/netavark/exec.go new file mode 100644 index 000000000..700df9a69 --- /dev/null +++ b/libpod/network/netavark/exec.go @@ -0,0 +1,116 @@ +package netavark + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "strconv" + + "github.com/sirupsen/logrus" +) + +type netavarkError struct { + exitCode int + // Set the json key to "error" so we can directly unmarshal into this struct + Msg string `json:"error"` + err error +} + +func (e *netavarkError) Error() string { + ec := "" + // only add the exit code the the error message if we have at least info log level + // the normal user does not need to care about the number + if e.exitCode > 0 && logrus.IsLevelEnabled(logrus.InfoLevel) { + ec = " (exit code " + strconv.Itoa(e.exitCode) + ")" + } + msg := "netavark" + ec + if len(msg) > 0 { + msg += ": " + e.Msg + } + if e.err != nil { + msg += ": " + e.err.Error() + } + return msg +} + +func (e *netavarkError) Unwrap() error { + return e.err +} + +func newNetavarkError(msg string, err error) error { + return &netavarkError{ + Msg: msg, + err: err, + } +} + +// execNetavark will execute netavark with the following arguments +// It takes the path to the binary, the list of args and an interface which is +// marshaled to json and send via stdin to netavark. The result interface is +// used to marshal the netavark output into it. This can be nil. +// All errors return by this function should be of the type netavarkError +// to provide a helpful error message. +func execNetavark(binary string, args []string, stdin, result interface{}) error { + stdinR, stdinW, err := os.Pipe() + if err != nil { + return newNetavarkError("failed to create stdin pipe", err) + } + defer stdinR.Close() + + stdoutR, stdoutW, err := os.Pipe() + if err != nil { + return newNetavarkError("failed to create stdout pipe", err) + } + defer stdoutR.Close() + defer stdoutW.Close() + + cmd := exec.Command(binary, args...) + // connect the pipes to stdin and stdout + cmd.Stdin = stdinR + cmd.Stdout = stdoutW + // connect stderr to the podman stderr for logging + cmd.Stderr = os.Stderr + // set the netavark log level to the same as the podman + cmd.Env = append(os.Environ(), "RUST_LOG="+logrus.GetLevel().String()) + // if we run with debug log level lets also set RUST_BACKTRACE=1 so we can get the full stack trace in case of panics + if logrus.IsLevelEnabled(logrus.DebugLevel) { + cmd.Env = append(cmd.Env, "RUST_BACKTRACE=1") + } + + err = cmd.Start() + if err != nil { + return newNetavarkError("failed to start process", err) + } + err = json.NewEncoder(stdinW).Encode(stdin) + stdinW.Close() + if err != nil { + return newNetavarkError("failed to encode stdin data", err) + } + + dec := json.NewDecoder(stdoutR) + + err = cmd.Wait() + stdoutW.Close() + if err != nil { + exitError := &exec.ExitError{} + if errors.As(err, &exitError) { + ne := &netavarkError{} + // lets disallow unknown fields to make sure we do not get some unexpected stuff + dec.DisallowUnknownFields() + // this will unmarshal the error message into the error struct + ne.err = dec.Decode(ne) + ne.exitCode = exitError.ExitCode() + return ne + } + return newNetavarkError("unexpected failure during execution", err) + } + + if result != nil { + err = dec.Decode(result) + if err != nil { + return newNetavarkError("failed to decode result", err) + } + } + return nil +} diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go new file mode 100644 index 000000000..b24f880e0 --- /dev/null +++ b/libpod/network/netavark/netavark_suite_test.go @@ -0,0 +1,37 @@ +// +build linux + +package netavark_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/podman/v3/libpod/network/netavark" + "github.com/containers/podman/v3/libpod/network/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestNetavark(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Netavark Suite") +} + +var netavarkBinary string + +func init() { + netavarkBinary = os.Getenv("NETAVARK_BINARY") + if netavarkBinary == "" { + netavarkBinary = "/usr/libexec/podman/netavark" + } +} + +func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, error) { + return netavark.NewNetworkInterface(netavark.InitConfig{ + NetworkConfigDir: confDir, + IsMachine: machine, + NetavarkBinary: netavarkBinary, + LockFile: filepath.Join(confDir, "netavark.lock"), + }) +} diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go new file mode 100644 index 000000000..aa7dc8183 --- /dev/null +++ b/libpod/network/netavark/network.go @@ -0,0 +1,275 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/storage/pkg/lockfile" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type netavarkNetwork struct { + // networkConfigDir is directory where the network config files are stored. + networkConfigDir string + + // netavarkBinary is the path to the netavark binary. + netavarkBinary string + + // defaultNetwork is the name for the default network. + defaultNetwork string + // defaultSubnet is the default subnet for the default network. + defaultSubnet types.IPNet + + // isMachine describes whenever podman runs in a podman machine environment. + isMachine bool + + // lock is a internal lock for critical operations + lock lockfile.Locker + + // modTime is the timestamp when the config dir was modified + modTime time.Time + + // networks is a map with loaded networks, the key is the network name + networks map[string]*types.Network +} + +type InitConfig struct { + // NetworkConfigDir is directory where the network config files are stored. + NetworkConfigDir string + + // NetavarkBinary is the path to the netavark binary. + NetavarkBinary string + + // DefaultNetwork is the name for the default network. + DefaultNetwork string + // DefaultSubnet is the default subnet for the default network. + DefaultSubnet string + + // IsMachine describes whenever podman runs in a podman machine environment. + IsMachine bool + + // LockFile is the path to lock file. + LockFile string +} + +// NewNetworkInterface creates the ContainerNetwork interface for the netavark backend. +// Note: The networks are not loaded from disk until a method is called. +func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) { + // TODO: consider using a shared memory lock + lock, err := lockfile.GetLockfile(conf.LockFile) + if err != nil { + return nil, err + } + + defaultNetworkName := conf.DefaultNetwork + if defaultNetworkName == "" { + defaultNetworkName = types.DefaultNetworkName + } + + defaultSubnet := conf.DefaultSubnet + if defaultSubnet == "" { + defaultSubnet = types.DefaultSubnet + } + defaultNet, err := types.ParseCIDR(defaultSubnet) + if err != nil { + return nil, errors.Wrap(err, "failed to parse default subnet") + } + + n := &netavarkNetwork{ + networkConfigDir: conf.NetworkConfigDir, + netavarkBinary: conf.NetavarkBinary, + defaultNetwork: defaultNetworkName, + defaultSubnet: defaultNet, + isMachine: conf.IsMachine, + lock: lock, + } + + return n, nil +} + +// Drivers will return the list of supported network drivers +// for this interface. +func (n *netavarkNetwork) Drivers() []string { + return []string{types.BridgeNetworkDriver} +} + +func (n *netavarkNetwork) loadNetworks() error { + // check the mod time of the config dir + f, err := os.Stat(n.networkConfigDir) + if err != nil { + return err + } + modTime := f.ModTime() + + // skip loading networks if they are already loaded and + // if the config dir was not modified since the last call + if n.networks != nil && modTime.Equal(n.modTime) { + return nil + } + // make sure the remove all networks before we reload them + n.networks = nil + n.modTime = modTime + + files, err := ioutil.ReadDir(n.networkConfigDir) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + networks := make(map[string]*types.Network, len(files)) + for _, f := range files { + if f.IsDir() { + continue + } + if filepath.Ext(f.Name()) != ".json" { + continue + } + + path := filepath.Join(n.networkConfigDir, f.Name()) + file, err := os.Open(path) + if err != nil { + // do not log ENOENT errors + if !errors.Is(err, os.ErrNotExist) { + logrus.Warnf("Error loading network config file %q: %v", path, err) + } + continue + } + network := new(types.Network) + err = json.NewDecoder(file).Decode(network) + if err != nil { + logrus.Warnf("Error reading network config file %q: %v", path, err) + continue + } + + // check that the filename matches the network name + if network.Name+".json" != f.Name() { + logrus.Warnf("Network config name %q does not match file name %q, skipping", network.Name, f.Name()) + continue + } + + if !define.NameRegex.MatchString(network.Name) { + logrus.Warnf("Network config %q has invalid name: %q, skipping: %v", path, network.Name, define.RegexError) + continue + } + + err = parseNetwork(network) + if err != nil { + logrus.Warnf("Network config %q could not be parsed, skipping: %v", path, err) + continue + } + + logrus.Debugf("Successfully loaded network %s: %v", network.Name, network) + networks[network.Name] = network + } + + // create the default network in memory if it did not exists on disk + if networks[n.defaultNetwork] == nil { + networkInfo, err := n.createDefaultNetwork() + if err != nil { + return errors.Wrapf(err, "failed to create default network %s", n.defaultNetwork) + } + networks[n.defaultNetwork] = networkInfo + } + logrus.Debugf("Successfully loaded %d networks", len(networks)) + n.networks = networks + return nil +} + +func parseNetwork(network *types.Network) error { + if network.Labels == nil { + network.Labels = map[string]string{} + } + if network.Options == nil { + network.Options = map[string]string{} + } + if network.IPAMOptions == nil { + network.IPAMOptions = map[string]string{} + } + + if len(network.ID) != 64 { + return errors.Errorf("invalid network ID %q", network.ID) + } + + return util.ValidateSubnets(network, nil) +} + +func (n *netavarkNetwork) createDefaultNetwork() (*types.Network, error) { + net := types.Network{ + Name: n.defaultNetwork, + NetworkInterface: defaultBridgeName + "0", + // Important do not change this ID + ID: "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + Driver: types.BridgeNetworkDriver, + Subnets: []types.Subnet{ + {Subnet: n.defaultSubnet}, + }, + } + return n.networkCreate(net, true) +} + +// getNetwork will lookup a network by name or ID. It returns an +// error when no network was found or when more than one network +// with the given (partial) ID exists. +// getNetwork will read from the networks map, therefore the caller +// must ensure that n.lock is locked before using it. +func (n *netavarkNetwork) getNetwork(nameOrID string) (*types.Network, error) { + // fast path check the map key, this will only work for names + if val, ok := n.networks[nameOrID]; ok { + return val, nil + } + // If there was no match we might got a full or partial ID. + var net *types.Network + for _, val := range n.networks { + // This should not happen because we already looked up the map by name but check anyway. + if val.Name == nameOrID { + return val, nil + } + + if strings.HasPrefix(val.ID, nameOrID) { + if net != nil { + return nil, errors.Errorf("more than one result for network ID %s", nameOrID) + } + net = val + } + } + if net != nil { + return net, nil + } + return nil, errors.Wrapf(define.ErrNoSuchNetwork, "unable to find network with name or ID %s", nameOrID) +} + +// Implement the NetUtil interface for easy code sharing with other network interfaces. + +// ForEach call the given function for each network +func (n *netavarkNetwork) ForEach(run func(types.Network)) { + for _, val := range n.networks { + run(*val) + } +} + +// Len return the number of networks +func (n *netavarkNetwork) Len() int { + return len(n.networks) +} + +// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number. +func (n *netavarkNetwork) DefaultInterfaceName() string { + return defaultBridgeName +} + +func (n *netavarkNetwork) Network(nameOrID string) (*types.Network, error) { + network, err := n.getNetwork(nameOrID) + if err != nil { + return nil, err + } + return network, nil +} diff --git a/libpod/network/netavark/run.go b/libpod/network/netavark/run.go new file mode 100644 index 000000000..bd26e957e --- /dev/null +++ b/libpod/network/netavark/run.go @@ -0,0 +1,90 @@ +// +build linux + +package netavark + +import ( + "encoding/json" + "fmt" + + "github.com/containers/podman/v3/libpod/network/internal/util" + "github.com/containers/podman/v3/libpod/network/types" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type netavarkOptions struct { + types.NetworkOptions + Networks map[string]*types.Network `json:"network_info"` +} + +// Setup will setup the container network namespace. It returns +// a map of StatusBlocks, the key is the network name. +func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions) (map[string]types.StatusBlock, error) { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return nil, err + } + + err = util.ValidateSetupOptions(n, namespacePath, options) + if err != nil { + return nil, err + } + + // TODO IP address assignment + + netavarkOpts, err := n.convertNetOpts(options.NetworkOptions) + if err != nil { + return nil, errors.Wrap(err, "failed to convert net opts") + } + + b, err := json.Marshal(&netavarkOpts) + if err != nil { + return nil, err + } + fmt.Println(string(b)) + + result := map[string]types.StatusBlock{} + err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, result) + + if len(result) != len(options.Networks) { + logrus.Errorf("unexpected netavark result: %v", result) + return nil, fmt.Errorf("unexpected netavark result length, want (%d), got (%d) networks", len(options.Networks), len(result)) + } + + return result, err +} + +// Teardown will teardown the container network namespace. +func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownOptions) error { + n.lock.Lock() + defer n.lock.Unlock() + err := n.loadNetworks() + if err != nil { + return err + } + + netavarkOpts, err := n.convertNetOpts(options.NetworkOptions) + if err != nil { + return errors.Wrap(err, "failed to convert net opts") + } + + return execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil) +} + +func (n *netavarkNetwork) convertNetOpts(opts types.NetworkOptions) (*netavarkOptions, error) { + netavarkOptions := netavarkOptions{ + NetworkOptions: opts, + Networks: make(map[string]*types.Network, len(opts.Networks)), + } + + for network := range opts.Networks { + net, err := n.getNetwork(network) + if err != nil { + return nil, err + } + netavarkOptions.Networks[network] = net + } + return &netavarkOptions, nil +} diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go new file mode 100644 index 000000000..6c3b4d970 --- /dev/null +++ b/libpod/network/netavark/run_test.go @@ -0,0 +1,363 @@ +// +build linux + +package netavark_test + +// The tests have to be run as root. +// For each test there will be two network namespaces created, +// netNSTest and netNSContainer. Each test must be run inside +// netNSTest to prevent leakage in the host netns, therefore +// it should use the following structure: +// It("test name", func() { +// runTest(func() { +// // add test logic here +// }) +// }) + +import ( + "bytes" + "fmt" + "io/ioutil" + "net" + "os" + "strconv" + "sync" + "time" + + "github.com/containernetworking/plugins/pkg/ns" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/containers/podman/v3/pkg/netns" + "github.com/containers/podman/v3/pkg/rootless" + "github.com/containers/storage/pkg/stringid" +) + +var _ = Describe("run netavark", func() { + var ( + libpodNet types.ContainerNetwork + confDir string + logBuffer bytes.Buffer + netNSTest ns.NetNS + netNSContainer ns.NetNS + ) + + // runTest is a helper function to run a test. It ensures that each test + // is run in its own netns. It also creates a mountns to mount a tmpfs to /var/lib/cni. + runTest := func(run func()) { + netNSTest.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + // we have to setup the loopback adapter in this netns to use port forwarding + link, err := netlink.LinkByName("lo") + Expect(err).To(BeNil(), "Failed to get loopback adapter") + err = netlink.LinkSetUp(link) + Expect(err).To(BeNil(), "Failed to set loopback adapter up") + run() + return nil + }) + } + + BeforeEach(func() { + logrus.SetLevel(logrus.TraceLevel) + logrus.SetFormatter(&logrus.TextFormatter{DisableQuote: true}) + // The tests need root privileges. + // Technically we could work around that by using user namespaces and + // the rootless cni code but this is to much work to get it right for a unit test. + if rootless.IsRootless() { + Skip("this test needs to be run as root") + } + + var err error + confDir, err = ioutil.TempDir("", "podman_netavark_test") + if err != nil { + Fail("Failed to create tmpdir") + } + logBuffer = bytes.Buffer{} + logrus.SetOutput(&logBuffer) + + netNSTest, err = netns.NewNS() + if err != nil { + Fail("Failed to create netns") + } + + netNSContainer, err = netns.NewNS() + if err != nil { + Fail("Failed to create netns") + } + }) + + JustBeforeEach(func() { + var err error + libpodNet, err = getNetworkInterface(confDir, false) + if err != nil { + Fail("Failed to create NewCNINetworkInterface") + } + }) + + AfterEach(func() { + logrus.SetFormatter(&logrus.TextFormatter{}) + logrus.SetLevel(logrus.InfoLevel) + os.RemoveAll(confDir) + + netns.UnmountNS(netNSTest) + netNSTest.Close() + + netns.UnmountNS(netNSContainer) + netNSContainer.Close() + + fmt.Println(logBuffer.String()) + }) + + It("test basic setup", func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + opts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: "someID", + ContainerName: "someName", + Networks: map[string]types.PerNetworkOptions{ + defNet: { + InterfaceName: intName, + }, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), opts) + Expect(err).ToNot(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + ip := res[defNet].Interfaces[intName].Networks[0].Subnet.IP + Expect(ip.String()).To(ContainSubstring("10.88.0.")) + gw := res[defNet].Interfaces[intName].Networks[0].Gateway + Expect(gw.String()).To(Equal("10.88.0.1")) + macAddress := res[defNet].Interfaces[intName].MacAddress + Expect(macAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + + // check in the container namespace if the settings are applied + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + i, err := net.InterfaceByName(intName) + Expect(err).To(BeNil()) + Expect(i.Name).To(Equal(intName)) + Expect(i.HardwareAddr).To(Equal(macAddress)) + addrs, err := i.Addrs() + Expect(err).To(BeNil()) + subnet := &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(16, 32), + } + Expect(addrs).To(ContainElements(subnet)) + + // check loopback adapter + i, err = net.InterfaceByName("lo") + Expect(err).To(BeNil()) + Expect(i.Name).To(Equal("lo")) + Expect(i.Flags & net.FlagLoopback).To(Equal(net.FlagLoopback)) + Expect(i.Flags&net.FlagUp).To(Equal(net.FlagUp), "Loopback adapter should be up") + return nil + }) + Expect(err).To(BeNil()) + + // default bridge name + bridgeName := "podman0" + // check settings on the host side + i, err := net.InterfaceByName(bridgeName) + Expect(err).ToNot(HaveOccurred()) + Expect(i.Name).To(Equal(bridgeName)) + addrs, err := i.Addrs() + Expect(err).ToNot(HaveOccurred()) + // test that the gateway ip is assigned to the interface + subnet := &net.IPNet{ + IP: gw, + Mask: net.CIDRMask(16, 32), + } + Expect(addrs).To(ContainElements(subnet)) + + wg := &sync.WaitGroup{} + expected := stringid.GenerateNonCryptoID() + // now check ip connectivity + err = netNSContainer.Do(func(_ ns.NetNS) error { + runNetListener(wg, "tcp", "0.0.0.0", 5000, expected) + return nil + }) + Expect(err).ToNot(HaveOccurred()) + + conn, err := net.Dial("tcp", ip.String()+":5000") + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(expected)) + Expect(err).To(BeNil()) + conn.Close() + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(opts)) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + for _, proto := range []string{"tcp", "udp"} { + // copy proto to extra var to keep correct references in the goroutines + protocol := proto + It("run with exposed ports protocol "+protocol, func() { + runTest(func() { + testdata := stringid.GenerateNonCryptoID() + defNet := types.DefaultNetworkName + intName := "eth0" + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + PortMappings: []types.PortMapping{{ + Protocol: protocol, + HostIP: "127.0.0.1", + HostPort: 5000, + ContainerPort: 5000, + }}, + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).To(BeNil()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + Expect(res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String()).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + var wg sync.WaitGroup + wg.Add(1) + // start a listener in the container ns + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + runNetListener(&wg, protocol, "0.0.0.0", 5000, testdata) + return nil + }) + Expect(err).To(BeNil()) + + conn, err := net.Dial(protocol, "127.0.0.1:5000") + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(testdata)) + Expect(err).To(BeNil()) + conn.Close() + + // wait for the listener to finish + wg.Wait() + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).To(BeNil()) + }) + }) + + It("run with range ports protocol "+protocol, func() { + runTest(func() { + defNet := types.DefaultNetworkName + intName := "eth0" + setupOpts := types.SetupOptions{ + NetworkOptions: types.NetworkOptions{ + ContainerID: stringid.GenerateNonCryptoID(), + PortMappings: []types.PortMapping{{ + Protocol: protocol, + HostIP: "127.0.0.1", + HostPort: 5001, + ContainerPort: 5000, + Range: 3, + }}, + Networks: map[string]types.PerNetworkOptions{ + defNet: {InterfaceName: intName}, + }, + }, + } + res, err := libpodNet.Setup(netNSContainer.Path(), setupOpts) + Expect(err).To(BeNil()) + Expect(res).To(HaveLen(1)) + Expect(res).To(HaveKey(defNet)) + Expect(res[defNet].Interfaces).To(HaveKey(intName)) + Expect(res[defNet].Interfaces[intName].Networks).To(HaveLen(1)) + containerIP := res[defNet].Interfaces[intName].Networks[0].Subnet.IP.String() + Expect(containerIP).To(ContainSubstring("10.88.0.")) + Expect(res[defNet].Interfaces[intName].MacAddress).To(HaveLen(6)) + // default network has no dns + Expect(res[defNet].DNSServerIPs).To(BeEmpty()) + Expect(res[defNet].DNSSearchDomains).To(BeEmpty()) + + // loop over all ports + for p := 5001; p < 5004; p++ { + port := p + var wg sync.WaitGroup + wg.Add(1) + testdata := stringid.GenerateNonCryptoID() + // start a listener in the container ns + err = netNSContainer.Do(func(_ ns.NetNS) error { + defer GinkgoRecover() + runNetListener(&wg, protocol, containerIP, port-1, testdata) + return nil + }) + Expect(err).To(BeNil()) + + conn, err := net.Dial(protocol, net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + Expect(err).To(BeNil()) + _, err = conn.Write([]byte(testdata)) + Expect(err).To(BeNil()) + conn.Close() + + // wait for the listener to finish + wg.Wait() + } + + err = libpodNet.Teardown(netNSContainer.Path(), types.TeardownOptions(setupOpts)) + Expect(err).To(BeNil()) + }) + }) + } + +}) + +func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) { + switch protocol { + case "tcp": + ln, err := net.Listen(protocol, net.JoinHostPort(ip, strconv.Itoa(port))) + Expect(err).To(BeNil()) + // make sure to read in a separate goroutine to not block + go func() { + defer GinkgoRecover() + defer wg.Done() + defer ln.Close() + conn, err := ln.Accept() + Expect(err).To(BeNil()) + defer conn.Close() + conn.SetDeadline(time.Now().Add(1 * time.Second)) + data, err := ioutil.ReadAll(conn) + Expect(err).To(BeNil()) + Expect(string(data)).To(Equal(expectedData)) + }() + case "udp": + conn, err := net.ListenUDP("udp", &net.UDPAddr{ + IP: net.ParseIP(ip), + Port: port, + }) + Expect(err).To(BeNil()) + conn.SetDeadline(time.Now().Add(1 * time.Second)) + go func() { + defer GinkgoRecover() + defer wg.Done() + defer conn.Close() + data := make([]byte, len(expectedData)) + i, err := conn.Read(data) + Expect(err).To(BeNil()) + Expect(i).To(Equal(len(expectedData))) + Expect(string(data)).To(Equal(expectedData)) + }() + default: + Fail("unsupported protocol") + } +} diff --git a/libpod/network/netavark/testfiles/invalid/broken.json b/libpod/network/netavark/testfiles/invalid/broken.json new file mode 100644 index 000000000..8968ddc73 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/broken.json @@ -0,0 +1,16 @@ +{ + "name": "bridge", + "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1", + "lease_range": { + "start_ip": "10.89.8.20", + "end_ip": "10.89.8.50" + } + } + ], diff --git a/libpod/network/netavark/testfiles/invalid/invalid name.json b/libpod/network/netavark/testfiles/invalid/invalid name.json new file mode 100644 index 000000000..02b441279 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/invalid name.json @@ -0,0 +1,19 @@ +{ + "name": "invalid name", + "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/invalid_gateway.json b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json new file mode 100644 index 000000000..6e3a83156 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/invalid_gateway.json @@ -0,0 +1,19 @@ +{ + "name": "invalid_gateway", + "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.9.0/24", + "gateway": "10.89.100.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/name_missmatch.json b/libpod/network/netavark/testfiles/invalid/name_missmatch.json new file mode 100644 index 000000000..a3142d8bb --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/name_missmatch.json @@ -0,0 +1,19 @@ +{ + "name": "name_miss", + "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", + "driver": "bridge", + "network_interface": "podman8", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.7.0/24", + "gateway": "10.89.7.1" + } + ], + "ipv6_enabled": false, + "internal": true, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/invalid/wrongID.json b/libpod/network/netavark/testfiles/invalid/wrongID.json new file mode 100644 index 000000000..7c1446306 --- /dev/null +++ b/libpod/network/netavark/testfiles/invalid/wrongID.json @@ -0,0 +1,19 @@ +{ + "name": "wrongID", + "id": "someID", + "driver": "bridge", + "network_interface": "podman1", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.0.0/24", + "gateway": "10.89.0.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/bridge.json b/libpod/network/netavark/testfiles/valid/bridge.json new file mode 100644 index 000000000..f4ec82188 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/bridge.json @@ -0,0 +1,23 @@ +{ + "name": "bridge", + "id": "17f29b073143d8cd97b5bbe492bdeffec1c5fee55cc1fe2112c8b9335f8b6121", + "driver": "bridge", + "network_interface": "podman9", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.8.0/24", + "gateway": "10.89.8.1", + "lease_range": { + "start_ip": "10.89.8.20", + "end_ip": "10.89.8.50" + } + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/dualstack.json b/libpod/network/netavark/testfiles/valid/dualstack.json new file mode 100644 index 000000000..bb4168f3a --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/dualstack.json @@ -0,0 +1,23 @@ +{ + "name": "dualstack", + "id": "6839f44f0fd01c5c5830856b66a1d7ce46842dd8798be0addf96f7255ce9f889", + "driver": "bridge", + "network_interface": "podman21", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "fd10:88:a::/64", + "gateway": "fd10:88:a::1" + }, + { + "subnet": "10.89.19.0/24", + "gateway": "10.89.19.10" + } + ], + "ipv6_enabled": true, + "internal": false, + "dns_enabled": true, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/internal.json b/libpod/network/netavark/testfiles/valid/internal.json new file mode 100644 index 000000000..3ccdd3889 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/internal.json @@ -0,0 +1,18 @@ +{ + "name": "internal", + "id": "3bed2cb3a3acf7b6a8ef408420cc682d5520e26976d354254f528c965612054f", + "driver": "bridge", + "network_interface": "podman8", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.7.0/24" + } + ], + "ipv6_enabled": false, + "internal": true, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/label.json b/libpod/network/netavark/testfiles/valid/label.json new file mode 100644 index 000000000..c4ed637ec --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/label.json @@ -0,0 +1,22 @@ +{ + "name": "label", + "id": "1aca80e8b55c802f7b43740da2990e1b5735bbb323d93eb5ebda8395b04025e2", + "driver": "bridge", + "network_interface": "podman15", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.13.0/24", + "gateway": "10.89.13.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "labels": { + "mykey": "value" + }, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/mtu.json b/libpod/network/netavark/testfiles/valid/mtu.json new file mode 100644 index 000000000..53fa4c9bc --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/mtu.json @@ -0,0 +1,22 @@ +{ + "name": "mtu", + "id": "49be6e401e7f8b9844afb969dcbc96e78205ed86ec1e5a46150bd4ab4fdd5686", + "driver": "bridge", + "network_interface": "podman13", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.11.0/24", + "gateway": "10.89.11.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "options": { + "mtu": "1500" + }, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/podman.json b/libpod/network/netavark/testfiles/valid/podman.json new file mode 100644 index 000000000..19acddc83 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/podman.json @@ -0,0 +1,19 @@ +{ + "name": "podman", + "id": "2f259bab93aaaaa2542ba43ef33eb990d0999ee1b9924b557b7be53c0b7a1bb9", + "driver": "bridge", + "network_interface": "podman0", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.88.0.0/16", + "gateway": "10.88.0.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": false, + "ipam_options": { + "driver": "host-local" + } +} diff --git a/libpod/network/netavark/testfiles/valid/vlan.json b/libpod/network/netavark/testfiles/valid/vlan.json new file mode 100644 index 000000000..30c88ec49 --- /dev/null +++ b/libpod/network/netavark/testfiles/valid/vlan.json @@ -0,0 +1,22 @@ +{ + "name": "vlan", + "id": "c3b258168c41c0bce97616716bef315eeed33eb1142904bfe7f32eb392c7cf80", + "driver": "bridge", + "network_interface": "podman14", + "created": "2021-10-06T18:50:54.25770461+02:00", + "subnets": [ + { + "subnet": "10.89.12.0/24", + "gateway": "10.89.12.1" + } + ], + "ipv6_enabled": false, + "internal": false, + "dns_enabled": true, + "options": { + "vlan": "5" + }, + "ipam_options": { + "driver": "host-local" + } +} -- cgit v1.2.3-54-g00ecf From 4febe557692aeec8ca9d9b9cdc732772ba7d5876 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 20 Oct 2021 15:55:22 +0200 Subject: netavark IPAM assignment Add a new boltdb to handle IPAM assignment. The db structure is the following: 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. The db should be stored on a tmpfs to ensure we always have a clean state after a reboot. Signed-off-by: Paul Holzinger --- libpod/network/cni/cni_conversion.go | 2 +- libpod/network/internal/util/bridge.go | 56 ++-- libpod/network/internal/util/ip.go | 8 - libpod/network/internal/util/validate.go | 6 +- libpod/network/netavark/ipam.go | 368 +++++++++++++++++++++ libpod/network/netavark/ipam_test.go | 433 +++++++++++++++++++++++++ libpod/network/netavark/netavark_suite_test.go | 1 + libpod/network/netavark/network.go | 26 ++ libpod/network/netavark/run.go | 43 ++- libpod/network/netavark/run_test.go | 10 +- libpod/network/util/ip.go | 12 +- libpod/network/util/ip_calc.go | 53 +++ 12 files changed, 964 insertions(+), 54 deletions(-) create mode 100644 libpod/network/netavark/ipam.go create mode 100644 libpod/network/netavark/ipam_test.go create mode 100644 libpod/network/util/ip_calc.go diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go index 14cab2573..70d259b60 100644 --- a/libpod/network/cni/cni_conversion.go +++ b/libpod/network/cni/cni_conversion.go @@ -157,7 +157,7 @@ func convertIPAMConfToNetwork(network *types.Network, ipam ipamConfig, confPath return errors.Errorf("failed to parse gateway ip %s", ipam.Gateway) } // convert to 4 byte if ipv4 - internalutil.NormalizeIP(&gateway) + util.NormalizeIP(&gateway) } else if !network.Internal { // only add a gateway address if the network is not internal gateway, err = util.FirstIPInSubnet(sub) diff --git a/libpod/network/internal/util/bridge.go b/libpod/network/internal/util/bridge.go index c054c7d4e..476557050 100644 --- a/libpod/network/internal/util/bridge.go +++ b/libpod/network/internal/util/bridge.go @@ -27,41 +27,43 @@ func CreateBridge(n NetUtil, network *types.Network, usedNetworks []*net.IPNet) } } - if len(network.Subnets) == 0 { - freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) - if err != nil { - return err - } - network.Subnets = append(network.Subnets, *freeSubnet) - } - // ipv6 enabled means dual stack, check if we already have - // a ipv4 or ipv6 subnet and add one if not. - if network.IPv6Enabled { - ipv4 := false - ipv6 := false - for _, subnet := range network.Subnets { - if util.IsIPv6(subnet.Subnet.IP) { - ipv6 = true - } - if util.IsIPv4(subnet.Subnet.IP) { - ipv4 = true - } - } - if !ipv4 { + if network.IPAMOptions["driver"] != types.DHCPIPAMDriver { + if len(network.Subnets) == 0 { freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) if err != nil { return err } network.Subnets = append(network.Subnets, *freeSubnet) } - if !ipv6 { - freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks) - if err != nil { - return err + // ipv6 enabled means dual stack, check if we already have + // a ipv4 or ipv6 subnet and add one if not. + if network.IPv6Enabled { + ipv4 := false + ipv6 := false + for _, subnet := range network.Subnets { + if util.IsIPv6(subnet.Subnet.IP) { + ipv6 = true + } + if util.IsIPv4(subnet.Subnet.IP) { + ipv4 = true + } + } + if !ipv4 { + freeSubnet, err := GetFreeIPv4NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) + } + if !ipv6 { + freeSubnet, err := GetFreeIPv6NetworkSubnet(usedNetworks) + if err != nil { + return err + } + network.Subnets = append(network.Subnets, *freeSubnet) } - network.Subnets = append(network.Subnets, *freeSubnet) } + network.IPAMOptions["driver"] = types.HostLocalIPAMDriver } - network.IPAMOptions["driver"] = types.HostLocalIPAMDriver return nil } diff --git a/libpod/network/internal/util/ip.go b/libpod/network/internal/util/ip.go index ee759fd65..7fe35d3d4 100644 --- a/libpod/network/internal/util/ip.go +++ b/libpod/network/internal/util/ip.go @@ -68,11 +68,3 @@ func getRandomIPv6Subnet() (net.IPNet, error) { ip = append(ip, make([]byte, 8)...) return net.IPNet{IP: ip, Mask: net.CIDRMask(64, 128)}, nil } - -// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible -func NormalizeIP(ip *net.IP) { - ipv4 := ip.To4() - if ipv4 != nil { - *ip = ipv4 - } -} diff --git a/libpod/network/internal/util/validate.go b/libpod/network/internal/util/validate.go index 4dced8631..62c3f3951 100644 --- a/libpod/network/internal/util/validate.go +++ b/libpod/network/internal/util/validate.go @@ -38,7 +38,7 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) if !s.Subnet.Contains(s.Gateway) { return errors.Errorf("gateway %s not in subnet %s", s.Gateway, &s.Subnet) } - NormalizeIP(&s.Gateway) + util.NormalizeIP(&s.Gateway) } else if addGateway { ip, err := util.FirstIPInSubnet(net) if err != nil { @@ -52,13 +52,13 @@ func ValidateSubnet(s *types.Subnet, addGateway bool, usedNetworks []*net.IPNet) if !s.Subnet.Contains(s.LeaseRange.StartIP) { return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) } - NormalizeIP(&s.LeaseRange.StartIP) + util.NormalizeIP(&s.LeaseRange.StartIP) } if s.LeaseRange.EndIP != nil { if !s.Subnet.Contains(s.LeaseRange.EndIP) { return errors.Errorf("lease range end ip %s not in subnet %s", s.LeaseRange.EndIP, &s.Subnet) } - NormalizeIP(&s.LeaseRange.EndIP) + util.NormalizeIP(&s.LeaseRange.EndIP) } } 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 index b24f880e0..4d0c04634 100644 --- a/libpod/network/netavark/netavark_suite_test.go +++ b/libpod/network/netavark/netavark_suite_test.go @@ -32,6 +32,7 @@ func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, NetworkConfigDir: confDir, IsMachine: machine, NetavarkBinary: netavarkBinary, + IPAMDBPath: filepath.Join(confDir, "ipam.db"), LockFile: filepath.Join(confDir, "netavark.lock"), }) } diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go index aa7dc8183..cd6db4f58 100644 --- a/libpod/network/netavark/network.go +++ b/libpod/network/netavark/network.go @@ -13,6 +13,7 @@ import ( "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" @@ -30,6 +31,9 @@ type netavarkNetwork struct { // 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 @@ -50,6 +54,10 @@ type InitConfig struct { // 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. @@ -85,9 +93,27 @@ func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) { 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") + } + n := &netavarkNetwork{ networkConfigDir: conf.NetworkConfigDir, netavarkBinary: conf.NetavarkBinary, + ipamDBPath: ipamdbPath, defaultNetwork: defaultNetworkName, defaultSubnet: defaultNet, isMachine: conf.IsMachine, diff --git a/libpod/network/netavark/run.go b/libpod/network/netavark/run.go index bd26e957e..2f839151e 100644 --- a/libpod/network/netavark/run.go +++ b/libpod/network/netavark/run.go @@ -32,21 +32,29 @@ func (n *netavarkNetwork) Setup(namespacePath string, options types.SetupOptions return nil, err } - // TODO IP address assignment + // 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") } - b, err := json.Marshal(&netavarkOpts) - if err != nil { - return nil, err + // 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) } - fmt.Println(string(b)) result := map[string]types.StatusBlock{} - err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, result) + err = execNetavark(n.netavarkBinary, []string{"setup", namespacePath}, netavarkOpts, &result) if len(result) != len(options.Networks) { logrus.Errorf("unexpected netavark result: %v", result) @@ -65,12 +73,33 @@ func (n *netavarkNetwork) Teardown(namespacePath string, options types.TeardownO 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") } - return execNetavark(n.netavarkBinary, []string{"teardown", namespacePath}, netavarkOpts, nil) + 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) { diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go index 6c3b4d970..84db89b49 100644 --- a/libpod/network/netavark/run_test.go +++ b/libpod/network/netavark/run_test.go @@ -14,8 +14,6 @@ package netavark_test // }) import ( - "bytes" - "fmt" "io/ioutil" "net" "os" @@ -39,7 +37,6 @@ var _ = Describe("run netavark", func() { var ( libpodNet types.ContainerNetwork confDir string - logBuffer bytes.Buffer netNSTest ns.NetNS netNSContainer ns.NetNS ) @@ -60,8 +57,11 @@ var _ = Describe("run netavark", func() { } BeforeEach(func() { + // 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. @@ -74,8 +74,6 @@ var _ = Describe("run netavark", func() { if err != nil { Fail("Failed to create tmpdir") } - logBuffer = bytes.Buffer{} - logrus.SetOutput(&logBuffer) netNSTest, err = netns.NewNS() if err != nil { @@ -106,8 +104,6 @@ var _ = Describe("run netavark", func() { netns.UnmountNS(netNSContainer) netNSContainer.Close() - - fmt.Println(logBuffer.String()) }) It("test basic setup", func() { diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go index e75107a1c..e82b4a781 100644 --- a/libpod/network/util/ip.go +++ b/libpod/network/util/ip.go @@ -1,6 +1,8 @@ package util -import "net" +import ( + "net" +) // IsIPv6 returns true if netIP is IPv6. func IsIPv6(netIP net.IP) bool { @@ -44,3 +46,11 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer cidr.IP[len(cidr.IP)-1]++ return cidr.IP, nil } + +// NormalizeIP will transform the given ip to the 4 byte len ipv4 if possible +func NormalizeIP(ip *net.IP) { + ipv4 := ip.To4() + if ipv4 != nil { + *ip = ipv4 + } +} diff --git a/libpod/network/util/ip_calc.go b/libpod/network/util/ip_calc.go new file mode 100644 index 000000000..a27ddf78b --- /dev/null +++ b/libpod/network/util/ip_calc.go @@ -0,0 +1,53 @@ +// Copyright 2015 CNI authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "math/big" + "net" +) + +// NextIP returns IP incremented by 1 +func NextIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Add(i, big.NewInt(1))) +} + +// PrevIP returns IP decremented by 1 +func PrevIP(ip net.IP) net.IP { + i := ipToInt(ip) + return intToIP(i.Sub(i, big.NewInt(1))) +} + +// Cmp compares two IPs, returning the usual ordering: +// a < b : -1 +// a == b : 0 +// a > b : 1 +func Cmp(a, b net.IP) int { + aa := ipToInt(a) + bb := ipToInt(b) + return aa.Cmp(bb) +} + +func ipToInt(ip net.IP) *big.Int { + if v := ip.To4(); v != nil { + return big.NewInt(0).SetBytes(v) + } + return big.NewInt(0).SetBytes(ip.To16()) +} + +func intToIP(i *big.Int) net.IP { + return net.IP(i.Bytes()) +} -- cgit v1.2.3-54-g00ecf From 3fe0c49174e2fba6695dcedff449ba73d15943c8 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 28 Oct 2021 20:04:33 +0200 Subject: Fix RUST_LOG envar for netavark THe rust netlink library is very verbose. It contains way to much debug and trave logs. We can set `RUST_LOG=netavark=` to make sure this log level only applies to netavark and not the libraries. Signed-off-by: Paul Holzinger --- libpod/network/netavark/exec.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/libpod/network/netavark/exec.go b/libpod/network/netavark/exec.go index 700df9a69..d6458eeb4 100644 --- a/libpod/network/netavark/exec.go +++ b/libpod/network/netavark/exec.go @@ -45,6 +45,18 @@ func newNetavarkError(msg string, err error) error { } } +// 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 @@ -72,7 +84,7 @@ func execNetavark(binary string, args []string, stdin, result interface{}) error // connect stderr to the podman stderr for logging cmd.Stderr = os.Stderr // set the netavark log level to the same as the podman - cmd.Env = append(os.Environ(), "RUST_LOG="+logrus.GetLevel().String()) + 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") -- cgit v1.2.3-54-g00ecf From 1c88f741a7a15a7c8ff666c080a325ef0056af02 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Mon, 1 Nov 2021 16:02:14 +0100 Subject: select network backend based on config You can change the network backendend in containers.conf supported values are "cni" and "netavark". Signed-off-by: Paul Holzinger --- libpod/network/netavark/network.go | 4 ++++ libpod/runtime.go | 46 +++++++++++++++++++++++++++++--------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/libpod/network/netavark/network.go b/libpod/network/netavark/network.go index cd6db4f58..cc6fb423c 100644 --- a/libpod/network/netavark/network.go +++ b/libpod/network/netavark/network.go @@ -110,6 +110,10 @@ func NewNetworkInterface(conf InitConfig) (types.ContainerNetwork, error) { 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, diff --git a/libpod/runtime.go b/libpod/runtime.go index b01f8dd13..71bfaff9a 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -28,6 +28,7 @@ import ( "github.com/containers/podman/v3/libpod/events" "github.com/containers/podman/v3/libpod/lock" "github.com/containers/podman/v3/libpod/network/cni" + "github.com/containers/podman/v3/libpod/network/netavark" nettypes "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/libpod/plugin" "github.com/containers/podman/v3/libpod/shutdown" @@ -483,16 +484,41 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } } - netInterface, err := cni.NewCNINetworkInterface(cni.InitConfig{ - CNIConfigDir: runtime.config.Network.NetworkConfigDir, - CNIPluginDirs: runtime.config.Network.CNIPluginDirs, - DefaultNetwork: runtime.config.Network.DefaultNetwork, - DefaultSubnet: runtime.config.Network.DefaultSubnet, - IsMachine: runtime.config.Engine.MachineEnabled, - LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "cni.lock"), - }) - if err != nil { - return errors.Wrapf(err, "could not create network interface") + var netInterface nettypes.ContainerNetwork + + switch runtime.config.Network.NetworkBackend { + case "", "cni": + netInterface, err = cni.NewCNINetworkInterface(cni.InitConfig{ + CNIConfigDir: runtime.config.Network.NetworkConfigDir, + CNIPluginDirs: runtime.config.Network.CNIPluginDirs, + DefaultNetwork: runtime.config.Network.DefaultNetwork, + DefaultSubnet: runtime.config.Network.DefaultSubnet, + IsMachine: runtime.config.Engine.MachineEnabled, + LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "cni.lock"), + }) + if err != nil { + return errors.Wrapf(err, "could not create network interface") + } + + case "netavark": + netavarkBin, err := runtime.config.FindHelperBinary("netavark", false) + if err != nil { + return err + } + + netInterface, err = netavark.NewNetworkInterface(netavark.InitConfig{ + NetavarkBinary: netavarkBin, + NetworkConfigDir: filepath.Join(runtime.config.Engine.StaticDir, "networks"), + DefaultNetwork: runtime.config.Network.DefaultNetwork, + DefaultSubnet: runtime.config.Network.DefaultSubnet, + IsMachine: runtime.config.Engine.MachineEnabled, + LockFile: filepath.Join(runtime.config.Network.NetworkConfigDir, "netavark.lock"), + }) + if err != nil { + return errors.Wrapf(err, "could not create network interface") + } + default: + return errors.Errorf("unsupported network backend %q, check network_backend in containers.conf", runtime.config.Network.NetworkBackend) } runtime.network = netInterface -- cgit v1.2.3-54-g00ecf From b2f7430b6751e4a8177ee0088ea8e09675703828 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Mon, 1 Nov 2021 18:17:39 +0100 Subject: Add more netavark tests Signed-off-by: Paul Holzinger --- libpod/network/netavark/netavark_suite_test.go | 37 +++ libpod/network/netavark/run_test.go | 309 ++++++++++++++++++++++++- 2 files changed, 343 insertions(+), 3 deletions(-) diff --git a/libpod/network/netavark/netavark_suite_test.go b/libpod/network/netavark/netavark_suite_test.go index 4d0c04634..6063a54e3 100644 --- a/libpod/network/netavark/netavark_suite_test.go +++ b/libpod/network/netavark/netavark_suite_test.go @@ -3,14 +3,19 @@ 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) { @@ -36,3 +41,35 @@ func getNetworkInterface(confDir string, machine bool) (types.ContainerNetwork, 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/run_test.go b/libpod/network/netavark/run_test.go index 84db89b49..e44828496 100644 --- a/libpod/network/netavark/run_test.go +++ b/libpod/network/netavark/run_test.go @@ -28,6 +28,7 @@ import ( "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" @@ -57,6 +58,10 @@ var _ = Describe("run netavark", func() { } 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 @@ -130,6 +135,7 @@ var _ = Describe("run netavark", func() { 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)) @@ -143,14 +149,14 @@ var _ = Describe("run netavark", func() { i, err := net.InterfaceByName(intName) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName)) - Expect(i.HardwareAddr).To(Equal(macAddress)) + 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(subnet)) + Expect(addrs).To(ContainElements(EqualSubnet(subnet))) // check loopback adapter i, err = net.InterfaceByName("lo") @@ -175,12 +181,13 @@ var _ = Describe("run netavark", func() { IP: gw, Mask: net.CIDRMask(16, 32), } - Expect(addrs).To(ContainElements(subnet)) + 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 }) @@ -194,6 +201,256 @@ var _ = Describe("run netavark", func() { 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()) }) }) @@ -316,6 +573,52 @@ var _ = Describe("run netavark", func() { }) } + 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()) + }) + }) }) func runNetListener(wg *sync.WaitGroup, protocol, ip string, port int, expectedData string) { -- cgit v1.2.3-54-g00ecf From 8041d44c93ac46d330325cda849716f5ba2c40d9 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 11 Nov 2021 16:29:54 +0100 Subject: Add network backend to podman info Signed-off-by: Paul Holzinger --- libpod/define/info.go | 1 + libpod/info.go | 1 + 2 files changed, 2 insertions(+) diff --git a/libpod/define/info.go b/libpod/define/info.go index 61f2f4c75..15400991f 100644 --- a/libpod/define/info.go +++ b/libpod/define/info.go @@ -39,6 +39,7 @@ type HostInfo struct { LogDriver string `json:"logDriver"` MemFree int64 `json:"memFree"` MemTotal int64 `json:"memTotal"` + NetworkBackend string `json:"networkBackend"` OCIRuntime *OCIRuntimeInfo `json:"ociRuntime"` OS string `json:"os"` // RemoteSocket returns the UNIX domain socket the Podman service is listening on diff --git a/libpod/info.go b/libpod/info.go index 8d0b19f23..a1db5763a 100644 --- a/libpod/info.go +++ b/libpod/info.go @@ -131,6 +131,7 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) { Kernel: kv, MemFree: mi.MemFree, MemTotal: mi.MemTotal, + NetworkBackend: r.config.Network.NetworkBackend, OS: runtime.GOOS, Security: define.SecurityInfo{ AppArmorEnabled: apparmor.IsEnabled(), -- cgit v1.2.3-54-g00ecf From fe90a45e0d914723b8c26e4e9497f414312b6af0 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 11 Nov 2021 16:37:13 +0100 Subject: Add flag to overwrite network backend from config To make testing easier we can overwrite the network backend with the global `--network-backend` option. Signed-off-by: Paul Holzinger --- cmd/podman/common/completion.go | 7 +++++++ cmd/podman/root.go | 5 +++++ libpod/options.go | 13 +++++++++++++ libpod/runtime.go | 4 ++++ pkg/domain/infra/runtime_libpod.go | 3 +++ 5 files changed, 32 insertions(+) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index ea453a331..4cb29383a 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1163,6 +1163,13 @@ func AutocompleteEventBackend(cmd *cobra.Command, args []string, toComplete stri return types, cobra.ShellCompDirectiveNoFileComp } +// AutocompleteNetworkBackend - Autocomplete network backend options. +// -> "cni", "netavark" +func AutocompleteNetworkBackend(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + types := []string{"cni", "netavark"} + return types, cobra.ShellCompDirectiveNoFileComp +} + // AutocompleteLogLevel - Autocomplete log level options. // -> "trace", "debug", "info", "warn", "error", "fatal", "panic" func AutocompleteLogLevel(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 6da34050e..418a70e1e 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -357,6 +357,11 @@ func rootFlags(cmd *cobra.Command, opts *entities.PodmanConfig) { pFlags.StringVar(&cfg.Engine.Namespace, namespaceFlagName, cfg.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") _ = cmd.RegisterFlagCompletionFunc(namespaceFlagName, completion.AutocompleteNone) + networkBackendFlagName := "network-backend" + pFlags.StringVar(&cfg.Network.NetworkBackend, networkBackendFlagName, cfg.Network.NetworkBackend, `Network backend to use ("cni"|"netavark")`) + _ = cmd.RegisterFlagCompletionFunc(networkBackendFlagName, common.AutocompleteNetworkBackend) + pFlags.MarkHidden(networkBackendFlagName) + rootFlagName := "root" pFlags.StringVar(&cfg.Engine.StaticDir, rootFlagName, "", "Path to the root directory in which data, including images, is stored") _ = cmd.RegisterFlagCompletionFunc(rootFlagName, completion.AutocompleteDefault) diff --git a/libpod/options.go b/libpod/options.go index 250b16556..0cc4c784c 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -227,6 +227,19 @@ func WithNetworkCmdPath(path string) RuntimeOption { } } +// WithNetworkBackend specifies the name of the network backend. +func WithNetworkBackend(name string) RuntimeOption { + return func(rt *Runtime) error { + if rt.valid { + return define.ErrRuntimeFinalized + } + + rt.config.Network.NetworkBackend = name + + return nil + } +} + // WithCgroupManager specifies the manager implementation name which is used to // handle cgroups for containers. // Current valid values are "cgroupfs" and "systemd". diff --git a/libpod/runtime.go b/libpod/runtime.go index 71bfaff9a..c751df79b 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -499,6 +499,10 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { if err != nil { return errors.Wrapf(err, "could not create network interface") } + if runtime.config.Network.NetworkBackend == "" { + // set backend to cni so that podman info can display it + runtime.config.Network.NetworkBackend = "cni" + } case "netavark": netavarkBin, err := runtime.config.FindHelperBinary("netavark", false) diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index 7ec6135ee..cfb674b6d 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -200,6 +200,9 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo if fs.Changed("network-cmd-path") { options = append(options, libpod.WithNetworkCmdPath(cfg.Engine.NetworkCmdPath)) } + if fs.Changed("network-backend") { + options = append(options, libpod.WithNetworkBackend(cfg.Network.NetworkBackend)) + } if fs.Changed("events-backend") { options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger)) -- cgit v1.2.3-54-g00ecf From 3af19917a1666fc3f29d98f7f0b937eb488f706c Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Thu, 11 Nov 2021 17:49:18 +0100 Subject: Add failing run test for netavark Signed-off-by: Paul Holzinger --- libpod/network/netavark/run_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/libpod/network/netavark/run_test.go b/libpod/network/netavark/run_test.go index e44828496..3279203cc 100644 --- a/libpod/network/netavark/run_test.go +++ b/libpod/network/netavark/run_test.go @@ -619,6 +619,37 @@ var _ = Describe("run netavark", func() { 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) { -- cgit v1.2.3-54-g00ecf