359 lines
10 KiB
Go
359 lines
10 KiB
Go
//go:build (linux || freebsd) && cni
|
|
|
|
package cni
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containernetworking/cni/libcni"
|
|
"github.com/containers/common/libnetwork/internal/rootlessnetns"
|
|
"github.com/containers/common/libnetwork/types"
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/common/pkg/version"
|
|
"github.com/containers/storage/pkg/fileutils"
|
|
"github.com/containers/storage/pkg/lockfile"
|
|
"github.com/containers/storage/pkg/unshare"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const defaultRootLockPath = "/run/lock/podman-cni.lock"
|
|
|
|
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
|
|
|
|
// defaultsubnetPools contains the subnets which must be used to allocate a free subnet by network create
|
|
defaultsubnetPools []config.SubnetPool
|
|
|
|
// isMachine describes whenever podman runs in a podman machine environment.
|
|
isMachine bool
|
|
|
|
// lock is a internal lock for critical operations
|
|
lock *lockfile.LockFile
|
|
|
|
// 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]*network
|
|
|
|
// rootlessNetns is used for the rootless network setup/teardown
|
|
rootlessNetns *rootlessnetns.Netns
|
|
}
|
|
|
|
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
|
|
// RunDir is a directory where temporary files can be stored.
|
|
RunDir string
|
|
|
|
// IsMachine describes whenever podman runs in a podman machine environment.
|
|
IsMachine bool
|
|
|
|
// Config containers.conf options
|
|
Config *config.Config
|
|
}
|
|
|
|
// 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) {
|
|
var netns *rootlessnetns.Netns
|
|
var err error
|
|
// Do not use unshare.IsRootless() here. We only care if we are running re-exec in the userns,
|
|
// IsRootless() also returns true if we are root in a userns which is not what we care about and
|
|
// causes issues as this slower more complicated rootless-netns logic should not be used as root.
|
|
val, ok := os.LookupEnv(unshare.UsernsEnvName)
|
|
useRootlessNetns := ok && val == "done"
|
|
if useRootlessNetns {
|
|
netns, err = rootlessnetns.New(conf.RunDir, rootlessnetns.CNI, conf.Config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// root needs to use a globally unique lock because there is only one host netns
|
|
lockPath := defaultRootLockPath
|
|
if useRootlessNetns {
|
|
lockPath = filepath.Join(conf.CNIConfigDir, "cni.lock")
|
|
}
|
|
|
|
lock, err := lockfile.GetLockFile(lockPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defaultNetworkName := conf.Config.Network.DefaultNetwork
|
|
if defaultNetworkName == "" {
|
|
defaultNetworkName = types.DefaultNetworkName
|
|
}
|
|
|
|
defaultSubnet := conf.Config.Network.DefaultSubnet
|
|
if defaultSubnet == "" {
|
|
defaultSubnet = types.DefaultSubnet
|
|
}
|
|
defaultNet, err := types.ParseCIDR(defaultSubnet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse default subnet: %w", err)
|
|
}
|
|
|
|
defaultSubnetPools := conf.Config.Network.DefaultSubnetPools
|
|
if defaultSubnetPools == nil {
|
|
defaultSubnetPools = config.DefaultSubnetPools
|
|
}
|
|
|
|
cni := libcni.NewCNIConfig(conf.Config.Network.CNIPluginDirs.Values, &cniExec{})
|
|
n := &cniNetwork{
|
|
cniConfigDir: conf.CNIConfigDir,
|
|
cniPluginDirs: conf.Config.Network.CNIPluginDirs.Get(),
|
|
cniConf: cni,
|
|
defaultNetwork: defaultNetworkName,
|
|
defaultSubnet: defaultNet,
|
|
defaultsubnetPools: defaultSubnetPools,
|
|
isMachine: conf.IsMachine,
|
|
lock: lock,
|
|
rootlessNetns: netns,
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// Drivers will return the list of supported network drivers
|
|
// for this interface.
|
|
func (n *cniNetwork) Drivers() []string {
|
|
return []string{types.BridgeNetworkDriver, types.MacVLANNetworkDriver, types.IPVLANNetworkDriver}
|
|
}
|
|
|
|
// DefaultNetworkName will return the default cni network name.
|
|
func (n *cniNetwork) DefaultNetworkName() string {
|
|
return n.defaultNetwork
|
|
}
|
|
|
|
func (n *cniNetwork) loadNetworks() error {
|
|
// check the mod time of the config dir
|
|
var modTime time.Time
|
|
f, err := os.Stat(n.cniConfigDir)
|
|
// ignore error if the file does not exists
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
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
|
|
|
|
// 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 !errors.Is(err, os.ErrNotExist) {
|
|
logrus.Warnf("Error loading CNI config file %s: %v", file, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if !types.NameRegex.MatchString(conf.Name) {
|
|
logrus.Warnf("CNI config list %s has invalid name, skipping: %v", file, types.ErrInvalidName)
|
|
continue
|
|
}
|
|
|
|
// podman < v4.0 used the podman-machine cni plugin for podman machine port forwarding
|
|
// since this is now build into podman we no longer use the plugin
|
|
// old configs may still contain it so we just remove it here
|
|
if n.isMachine {
|
|
conf = removeMachinePlugin(conf)
|
|
}
|
|
|
|
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 {
|
|
// ignore ENOENT as the config has been removed in the meantime so we can just ignore this case
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
logrus.Errorf("CNI config list %s could not be converted to a libpod config, skipping: %v", file, err)
|
|
}
|
|
continue
|
|
}
|
|
logrus.Debugf("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 fmt.Errorf("failed to create default network %s: %w", n.defaultNetwork, err)
|
|
}
|
|
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, 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 *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, fmt.Errorf("more than one result for network ID %s", nameOrID)
|
|
}
|
|
net = val
|
|
}
|
|
}
|
|
if net != nil {
|
|
return net, nil
|
|
}
|
|
return nil, fmt.Errorf("unable to find network with name or ID %s: %w", nameOrID, types.ErrNoSuchNetwork)
|
|
}
|
|
|
|
// 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[:])
|
|
}
|
|
|
|
// Implement the NetUtil interface for easy code sharing with other network interfaces.
|
|
|
|
// ForEach call the given function for each network
|
|
func (n *cniNetwork) ForEach(run func(types.Network)) {
|
|
for _, val := range n.networks {
|
|
run(*val.libpodNet)
|
|
}
|
|
}
|
|
|
|
// Len return the number of networks
|
|
func (n *cniNetwork) Len() int {
|
|
return len(n.networks)
|
|
}
|
|
|
|
// DefaultInterfaceName return the default cni bridge name, must be suffixed with a number.
|
|
func (n *cniNetwork) DefaultInterfaceName() string {
|
|
return cniDeviceName
|
|
}
|
|
|
|
// NetworkInfo return the network information about binary path,
|
|
// package version and program version.
|
|
func (n *cniNetwork) NetworkInfo() types.NetworkInfo {
|
|
path := ""
|
|
packageVersion := ""
|
|
for _, p := range n.cniPluginDirs {
|
|
ver := version.Package(p)
|
|
if ver != version.UnknownPackage {
|
|
path = p
|
|
packageVersion = ver
|
|
break
|
|
}
|
|
}
|
|
|
|
info := types.NetworkInfo{
|
|
Backend: types.CNI,
|
|
Package: packageVersion,
|
|
Path: path,
|
|
}
|
|
|
|
dnsPath := filepath.Join(path, "dnsname")
|
|
dnsPackage := version.Package(dnsPath)
|
|
dnsProgram, err := version.ProgramDnsname(dnsPath)
|
|
if err != nil {
|
|
logrus.Infof("Failed to get the dnsname plugin version: %v", err)
|
|
}
|
|
if err := fileutils.Exists(dnsPath); err == nil {
|
|
info.DNS = types.DNSNetworkInfo{
|
|
Path: dnsPath,
|
|
Package: dnsPackage,
|
|
Version: dnsProgram,
|
|
}
|
|
}
|
|
|
|
return info
|
|
}
|
|
|
|
func (n *cniNetwork) Network(nameOrID string) (*types.Network, error) {
|
|
network, err := n.getNetwork(nameOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return network.libpodNet, err
|
|
}
|