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