From 533d88b6566974c979932bab071e9408580ac7f8 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Mon, 24 May 2021 16:11:00 -0400 Subject: Add the option of Rootless CNI networking by default When the containers.conf field "NetNS" is set to "Bridge" and the "RootlessNetworking" field is set to "cni", Podman will now handle rootless in the same way it does root - all containers will be joined to a default CNI network, instead of exclusively using slirp4netns. If no CNI default network config is present for the user, one will be auto-generated (this also works for root, but it won't be nearly as common there since the package should already ship a config). I eventually hope to remove the "NetNS=Bridge" bit from containers.conf, but let's get something in for Brent to work with. Signed-off-by: Matthew Heon --- cmd/podman/common/create_opts.go | 7 +- cmd/podman/common/netflags.go | 2 +- libpod/container.go | 3 +- libpod/runtime.go | 6 + pkg/api/handlers/compat/containers_create.go | 2 +- pkg/specgen/generate/namespaces.go | 2 +- pkg/specgen/namespaces.go | 8 +- test/e2e/run_networking_test.go | 14 ++ .../common/pkg/defaultnet/default_network.go | 222 +++++++++++++++++++++ vendor/modules.txt | 1 + 10 files changed, 257 insertions(+), 10 deletions(-) create mode 100644 vendor/github.com/containers/common/pkg/defaultnet/default_network.go diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 77ac781a5..76d7345fc 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/cgroups" @@ -140,7 +141,7 @@ func stringMaptoArray(m map[string]string) []string { // ContainerCreateToContainerCLIOpts converts a compat input struct to cliopts so it can be converted to // a specgen spec. -func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroupsManager string) (*ContainerCLIOpts, []string, error) { +func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*ContainerCLIOpts, []string, error) { var ( capAdd []string cappDrop []string @@ -248,7 +249,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup } // netMode - nsmode, _, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode)) + nsmode, _, err := specgen.ParseNetworkNamespace(string(cc.HostConfig.NetworkMode), true) if err != nil { return nil, nil, err } @@ -507,7 +508,7 @@ func ContainerCreateToContainerCLIOpts(cc handlers.CreateContainerConfig, cgroup cliOpts.Restart = policy } - if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && cgroupsManager == "systemd") { + if cc.HostConfig.MemorySwappiness != nil && (!rootless.IsRootless() || rootless.IsRootless() && cgroupsv2 && rtc.Engine.CgroupManager == "systemd") { cliOpts.MemorySwappiness = *cc.HostConfig.MemorySwappiness } else { cliOpts.MemorySwappiness = -1 diff --git a/cmd/podman/common/netflags.go b/cmd/podman/common/netflags.go index 4d0a554a6..9941bc716 100644 --- a/cmd/podman/common/netflags.go +++ b/cmd/podman/common/netflags.go @@ -201,7 +201,7 @@ func NetFlagsToNetOptions(cmd *cobra.Command) (*entities.NetOptions, error) { parts := strings.SplitN(network, ":", 2) - ns, cniNets, err := specgen.ParseNetworkNamespace(network) + ns, cniNets, err := specgen.ParseNetworkNamespace(network, containerConfig.Containers.RootlessNetworking == "cni") if err != nil { return nil, err } diff --git a/libpod/container.go b/libpod/container.go index 591cf9bc5..c6f0cd618 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -14,7 +14,6 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/libpod/lock" - "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/storage" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -1168,7 +1167,7 @@ func (c *Container) Networks() ([]string, bool, error) { func (c *Container) networks() ([]string, bool, error) { networks, err := c.runtime.state.GetNetworks(c) if err != nil && errors.Cause(err) == define.ErrNoSuchNetwork { - if len(c.config.Networks) == 0 && !rootless.IsRootless() { + if len(c.config.Networks) == 0 && c.config.NetMode.IsBridge() { return []string{c.runtime.netPlugin.GetDefaultNetworkName()}, true, nil } return c.config.Networks, false, nil diff --git a/libpod/runtime.go b/libpod/runtime.go index e551e6fe8..d14048311 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -17,6 +17,7 @@ import ( "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/defaultnet" "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/pkg/sysregistriesv2" is "github.com/containers/image/v5/storage" @@ -458,6 +459,11 @@ func makeRuntime(ctx context.Context, runtime *Runtime) (retErr error) { } } + // If we need to make a default network - do so now. + if err := defaultnet.Create(runtime.config.Network.DefaultNetwork, runtime.config.Network.DefaultSubnet, runtime.config.Network.NetworkConfigDir, runtime.config.Engine.StaticDir, runtime.config.Engine.MachineEnabled); err != nil { + logrus.Errorf("Failed to created default CNI network: %v", err) + } + // Set up the CNI net plugin netPlugin, err := ocicni.InitCNI(runtime.config.Network.DefaultNetwork, runtime.config.Network.NetworkConfigDir, runtime.config.Network.CNIPluginDirs...) if err != nil { diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 162a98135..8e9e1fb39 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -62,7 +62,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { } // Take body structure and convert to cliopts - cliOpts, args, err := common.ContainerCreateToContainerCLIOpts(body, rtc.Engine.CgroupManager) + cliOpts, args, err := common.ContainerCreateToContainerCLIOpts(body, rtc) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "make cli opts()")) return diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 278f35c22..f41186ae4 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -66,7 +66,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) case "cgroup": return specgen.ParseCgroupNamespace(cfg.Containers.CgroupNS) case "net": - ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS) + ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS, cfg.Containers.RootlessNetworking == "cni") return ns, err } diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index f665fc0be..80852930a 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -253,7 +253,7 @@ func ParseUserNamespace(ns string) (Namespace, error) { // ParseNetworkNamespace parses a network namespace specification in string // form. // Returns a namespace and (optionally) a list of CNI networks to join. -func ParseNetworkNamespace(ns string) (Namespace, []string, error) { +func ParseNetworkNamespace(ns string, rootlessDefaultCNI bool) (Namespace, []string, error) { toReturn := Namespace{} var cniNetworks []string // Net defaults to Slirp on rootless @@ -264,7 +264,11 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { toReturn.NSMode = FromPod case ns == "" || ns == string(Default) || ns == string(Private): if rootless.IsRootless() { - toReturn.NSMode = Slirp + if rootlessDefaultCNI { + toReturn.NSMode = Bridge + } else { + toReturn.NSMode = Slirp + } } else { toReturn.NSMode = Bridge } diff --git a/test/e2e/run_networking_test.go b/test/e2e/run_networking_test.go index 37e837b1d..696cec76c 100644 --- a/test/e2e/run_networking_test.go +++ b/test/e2e/run_networking_test.go @@ -786,4 +786,18 @@ var _ = Describe("Podman run networking", func() { Expect(session.ExitCode()).To(BeZero()) Expect(session.OutputToString()).To(ContainSubstring("search dns.podman")) }) + + It("Rootless podman run with --net=bridge works and connects to default network", func() { + // This is harmless when run as root, so we'll just let it run. + ctrName := "testctr" + ctr := podmanTest.Podman([]string{"run", "-d", "--net=bridge", "--name", ctrName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + inspectOut := podmanTest.InspectContainer(ctrName) + Expect(len(inspectOut)).To(Equal(1)) + Expect(len(inspectOut[0].NetworkSettings.Networks)).To(Equal(1)) + _, ok := inspectOut[0].NetworkSettings.Networks["podman"] + Expect(ok).To(BeTrue()) + }) }) diff --git a/vendor/github.com/containers/common/pkg/defaultnet/default_network.go b/vendor/github.com/containers/common/pkg/defaultnet/default_network.go new file mode 100644 index 000000000..9b32241d6 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/defaultnet/default_network.go @@ -0,0 +1,222 @@ +package defaultnet + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "regexp" + "text/template" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// TODO: A smarter implementation would make sure cni-podman0 was unused before +// making the default, and adjust if necessary +const networkTemplate = `{ + "cniVersion": "0.4.0", + "name": "{{{{.Name}}}}", + "plugins": [ + { + "type": "bridge", + "bridge": "cni-podman0", + "isGateway": true, + "ipMasq": true, + "hairpinMode": true, + "ipam": { + "type": "host-local", + "routes": [{ "dst": "0.0.0.0/0" }], + "ranges": [ + [ + { + "subnet": "{{{{.Subnet}}}}", + "gateway": "{{{{.Gateway}}}}" + } + ] + ] + } + }, +{{{{- if (eq .Machine true) }}}} + { + "type": "podman-machine", + "capabilities": { + "portMappings": true + } + }, +{{{{- end}}}} + { + "type": "portmap", + "capabilities": { + "portMappings": true + } + }, + { + "type": "firewall" + }, + { + "type": "tuning" + } + ] +} +` + +var ( + // Borrowed from Podman, modified to remove dashes and periods. + nameRegex = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_]*$") +) + +// Used to pass info into the template engine +type networkInfo struct { + Name string + Subnet string + Gateway string + Machine bool +} + +// The most trivial definition of a CNI network possible for our use here. +// We need the name, and nothing else. +type network struct { + Name string `json:"name"` +} + +// Create makes the CNI default network, if necessary. +// Accepts the name and subnet of the network to create (a standard template +// will be used, with these values plugged in), the configuration directory +// where CNI configs are stored (to verify if a named configuration already +// exists), an exists directory (where a sentinel file will be stored, to ensure +// the network is only made once), and an isMachine bool (to determine whether +// the machine block will be added to the config). +// Create first checks if a default network has already been created via the +// presence of a sentinel file. If it does exist, it returns immediately without +// error. +// It next checks if a CNI network with the given name already exists. In that +// case, it creates the sentinel file and returns without error. +// If neither of these are true, the default network is created. +func Create(name, subnet, configDir, existsDir string, isMachine bool) error { + // TODO: Should probably regex name to make sure it's valid. + if name == "" || subnet == "" || configDir == "" || existsDir == "" { + return errors.Errorf("must provide values for all arguments to MakeDefaultNetwork") + } + if !nameRegex.MatchString(name) { + return errors.Errorf("invalid default network name %s - letters, numbers, and underscores only", name) + } + + sentinelFile := filepath.Join(existsDir, "defaultCNINetExists") + + // Check if sentinel file exists, return immediately if it does. + if _, err := os.Stat(sentinelFile); err == nil { + return nil + } + + // Create the sentinel file if it doesn't exist, so subsequent checks + // don't need to go further. + file, err := os.Create(sentinelFile) + if err != nil { + return err + } + file.Close() + + // We may need to make the config dir. + if err := os.MkdirAll(configDir, 0755); err != nil && !os.IsExist(err) { + return errors.Wrapf(err, "error creating CNI configuration directory") + } + + // Check all networks in the CNI conflist. + files, err := ioutil.ReadDir(configDir) + if err != nil { + return errors.Wrapf(err, "error reading CNI configuration directory") + } + if len(files) > 0 { + configPaths := make([]string, 0, len(files)) + for _, path := range files { + if !path.IsDir() && filepath.Ext(path.Name()) == ".conflist" { + configPaths = append(configPaths, filepath.Join(configDir, path.Name())) + } + } + for _, config := range configPaths { + configName, err := getConfigName(config) + if err != nil { + logrus.Errorf("Error reading CNI configuration file: %v", err) + continue + } + if configName == name { + return nil + } + } + } + + // We need to make the config. + // Get subnet and gateway. + _, ipNet, err := net.ParseCIDR(subnet) + if err != nil { + return errors.Wrapf(err, "default network subnet %s is invalid", subnet) + } + + ones, bits := ipNet.Mask.Size() + if ones == bits { + return errors.Wrapf(err, "default network subnet %s is to small", subnet) + } + gateway := make(net.IP, len(ipNet.IP)) + // copy the subnet ip to the gateway so we can modify it + copy(gateway, ipNet.IP) + // the default gateway should be the first ip in the subnet + gateway[len(gateway)-1]++ + + netInfo := new(networkInfo) + netInfo.Name = name + netInfo.Gateway = gateway.String() + netInfo.Subnet = ipNet.String() + netInfo.Machine = isMachine + + templ, err := template.New("network_template").Delims("{{{{", "}}}}").Parse(networkTemplate) + if err != nil { + return errors.Wrapf(err, "error compiling template for default network") + } + var output bytes.Buffer + if err := templ.Execute(&output, netInfo); err != nil { + return errors.Wrapf(err, "error executing template for default network") + } + + // Next, we need to place the config on disk. + // Loop through possible indexes, with a limit of 100 attempts. + created := false + for i := 87; i < 187; i++ { + configFile, err := os.OpenFile(filepath.Join(configDir, fmt.Sprintf("%d-%s.conflist", i, name)), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) + if err != nil { + logrus.Infof("Attempt to create default CNI network config file failed: %v", err) + continue + } + defer configFile.Close() + + created = true + + // Success - file is open. Write our buffer to it. + if _, err := configFile.Write(output.Bytes()); err != nil { + return errors.Wrapf(err, "error writing default CNI config to file") + } + break + } + if !created { + return errors.Errorf("no available default network configuration file was found") + } + + return nil +} + +// Get the name of the configuration contained in a given conflist file. Accepts +// the full path of a .conflist CNI configuration. +func getConfigName(file string) (string, error) { + contents, err := ioutil.ReadFile(file) + if err != nil { + return "", err + } + config := new(network) + if err := json.Unmarshal(contents, config); err != nil { + return "", errors.Wrapf(err, "error decoding CNI configuration %s", filepath.Base(file)) + } + return config.Name, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b4c2c6330..50f8e7338 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -102,6 +102,7 @@ github.com/containers/common/pkg/cgroupv2 github.com/containers/common/pkg/chown github.com/containers/common/pkg/completion github.com/containers/common/pkg/config +github.com/containers/common/pkg/defaultnet github.com/containers/common/pkg/filters github.com/containers/common/pkg/manifests github.com/containers/common/pkg/parse -- cgit v1.2.3-54-g00ecf