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 <mheon@redhat.com>
This commit is contained in:
Matthew Heon
2021-05-24 16:11:00 -04:00
parent ac94be37e9
commit 533d88b656
10 changed files with 257 additions and 10 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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() {
if rootlessDefaultCNI {
toReturn.NSMode = Bridge
} else {
toReturn.NSMode = Slirp
}
} else {
toReturn.NSMode = Bridge
}

View File

@ -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())
})
})

View File

@ -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
}

1
vendor/modules.txt vendored
View File

@ -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