diff options
61 files changed, 4557 insertions, 507 deletions
diff --git a/.cirrus.yml b/.cirrus.yml index 9897a9f7f..091b37627 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -26,10 +26,10 @@ env: #### FEDORA_NAME: "fedora-34" PRIOR_FEDORA_NAME: "fedora-33" - UBUNTU_NAME: "ubuntu-2104" + UBUNTU_NAME: "ubuntu-2110" # Google-cloud VM Images - IMAGE_SUFFIX: "c6431352024203264" + IMAGE_SUFFIX: "c4955591916388352" FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" PRIOR_FEDORA_CACHE_IMAGE_NAME: "prior-fedora-${IMAGE_SUFFIX}" UBUNTU_CACHE_IMAGE_NAME: "ubuntu-${IMAGE_SUFFIX}" @@ -148,11 +148,11 @@ build_task: CTR_FQIN: ${FEDORA_CONTAINER_FQIN} # ID for re-use of build output _BUILD_CACHE_HANDLE: ${FEDORA_NAME}-build-${CIRRUS_BUILD_ID} - - env: &priorfedora_envvars - DISTRO_NV: ${PRIOR_FEDORA_NAME} - VM_IMAGE_NAME: ${PRIOR_FEDORA_CACHE_IMAGE_NAME} - CTR_FQIN: ${PRIOR_FEDORA_CONTAINER_FQIN} - _BUILD_CACHE_HANDLE: ${PRIOR_FEDORA_NAME}-build-${CIRRUS_BUILD_ID} + # - env: &priorfedora_envvars + # DISTRO_NV: ${PRIOR_FEDORA_NAME} + # VM_IMAGE_NAME: ${PRIOR_FEDORA_CACHE_IMAGE_NAME} + # CTR_FQIN: ${PRIOR_FEDORA_CONTAINER_FQIN} + # _BUILD_CACHE_HANDLE: ${PRIOR_FEDORA_NAME}-build-${CIRRUS_BUILD_ID} - env: &ubuntu_envvars DISTRO_NV: ${UBUNTU_NAME} VM_IMAGE_NAME: ${UBUNTU_CACHE_IMAGE_NAME} @@ -402,7 +402,7 @@ unit_test_task: - validate matrix: - env: *stdenvars - - env: *priorfedora_envvars + #- env: *priorfedora_envvars - env: *ubuntu_envvars # Special-case: Rootless on latest Fedora (standard) VM - name: "Rootless unit on $DISTRO_NV" @@ -522,11 +522,11 @@ container_integration_test_task: _BUILD_CACHE_HANDLE: ${FEDORA_NAME}-build-${CIRRUS_BUILD_ID} VM_IMAGE_NAME: ${FEDORA_CACHE_IMAGE_NAME} CTR_FQIN: ${FEDORA_CONTAINER_FQIN} - - env: - DISTRO_NV: ${PRIOR_FEDORA_NAME} - _BUILD_CACHE_HANDLE: ${PRIOR_FEDORA_NAME}-build-${CIRRUS_BUILD_ID} - VM_IMAGE_NAME: ${PRIOR_FEDORA_CACHE_IMAGE_NAME} - CTR_FQIN: ${PRIOR_FEDORA_CONTAINER_FQIN} + # - env: + # DISTRO_NV: ${PRIOR_FEDORA_NAME} + # _BUILD_CACHE_HANDLE: ${PRIOR_FEDORA_NAME}-build-${CIRRUS_BUILD_ID} + # VM_IMAGE_NAME: ${PRIOR_FEDORA_CACHE_IMAGE_NAME} + # CTR_FQIN: ${PRIOR_FEDORA_CONTAINER_FQIN} gce_instance: *standardvm timeout_in: 90m env: @@ -168,7 +168,11 @@ was replaced by the REST API. Varlink support has been removed as of the 3.0 rel For more details, you can see [this blog](https://podman.io/blogs/2020/01/17/podman-new-api.html). ## Static Binary Builds -The Cirrus CI integration within this repository contains a `static_build` job -which produces a static Podman binary for testing purposes. Please note that -this binary is not officially supported with respect to feature-completeness -and functionality and should be only used for testing. +The Cirrus CI integration within this repository contains a `Static_Build` job +which produces static Podman binaries for testing purposes. Please note that +these binaries are not officially supported with respect to feature-completeness +and functionality and should be only used for testing. To download these binaries, +load the build link with the commit SHA at +[main](https://cirrus-ci.com/github/containers/podman/main) or +`https://cirrus-ci.com/github/containers/podman/pull/<selected PR>` +and open the artifacts folder within `Static Build`. 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/images/scp.go b/cmd/podman/images/scp.go index 67a531e6b..5c9cadc7a 100644 --- a/cmd/podman/images/scp.go +++ b/cmd/podman/images/scp.go @@ -137,9 +137,15 @@ func scp(cmd *cobra.Command, args []string) (finalErr error) { scpOpts.Save.Format = "oci-archive" abiErr := abiEng.Save(context.Background(), scpOpts.SourceImageName, []string{}, scpOpts.Save) // save the image locally before loading it on remote, local, or ssh if abiErr != nil { - errors.Wrapf(abiErr, "could not save image as specified") + return errors.Wrapf(abiErr, "could not save image as specified") } if !rootless.IsRootless() && scpOpts.Rootless { + if scpOpts.User == "" { + scpOpts.User = os.Getenv("SUDO_USER") + if scpOpts.User == "" { + return errors.New("could not obtain root user, make sure the environmental variable SUDO_USER is set, and that this command is being run as root") + } + } err := abiEng.Transfer(context.Background(), scpOpts) if err != nil { return err @@ -270,7 +276,13 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination cliConnections := []string{} switch len(args) { case 1: - if strings.Contains(args[0], "::") { + if strings.Contains(args[0], "localhost") { + if strings.Split(args[0], "@")[0] != "root" { + return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") + } + scpOpts.Rootless = true + scpOpts.SourceImageName = strings.Split(args[0], "::")[1] + } else if strings.Contains(args[0], "::") { scpOpts.FromRemote = true cliConnections = append(cliConnections, args[0]) } else { @@ -282,11 +294,15 @@ func parseArgs(args []string, cfg *config.Config) (map[string]config.Destination } case 2: if strings.Contains(args[0], "localhost") || strings.Contains(args[1], "localhost") { // only supporting root to local using sudo at the moment - scpOpts.Rootless = true - scpOpts.User = strings.Split(args[1], "@")[0] - scpOpts.SourceImageName = strings.Split(args[0], "::")[1] if strings.Split(args[0], "@")[0] != "root" { - return nil, errors.Wrapf(define.ErrInvalidArg, "cannot transfer images from any user besides root using sudo") + return nil, errors.Wrapf(define.ErrInvalidArg, "currently, transferring images to a user account is not supported") + } + if len(strings.Split(args[0], "::")) > 1 { + scpOpts.Rootless = true + scpOpts.User = strings.Split(args[1], "@")[0] + scpOpts.SourceImageName = strings.Split(args[0], "::")[1] + } else { + return nil, errors.Wrapf(define.ErrInvalidArg, "currently, you cannot rename images during the transfer or transfer them to a user account") } } else if strings.Contains(args[0], "::") { if !(strings.Contains(args[1], "::")) && remoteArgLength(args[0], 1) == 0 { // if an image is specified, this mean we are loading to our client 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/docs/source/markdown/podman-completion.1.md b/docs/source/markdown/podman-completion.1.md index 7679f74b1..538bb9e60 100644 --- a/docs/source/markdown/podman-completion.1.md +++ b/docs/source/markdown/podman-completion.1.md @@ -40,7 +40,7 @@ Shell completion needs to be already enabled in the environment. The following c **echo "autoload -U compinit; compinit" >> ~/.zshrc** To make it available for all zsh sessions run:\ -**podman completion -f "${fpath[1]}/_podman zsh"** +**podman completion -f "${fpath[1]}/_podman" zsh** Once the shell is reloaded the auto-completion should be working. diff --git a/docs/source/markdown/podman-image-scp.1.md b/docs/source/markdown/podman-image-scp.1.md index b4caf353a..d39882417 100644 --- a/docs/source/markdown/podman-image-scp.1.md +++ b/docs/source/markdown/podman-image-scp.1.md @@ -68,8 +68,6 @@ Copying blob e2eb06d8af82 done Copying config 696d33ca15 done Writing manifest to image destination Storing signatures -Run Directory Obtained: /run/user/1000/ -[Run Root: /var/tmp/containers-user-1000/containers Graph Root: /root/.local/share/containers/storage DB Path: /root/.local/share/containers/storage/libpod/bolt_state.db] Getting image source signatures Copying blob 5eb901baf107 skipped: already exists Copying config 696d33ca15 done @@ -78,6 +76,20 @@ Storing signatures Loaded image(s): docker.io/library/alpine:latest ``` +``` +$ sudo podman image scp root@localhost::alpine +Copying blob e2eb06d8af82 done +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Getting image source signatures +Copying blob 5eb901baf107 +Copying config 696d33ca15 done +Writing manifest to image destination +Storing signatures +Loaded image(s): docker.io/library/alpine:latest +``` + ## SEE ALSO **[podman(1)](podman.1.md)**, **[podman-load(1)](podman-load.1.md)**, **[podman-save(1)](podman-save.1.md)**, **[podman-remote(1)](podman-remote.1.md)**, **[podman-system-connection-add(1)](podman-system-connection-add.1.md)**, **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)**, **[containers-transports(5)](https://github.com/containers/image/blob/main/docs/containers-transports.5.md)** diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 64fe99132..de23a4aeb 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -39,6 +39,7 @@ import ( "github.com/opencontainers/selinux/go-selinux/label" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) const ( @@ -1592,14 +1593,49 @@ func (c *Container) mountStorage() (_ string, deferredErr error) { }() } + rootUID, rootGID := c.RootUID(), c.RootGID() + + dirfd, err := unix.Open(mountPoint, unix.O_RDONLY|unix.O_PATH, 0) + if err != nil { + return "", errors.Wrap(err, "open mount point") + } + defer unix.Close(dirfd) + + err = unix.Mkdirat(dirfd, "etc", 0755) + if err != nil && !os.IsExist(err) { + return "", errors.Wrap(err, "create /etc") + } + // If the etc directory was created, chown it to root in the container + if err == nil && (rootUID != 0 || rootGID != 0) { + err = unix.Fchownat(dirfd, "etc", rootUID, rootGID, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return "", errors.Wrap(err, "chown /etc") + } + } + + etcInTheContainerPath, err := securejoin.SecureJoin(mountPoint, "etc") + if err != nil { + return "", errors.Wrap(err, "resolve /etc in the container") + } + + etcInTheContainerFd, err := unix.Open(etcInTheContainerPath, unix.O_RDONLY|unix.O_PATH, 0) + if err != nil { + return "", errors.Wrap(err, "open /etc in the container") + } + defer unix.Close(etcInTheContainerFd) + // If /etc/mtab does not exist in container image, then we need to // create it, so that mount command within the container will work. - mtab := filepath.Join(mountPoint, "/etc/mtab") - if err := idtools.MkdirAllAs(filepath.Dir(mtab), 0755, c.RootUID(), c.RootGID()); err != nil { - return "", errors.Wrap(err, "error creating mtab directory") + err = unix.Symlinkat("/proc/mounts", etcInTheContainerFd, "mtab") + if err != nil && !os.IsExist(err) { + return "", errors.Wrap(err, "creating /etc/mtab symlink") } - if err = os.Symlink("/proc/mounts", mtab); err != nil && !os.IsExist(err) { - return "", err + // If the symlink was created, then also chown it to root in the container + if err == nil && (rootUID != 0 || rootGID != 0) { + err = unix.Fchownat(etcInTheContainerFd, "mtab", rootUID, rootGID, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + return "", errors.Wrap(err, "chown /etc/mtab") + } } // Request a mount of all named volumes 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(), diff --git a/libpod/network/cni/cni_conversion.go b/libpod/network/cni/cni_conversion.go index 01e149114..70d259b60 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 - } + util.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 3df155637..5d587da23 100644 --- a/libpod/network/cni/config.go +++ b/libpod/network/cni/config.go @@ -7,8 +7,8 @@ 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" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -47,32 +47,9 @@ func (n *cniNetwork) networkCreate(newNetwork types.Network, defaultNet bool) (* 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 +61,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 +69,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 } @@ -109,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 := 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 @@ -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..476557050 --- /dev/null +++ b/libpod/network/internal/util/bridge.go @@ -0,0 +1,69 @@ +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 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) + } + // 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..cecfd7133 --- /dev/null +++ b/libpod/network/internal/util/create.go @@ -0,0 +1,42 @@ +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 { + 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/util/interfaces.go b/libpod/network/internal/util/interfaces.go index dc2bd601d..20819f756 100644 --- a/libpod/network/util/interfaces.go +++ b/libpod/network/internal/util/interfaces.go @@ -2,9 +2,9 @@ package util import "net" -// GetLiveNetworkSubnets returns a slice of subnets representing what the system +// getLiveNetworkSubnets returns a slice of subnets representing what the system // has defined as network interfaces -func GetLiveNetworkSubnets() ([]*net.IPNet, error) { +func getLiveNetworkSubnets() ([]*net.IPNet, error) { addrs, err := net.InterfaceAddrs() if err != nil { return nil, err 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/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/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..62c3f3951 --- /dev/null +++ b/libpod/network/internal/util/validate.go @@ -0,0 +1,121 @@ +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) + } + util.NormalizeIP(&s.Gateway) + } 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 { + if !s.Subnet.Contains(s.LeaseRange.StartIP) { + return errors.Errorf("lease range start ip %s not in subnet %s", s.LeaseRange.StartIP, &s.Subnet) + } + 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) + } + util.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 util.IsIPv6(network.Subnets[i].Subnet.IP) { + network.IPv6Enabled = true + } + } + 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/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..d6458eeb4 --- /dev/null +++ b/libpod/network/netavark/exec.go @@ -0,0 +1,128 @@ +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, + } +} + +// 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 +// 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(), 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") + } + + 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/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 new file mode 100644 index 000000000..6063a54e3 --- /dev/null +++ b/libpod/network/netavark/netavark_suite_test.go @@ -0,0 +1,75 @@ +// +build linux + +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) { + 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, + IPAMDBPath: filepath.Join(confDir, "ipam.db"), + 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/network.go b/libpod/network/netavark/network.go new file mode 100644 index 000000000..cc6fb423c --- /dev/null +++ b/libpod/network/netavark/network.go @@ -0,0 +1,305 @@ +// +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" + pkgutil "github.com/containers/podman/v3/pkg/util" + "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 + + // ipamDBPath is the path to the ip allocation bolt db + ipamDBPath string + + // 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 + + // 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. + 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") + } + + 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") + } + + if err := os.MkdirAll(conf.NetworkConfigDir, 0755); err != nil { + return nil, err + } + + n := &netavarkNetwork{ + networkConfigDir: conf.NetworkConfigDir, + netavarkBinary: conf.NetavarkBinary, + ipamDBPath: ipamdbPath, + 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..2f839151e --- /dev/null +++ b/libpod/network/netavark/run.go @@ -0,0 +1,119 @@ +// +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 + } + + // 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") + } + + // 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) + } + + 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 + } + + // 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") + } + + 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) { + 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..3279203cc --- /dev/null +++ b/libpod/network/netavark/run_test.go @@ -0,0 +1,693 @@ +// +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 ( + "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/libpod/network/util" + "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 + 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() { + 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 + 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. + 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") + } + + 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() + }) + + 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 + util.NormalizeIP(&gw) + 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(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(EqualSubnet(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(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 + }) + 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()) + 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()) + }) + }) + + 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()) + }) + }) + } + + 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()) + }) + }) + + 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) { + 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" + } +} diff --git a/libpod/network/util/ip.go b/libpod/network/util/ip.go index b2ba92735..e82b4a781 100644 --- a/libpod/network/util/ip.go +++ b/libpod/network/util/ip.go @@ -1,10 +1,7 @@ package util import ( - "crypto/rand" "net" - - "github.com/pkg/errors" ) // IsIPv6 returns true if netIP is IPv6. @@ -17,40 +14,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 @@ -84,30 +47,10 @@ func FirstIPInSubnet(addr *net.IPNet) (net.IP, error) { //nolint:interfacer 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 +// 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 } - // 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_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()) +} 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) - } - }) - } -} 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 b01f8dd13..c751df79b 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,45 @@ 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") + } + 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) + 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 diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 94d20a04a..1e175d664 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -86,6 +86,8 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "fill out specgen")) return } + // moby always create the working directory + sg.CreateWorkingDir = true ic := abi.ContainerEngine{Libpod: runtime} report, err := ic.ContainerCreate(r.Context(), sg) diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 4d21751d1..3fdb3f286 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -269,17 +269,11 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } if podOpt.Infra { - containerConfig := util.DefaultContainerConfig() - - pulledImages, err := pullImage(ic, writer, containerConfig.Engine.InfraImage, options, config.PullPolicyNewer) - if err != nil { - return nil, err - } + infraImage := util.DefaultContainerConfig().Engine.InfraImage infraOptions := entities.ContainerCreateOptions{ImageVolume: "bind"} - - podSpec.PodSpecGen.InfraImage = pulledImages[0].Names()[0] + podSpec.PodSpecGen.InfraImage = infraImage podSpec.PodSpecGen.NoInfra = false - podSpec.PodSpecGen.InfraContainerSpec = specgen.NewSpecGenerator(pulledImages[0].Names()[0], false) + podSpec.PodSpecGen.InfraContainerSpec = specgen.NewSpecGenerator(infraImage, false) podSpec.PodSpecGen.InfraContainerSpec.NetworkOptions = p.NetworkOptions err = specgenutil.FillOutSpecGen(podSpec.PodSpecGen.InfraContainerSpec, &infraOptions, []string{}) @@ -758,21 +752,3 @@ func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, path string, _ enti } return reports, nil } - -// pullImage is a helper function to set up the proper pull options and pull the image for certain containers -func pullImage(ic *ContainerEngine, writer io.Writer, imagePull string, options entities.PlayKubeOptions, pullPolicy config.PullPolicy) ([]*libimage.Image, error) { - // This ensures the image is the image store - pullOptions := &libimage.PullOptions{} - pullOptions.AuthFilePath = options.Authfile - pullOptions.CertDirPath = options.CertDir - pullOptions.SignaturePolicyPath = options.SignaturePolicy - pullOptions.Writer = writer - pullOptions.Username = options.Username - pullOptions.Password = options.Password - pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify - pulledImages, err := ic.Libpod.LibimageRuntime().Pull(context.Background(), imagePull, pullPolicy, pullOptions) - if err != nil { - return nil, err - } - return pulledImages, nil -} 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)) diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index 7f9228666..3e81d5c14 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -325,7 +325,7 @@ func becomeRootInUserNS(pausePid, fileToRead string, fileOutput *os.File) (_ boo uidsMapped = err == nil } if !uidsMapped { - logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids") + logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids if not using a network user") setgroups := fmt.Sprintf("/proc/%d/setgroups", pid) err = ioutil.WriteFile(setgroups, []byte("deny\n"), 0666) if err != nil { diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 9f398a0ed..f3dc28b01 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -378,6 +378,9 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if s.WorkDir == "" { s.WorkDir = "/" } + if s.CreateWorkingDir { + options = append(options, libpod.WithCreateWorkingDir()) + } if s.StopSignal != nil { options = append(options, libpod.WithStopSignal(*s.StopSignal)) } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 8a4497130..d777287d7 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -272,6 +272,10 @@ type ContainerStorageConfig struct { // If unset, the default, /, will be used. // Optional. WorkDir string `json:"work_dir,omitempty"` + // Create the working directory if it doesn't exist. + // If unset, it doesn't create it. + // Optional. + CreateWorkingDir bool `json:"create_working_dir,omitempty"` // StorageOpts is the container's storage options // Optional. StorageOpts map[string]string `json:"storage_opts,omitempty"` diff --git a/test/e2e/image_scp_test.go b/test/e2e/image_scp_test.go index acea2993d..3e7e8da48 100644 --- a/test/e2e/image_scp_test.go +++ b/test/e2e/image_scp_test.go @@ -78,6 +78,10 @@ var _ = Describe("podman image scp", func() { list.WaitWithDefaultTimeout() Expect(list).To(Exit(0)) Expect(list.LineInOutputStartsWith("quay.io/libpod/alpine")).To(BeTrue()) + + scp = podmanTest.PodmanAsUser([]string{"image", "scp", "root@localhost::" + ALPINE}, 0, 0, "", env) //transfer from root to rootless (us) + scp.WaitWithDefaultTimeout() + Expect(scp).To(Exit(0)) }) It("podman image scp bogus image", func() { diff --git a/test/python/docker/compat/test_containers.py b/test/python/docker/compat/test_containers.py index 1ad1e7f15..e6f7d992d 100644 --- a/test/python/docker/compat/test_containers.py +++ b/test/python/docker/compat/test_containers.py @@ -251,3 +251,17 @@ class TestContainers(unittest.TestCase): ctr.start() ret, out = ctr.exec_run(["stat", "-c", "%u:%g", "/workspace"]) self.assertEqual(out.rstrip(), b'1042:1043', "UID/GID set in dockerfile") + + + def test_non_existant_workdir(self): + dockerfile = (B'FROM quay.io/libpod/alpine:latest\n' + B'USER root\n' + B'WORKDIR /workspace/scratch\n' + B'RUN touch test') + img: Image + img, out = self.client.images.build(fileobj=io.BytesIO(dockerfile)) + ctr: Container = self.client.containers.create(image=img.id, detach=True, command="top", + volumes=["test_non_existant_workdir:/workspace"]) + ctr.start() + ret, out = ctr.exec_run(["stat", "/workspace/scratch/test"]) + self.assertEqual(ret, 0, "Working directory created if it doesn't exist") diff --git a/test/system/700-play.bats b/test/system/700-play.bats index 8cf279ada..c3e5e9354 100644 --- a/test/system/700-play.bats +++ b/test/system/700-play.bats @@ -76,6 +76,12 @@ RELABEL="system_u:object_r:container_file_t:s0" is "$output" "${RELABEL} $TESTDIR" "selinux relabel should have happened" fi + # Make sure that the K8s pause image isn't pulled but the local podman-pause is built. + run_podman images + run_podman 1 image exists k8s.gcr.io/pause + run_podman version --format "{{.Server.Version}}-{{.Server.Built}}" + run_podman image exists localhost/podman-pause:$output + run_podman stop -a -t 0 run_podman pod rm -t 0 -f test_pod } diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats index f6f32242b..c0d601586 100644 --- a/test/upgrade/test-upgrade.bats +++ b/test/upgrade/test-upgrade.bats @@ -123,8 +123,15 @@ EOF # Clean up vestiges of previous run $PODMAN rm -f podman_parent || true - # Not entirely a NOP! This is just so we get /run/crun created on a CI VM - $PODMAN run --rm $OLD_PODMAN true + + local netname=testnet-$(random_string 10) + $PODMAN network create $netname + + # Not entirely a NOP! This is just so we get the /run/... mount points created on a CI VM + # --mac-address is needed to create /run/cni, --network is needed to create /run/containers for dnsname + $PODMAN run --rm --mac-address 78:28:a6:8d:24:8a --network $netname $OLD_PODMAN true + $PODMAN network rm -f $netname + # # Use new-podman to run the above script under old-podman. @@ -136,7 +143,8 @@ EOF # # mount /etc/containers/storage.conf to use the same storage settings as on the host # mount /dev/shm because the container locks are stored there - # mount /var/lib/cni and /etc/cni/net.d for cni networking + # mount /var/lib/cni, /run/cni and /etc/cni/net.d for cni networking + # mount /run/containers for the dnsname plugin # $PODMAN run -d --name podman_parent --pid=host \ --privileged \ @@ -147,6 +155,8 @@ EOF -v /dev/fuse:/dev/fuse \ -v /run/crun:/run/crun \ -v /run/netns:/run/netns:rshared \ + -v /run/containers:/run/containers \ + -v /run/cni:/run/cni \ -v /var/lib/cni:/var/lib/cni \ -v /etc/cni/net.d:/etc/cni/net.d \ -v /dev/shm:/dev/shm \ |