mirror of
https://github.com/containers/podman
synced 2024-10-22 02:03:38 +00:00
54b588c07d
Instead of creating an extra container create a network and mount namespace inside the podman user namespace. This ns is used to for rootless cni operations. This helps to align the rootless and rootful network code path. If we run as rootless we just have to set up a extra net ns and initialize slirp4netns in it. The ocicni lib will be called in that net ns. This design allows allows easier maintenance, no extra container with pause processes, support for rootless cni with --uidmap and possibly more. The biggest problem is backwards compatibility. I don't think live migration can be possible. If the user reboots or restart all cni containers everything should work as expected again. The user is left with the rootless-cni-infa container and image but this can safely be removed. To make the existing cni configs work we need execute the cni plugins in a extra mount namespace. This ensures that we can safely mount over /run and /var which have to be writeable for the cni plugins without removing access to these files by the main podman process. One caveat is that we need to keep the netns files at `XDG_RUNTIME_DIR/netns` accessible. `XDG_RUNTIME_DIR/rootless-cni/{run,var}` will be mounted to `/{run,var}`. To ensure that we keep the netns directory we bind mount this relative to the new root location, e.g. XDG_RUNTIME_DIR/rootless-cni/run/user/1000/netns before we mount the run directory. The run directory is mounted recursive, this makes the netns directory at the same path accessible as before. This also allows iptables-legacy to work because /run/xtables.lock is now writeable. Signed-off-by: Paul Holzinger <paul.holzinger@web.de>
307 lines
8.8 KiB
Go
307 lines
8.8 KiB
Go
package network
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/containernetworking/cni/pkg/version"
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/podman/v3/pkg/domain/entities"
|
|
"github.com/containers/podman/v3/pkg/util"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Create the CNI network
|
|
func Create(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (*entities.NetworkCreateReport, error) {
|
|
var fileName string
|
|
if err := isSupportedDriver(options.Driver); err != nil {
|
|
return nil, err
|
|
}
|
|
// Acquire a lock for CNI
|
|
l, err := acquireCNILock(runtimeConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer l.releaseCNILock()
|
|
if len(options.MacVLAN) > 0 || options.Driver == MacVLANNetworkDriver {
|
|
fileName, err = createMacVLAN(name, options, runtimeConfig)
|
|
} else {
|
|
fileName, err = createBridge(name, options, runtimeConfig)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &entities.NetworkCreateReport{Filename: fileName}, nil
|
|
}
|
|
|
|
// validateBridgeOptions validate the bridge networking options
|
|
func validateBridgeOptions(options entities.NetworkCreateOptions) error {
|
|
subnet := &options.Subnet
|
|
ipRange := &options.Range
|
|
gateway := options.Gateway
|
|
// if IPv6 is set an IPv6 subnet MUST be specified
|
|
if options.IPv6 && ((subnet.IP == nil) || (subnet.IP != nil && !IsIPv6(subnet.IP))) {
|
|
return errors.Errorf("ipv6 option requires an IPv6 --subnet to be provided")
|
|
}
|
|
// range and gateway depend on subnet
|
|
if subnet.IP == nil && (ipRange.IP != nil || gateway != nil) {
|
|
return errors.Errorf("every ip-range or gateway must have a corresponding subnet")
|
|
}
|
|
|
|
// if a range is given, we need to ensure it is "in" the network range.
|
|
if ipRange.IP != nil {
|
|
firstIP, err := FirstIPInSubnet(ipRange)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get first IP address from ip-range")
|
|
}
|
|
lastIP, err := LastIPInSubnet(ipRange)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get last IP address from ip-range")
|
|
}
|
|
if !subnet.Contains(firstIP) || !subnet.Contains(lastIP) {
|
|
return errors.Errorf("the ip range %s does not fall within the subnet range %s", ipRange.String(), subnet.String())
|
|
}
|
|
}
|
|
|
|
// if network is provided and if gateway is provided, make sure it is "in" network
|
|
if gateway != nil && !subnet.Contains(gateway) {
|
|
return errors.Errorf("gateway %s is not in valid for subnet %s", gateway.String(), subnet.String())
|
|
}
|
|
|
|
return 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("the value %d for mtu 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
|
|
}
|
|
return strconv.Atoi(vlan)
|
|
}
|
|
|
|
// createBridge creates a CNI network
|
|
func createBridge(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) {
|
|
var (
|
|
ipamRanges [][]IPAMLocalHostRangeConf
|
|
err error
|
|
routes []IPAMRoute
|
|
)
|
|
isGateway := true
|
|
ipMasq := true
|
|
|
|
// validate options
|
|
if err := validateBridgeOptions(options); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// For compatibility with the docker implementation:
|
|
// if IPv6 is enabled (it really means dual-stack) then an IPv6 subnet has to be provided, and one free network is allocated for IPv4
|
|
// if IPv6 is not specified the subnet may be specified and can be either IPv4 or IPv6 (podman, unlike docker, allows IPv6 only networks)
|
|
// If not subnet is specified an IPv4 subnet will be allocated
|
|
subnet := &options.Subnet
|
|
ipRange := &options.Range
|
|
gateway := options.Gateway
|
|
if subnet.IP != nil {
|
|
// if network is provided, does it conflict with existing CNI or live networks
|
|
err = ValidateUserNetworkIsAvailable(runtimeConfig, subnet)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// obtain CNI subnet default route
|
|
defaultRoute, err := NewIPAMDefaultRoute(IsIPv6(subnet.IP))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
routes = append(routes, defaultRoute)
|
|
// obtain CNI range
|
|
ipamRange, err := NewIPAMLocalHostRange(subnet, ipRange, gateway)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ipamRanges = append(ipamRanges, ipamRange)
|
|
}
|
|
// if no network is provided or IPv6 flag used, figure out the IPv4 network
|
|
if options.IPv6 || len(routes) == 0 {
|
|
subnetV4, err := GetFreeNetwork(runtimeConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// obtain IPv4 default route
|
|
defaultRoute, err := NewIPAMDefaultRoute(false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
routes = append(routes, defaultRoute)
|
|
// the CNI bridge plugin does not need to set
|
|
// the range or gateway options explicitly
|
|
ipamRange, err := NewIPAMLocalHostRange(subnetV4, nil, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ipamRanges = append(ipamRanges, ipamRange)
|
|
}
|
|
|
|
// create CNI config
|
|
ipamConfig, err := NewIPAMHostLocalConf(routes, ipamRanges)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if options.Internal {
|
|
isGateway = false
|
|
ipMasq = false
|
|
}
|
|
|
|
var mtu int
|
|
var vlan int
|
|
for k, v := range options.Options {
|
|
var err error
|
|
switch k {
|
|
case "mtu":
|
|
mtu, err = parseMTU(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
case "vlan":
|
|
vlan, err = parseVlan(v)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
default:
|
|
return "", errors.Errorf("unsupported option %s", k)
|
|
}
|
|
}
|
|
|
|
// obtain host bridge name
|
|
bridgeDeviceName, err := GetFreeDeviceName(runtimeConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(name) > 0 {
|
|
netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if util.StringInSlice(name, netNames) {
|
|
return "", errors.Errorf("the network name %s is already used", name)
|
|
}
|
|
} else {
|
|
// If no name is given, we give the name of the bridge device
|
|
name = bridgeDeviceName
|
|
}
|
|
|
|
// create CNI plugin configuration
|
|
ncList := NewNcList(name, version.Current(), options.Labels)
|
|
var plugins []CNIPlugins
|
|
// TODO need to iron out the role of isDefaultGW and IPMasq
|
|
bridge := NewHostLocalBridge(bridgeDeviceName, isGateway, false, ipMasq, mtu, vlan, ipamConfig)
|
|
plugins = append(plugins, bridge)
|
|
plugins = append(plugins, NewPortMapPlugin())
|
|
plugins = append(plugins, NewFirewallPlugin())
|
|
plugins = append(plugins, NewTuningPlugin())
|
|
// if we find the dnsname plugin we add configuration for it
|
|
if HasDNSNamePlugin(runtimeConfig.Network.CNIPluginDirs) && !options.DisableDNS {
|
|
if options.Internal {
|
|
logrus.Warnf("dnsname and --internal networks are incompatible. dnsname plugin not configured for network %s", name)
|
|
} else {
|
|
// Note: in the future we might like to allow for dynamic domain names
|
|
plugins = append(plugins, NewDNSNamePlugin(DefaultPodmanDomainName))
|
|
}
|
|
}
|
|
ncList["plugins"] = plugins
|
|
b, err := json.MarshalIndent(ncList, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(GetCNIConfDir(runtimeConfig), 0755); err != nil {
|
|
return "", err
|
|
}
|
|
cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name))
|
|
err = ioutil.WriteFile(cniPathName, b, 0644)
|
|
return cniPathName, err
|
|
}
|
|
|
|
func createMacVLAN(name string, options entities.NetworkCreateOptions, runtimeConfig *config.Config) (string, error) {
|
|
var (
|
|
mtu int
|
|
plugins []CNIPlugins
|
|
)
|
|
liveNetNames, err := GetLiveNetworkNames()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// The parent can be defined with --macvlan or as an option (-o parent:device)
|
|
parentNetworkDevice := options.MacVLAN
|
|
if len(parentNetworkDevice) < 1 {
|
|
if parent, ok := options.Options["parent"]; ok {
|
|
parentNetworkDevice = parent
|
|
}
|
|
}
|
|
|
|
// Make sure the host-device exists if provided
|
|
if len(parentNetworkDevice) > 0 && !util.StringInSlice(parentNetworkDevice, liveNetNames) {
|
|
return "", errors.Errorf("failed to find network interface %q", parentNetworkDevice)
|
|
}
|
|
if len(name) > 0 {
|
|
netNames, err := GetNetworkNamesFromFileSystem(runtimeConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if util.StringInSlice(name, netNames) {
|
|
return "", errors.Errorf("the network name %s is already used", name)
|
|
}
|
|
} else {
|
|
name, err = GetFreeDeviceName(runtimeConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
ncList := NewNcList(name, version.Current(), options.Labels)
|
|
if val, ok := options.Options["mtu"]; ok {
|
|
intVal, err := strconv.Atoi(val)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if intVal > 0 {
|
|
mtu = intVal
|
|
}
|
|
}
|
|
macvlan, err := NewMacVLANPlugin(parentNetworkDevice, options.Gateway, &options.Range, &options.Subnet, mtu)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
plugins = append(plugins, macvlan)
|
|
ncList["plugins"] = plugins
|
|
b, err := json.MarshalIndent(ncList, "", " ")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cniPathName := filepath.Join(GetCNIConfDir(runtimeConfig), fmt.Sprintf("%s.conflist", name))
|
|
err = ioutil.WriteFile(cniPathName, b, 0644)
|
|
return cniPathName, err
|
|
}
|