diff options
author | Paul Holzinger <pholzing@redhat.com> | 2021-07-15 10:29:19 +0200 |
---|---|---|
committer | Paul Holzinger <pholzing@redhat.com> | 2021-08-24 12:28:03 +0200 |
commit | c0b1edd6a4dcad2b89a01975988d186b21b3158e (patch) | |
tree | ea500d48f28682ad36f6eb4954717c6629fb68b4 /libpod/network/cni/network.go | |
parent | e20ec47a59b4ac65d42f3fee7b8b7ec5760ea35d (diff) | |
download | podman-c0b1edd6a4dcad2b89a01975988d186b21b3158e.tar.gz podman-c0b1edd6a4dcad2b89a01975988d186b21b3158e.tar.bz2 podman-c0b1edd6a4dcad2b89a01975988d186b21b3158e.zip |
Network interface
Implement a new network interface to abstract CNI from libpod. The
interface is implemented for the CNI backend but in the future we can
add more backends.
The code is structured in three new packages:
- `libpod/network/types`: contains the interface definition
and the necessary types for it.
- `libpod/network/cni` contains the interface implementation for the CNI
backend.
- `libpod/network/util` a set of utility functions related to
networking.
The CNI package uses ginkgo style unit tests. To test Setup/Teardown the
test must be run as root. Each test will run in their own namespace to
make the test independent from the host environment.
New features with the CNI backend:
- The default network will be created in memory if it does not exists on
disk.
- It can set more than one static IP per container network.
- Networks are loaded once from disk and only if this interface is
used, e.g. for commands such as `podman info` networks are not loaded.
This reduces unnecessary disk IO.
This commit only adds the interface it is not wired into libpod. This
requires a lot of breaking changes which will be done in a followup
commit.
Once this is integrated into libpod the current network code under
`libpod/network` should be removed. Also the dependency on OCICNI
should be dropped.
Signed-off-by: Paul Holzinger <pholzing@redhat.com>
Diffstat (limited to 'libpod/network/cni/network.go')
-rw-r--r-- | libpod/network/cni/network.go | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/libpod/network/cni/network.go b/libpod/network/cni/network.go new file mode 100644 index 000000000..fde08a0c6 --- /dev/null +++ b/libpod/network/cni/network.go @@ -0,0 +1,340 @@ +// +build linux + +package cni + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "net" + "os" + "strings" + + "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" +) + +type cniNetwork struct { + // cniConfigDir is directory where the cni config files are stored. + cniConfigDir string + // cniPluginDirs is a list of directories where cni should look for the plugins. + cniPluginDirs []string + + cniConf *libcni.CNIConfig + + // 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 + + // networks is a map with loaded networks, the key is the network name + networks map[string]*network +} + +type network struct { + // filename is the full path to the cni config file on disk + filename string + libpodNet *types.Network + cniNet *libcni.NetworkConfigList +} + +type InitConfig struct { + // CNIConfigDir is directory where the cni config files are stored. + CNIConfigDir string + // CNIPluginDirs is a list of directories where cni should look for the plugins. + CNIPluginDirs []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 +} + +// NewCNINetworkInterface creates the ContainerNetwork interface for the CNI backend. +// Note: The networks are not loaded from disk until a method is called. +func NewCNINetworkInterface(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") + } + + cni := libcni.NewCNIConfig(conf.CNIPluginDirs, &cniExec{}) + n := &cniNetwork{ + cniConfigDir: conf.CNIConfigDir, + cniPluginDirs: conf.CNIPluginDirs, + cniConf: cni, + defaultNetwork: defaultNetworkName, + defaultSubnet: defaultNet, + isMachine: conf.IsMachine, + lock: lock, + } + + return n, nil +} + +func (n *cniNetwork) loadNetworks() error { + // skip loading networks if they are already loaded + if n.networks != nil { + return nil + } + // FIXME: do we have to support other file types as well, e.g. .conf? + files, err := libcni.ConfFiles(n.cniConfigDir, []string{".conflist"}) + if err != nil { + return err + } + networks := make(map[string]*network, len(files)) + for _, file := range files { + conf, err := libcni.ConfListFromFile(file) + if err != nil { + // do not log ENOENT errors + if !os.IsNotExist(err) { + logrus.Warnf("Error loading CNI config file %s: %v", file, err) + } + continue + } + + if !define.NameRegex.MatchString(conf.Name) { + logrus.Warnf("CNI config list %s has invalid name, skipping: %v", file, define.RegexError) + continue + } + + if _, err := n.cniConf.ValidateNetworkList(context.Background(), conf); err != nil { + logrus.Warnf("Error validating CNI config file %s: %v", file, err) + continue + } + + if val, ok := networks[conf.Name]; ok { + logrus.Warnf("CNI config list %s has the same network name as %s, skipping", file, val.filename) + continue + } + + net, err := createNetworkFromCNIConfigList(conf, file) + if err != nil { + logrus.Errorf("CNI config list %s could not be converted to a libpod config, skipping: %v", file, err) + continue + } + logrus.Tracef("Successfully loaded network %s: %v", net.Name, net) + networkInfo := network{ + filename: file, + cniNet: conf, + libpodNet: net, + } + networks[net.Name] = &networkInfo + } + + // 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 (n *cniNetwork) createDefaultNetwork() (*network, error) { + net := types.Network{ + Name: n.defaultNetwork, + NetworkInterface: "cni-podman0", + Driver: types.BridgeNetworkDriver, + Subnets: []types.Subnet{ + {Subnet: n.defaultSubnet}, + }, + } + return n.networkCreate(net, false) +} + +// 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 *cniNetwork) getNetwork(nameOrID string) (*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 *network + for _, val := range n.networks { + // This should not happen because we already looked up the map by name but check anyway. + if val.libpodNet.Name == nameOrID { + return val, nil + } + + if strings.HasPrefix(val.libpodNet.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) +} + +// getNetworkIDFromName creates a network ID from the name. It is just the +// sha256 hash so it is not safe but it should be safe enough for our use case. +func getNetworkIDFromName(name string) string { + hash := sha256.Sum256([]byte(name)) + return hex.EncodeToString(hash[:]) +} + +// getFreeIPv6NetworkSubnet returns a unused ipv4 subnet +func (n *cniNetwork) getFreeIPv4NetworkSubnet() (*types.Subnet, error) { + networks, err := n.getUsedSubnets() + if err != nil { + return nil, err + } + + // 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, networks); !intersectsConfig { + logrus.Debugf("found free ipv4 network subnet %s", network.String()) + return &types.Subnet{ + Subnet: types.IPNet{IPNet: *network}, + }, nil + } + network, err = util.NextSubnet(network) + if err != nil { + return nil, err + } + } +} + +// getFreeIPv6NetworkSubnet returns a unused ipv6 subnet +func (n *cniNetwork) getFreeIPv6NetworkSubnet() (*types.Subnet, error) { + networks, err := n.getUsedSubnets() + if err != nil { + return nil, err + } + + // 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, networks); !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)) + for _, val := range n.networks { + for _, subnet := range val.libpodNet.Subnets { + // nolint:exportloopref + subnets = append(subnets, &subnet.Subnet.IPNet) + } + } + // 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") +} + +// 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 +} + +// 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) + } + } + return names +} |