Skip to content

Commit

Permalink
Merge pull request #1761 from Luap99/rootlessnetns
Browse files Browse the repository at this point in the history
libnetwork: add rootlessnetns package
  • Loading branch information
openshift-merge-bot[bot] authored Dec 6, 2023
2 parents e017a84 + 1e69dbf commit b647eb3
Show file tree
Hide file tree
Showing 25 changed files with 1,261 additions and 158 deletions.
12 changes: 12 additions & 0 deletions libnetwork/cni/cni_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/containernetworking/cni/pkg/invoke"
"github.com/containernetworking/cni/pkg/version"
Expand Down Expand Up @@ -80,6 +82,16 @@ func (e *cniExec) ExecPlugin(ctx context.Context, pluginPath string, stdinData [
c.Env = append(c.Env, "XDG_RUNTIME_DIR=")
}

// The CNI plugins need access to iptables in $PATH. As it turns out debian doesn't put
// /usr/sbin in $PATH for rootless users. This will break rootless networking completely.
// We might break existing users and we cannot expect everyone to change their $PATH so
// let's add /usr/sbin to $PATH ourselves.
path := os.Getenv("PATH")
if !strings.Contains(path, "/usr/sbin") {
path += ":/usr/sbin"
c.Env = append(c.Env, "PATH="+path)
}

err := c.Run()
if err != nil {
return nil, annotatePluginError(err, pluginPath, stdout.Bytes(), stderr.Bytes())
Expand Down
10 changes: 8 additions & 2 deletions libnetwork/cni/cni_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"path/filepath"
"testing"

"github.com/containers/common/internal/attributedstring"
"github.com/containers/common/libnetwork/cni"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
Expand All @@ -28,8 +30,12 @@ func TestCni(t *testing.T) {

func getNetworkInterface(cniConfDir string) (types.ContainerNetwork, error) {
return cni.NewCNINetworkInterface(&cni.InitConfig{
CNIConfigDir: cniConfDir,
CNIPluginDirs: cniPluginDirs,
CNIConfigDir: cniConfDir,
Config: &config.Config{
Network: config.NetworkConfig{
CNIPluginDirs: attributedstring.NewSlice(cniPluginDirs),
},
},
})
}

Expand Down
36 changes: 21 additions & 15 deletions libnetwork/cni/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"time"

"github.com/containernetworking/cni/libcni"
"github.com/containers/common/libnetwork/internal/rootlessnetns"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/version"
Expand Down Expand Up @@ -53,6 +54,9 @@ type cniNetwork struct {

// networks is a map with loaded networks, the key is the network name
networks map[string]*network

// rootlessNetns is used for the rootless network setup/teardown
rootlessNetns *rootlessnetns.Netns
}

type network struct {
Expand All @@ -65,21 +69,14 @@ type network struct {
type InitConfig struct {
// CNIConfigDir is directory where the cni config files are stored.
CNIConfigDir string
// CNIPluginDirs is a list of directories where cni should look for the plugins.
CNIPluginDirs []string
// RunDir is a directory where temporary files can be stored.
RunDir string

// DefaultNetwork is the name for the default network.
DefaultNetwork string
// DefaultSubnet is the default subnet for the default network.
DefaultSubnet string

// DefaultsubnetPools contains the subnets which must be used to allocate a free subnet by network create
DefaultsubnetPools []config.SubnetPool

// IsMachine describes whenever podman runs in a podman machine environment.
IsMachine bool

// Config containers.conf options
Config *config.Config
}

// NewCNINetworkInterface creates the ContainerNetwork interface for the CNI backend.
Expand All @@ -96,12 +93,12 @@ func NewCNINetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
return nil, err
}

defaultNetworkName := conf.DefaultNetwork
defaultNetworkName := conf.Config.Network.DefaultNetwork
if defaultNetworkName == "" {
defaultNetworkName = types.DefaultNetworkName
}

defaultSubnet := conf.DefaultSubnet
defaultSubnet := conf.Config.Network.DefaultSubnet
if defaultSubnet == "" {
defaultSubnet = types.DefaultSubnet
}
Expand All @@ -110,21 +107,30 @@ func NewCNINetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
return nil, fmt.Errorf("failed to parse default subnet: %w", err)
}

defaultSubnetPools := conf.DefaultsubnetPools
defaultSubnetPools := conf.Config.Network.DefaultSubnetPools
if defaultSubnetPools == nil {
defaultSubnetPools = config.DefaultSubnetPools
}

cni := libcni.NewCNIConfig(conf.CNIPluginDirs, &cniExec{})
var netns *rootlessnetns.Netns
if unshare.IsRootless() {
netns, err = rootlessnetns.New(conf.RunDir, rootlessnetns.CNI, conf.Config)
if err != nil {
return nil, err
}
}

cni := libcni.NewCNIConfig(conf.Config.Network.CNIPluginDirs.Values, &cniExec{})
n := &cniNetwork{
cniConfigDir: conf.CNIConfigDir,
cniPluginDirs: conf.CNIPluginDirs,
cniPluginDirs: conf.Config.Network.CNIPluginDirs.Get(),
cniConf: cni,
defaultNetwork: defaultNetworkName,
defaultSubnet: defaultNet,
defaultsubnetPools: defaultSubnetPools,
isMachine: conf.IsMachine,
lock: lock,
rootlessNetns: netns,
}

return n, nil
Expand Down
152 changes: 90 additions & 62 deletions libnetwork/cni/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,61 +39,71 @@ func (n *cniNetwork) Setup(namespacePath string, options types.SetupOptions) (ma
return nil, fmt.Errorf("failed to set the loopback adapter up: %w", err)
}

var retErr error
teardownOpts := options
teardownOpts.Networks = map[string]types.PerNetworkOptions{}
// make sure to teardown the already connected networks on error
defer func() {
if retErr != nil {
if len(teardownOpts.Networks) > 0 {
err := n.teardown(namespacePath, types.TeardownOptions(teardownOpts))
if err != nil {
logrus.Warn(err)
results := make(map[string]types.StatusBlock, len(options.Networks))

setup := func() error {
var retErr error
teardownOpts := options
teardownOpts.Networks = map[string]types.PerNetworkOptions{}
// make sure to teardown the already connected networks on error
defer func() {
if retErr != nil {
if len(teardownOpts.Networks) > 0 {
err := n.teardown(namespacePath, types.TeardownOptions(teardownOpts))
if err != nil {
logrus.Warn(err)
}
}
}
}()

ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
if err != nil {
return err
}
}()

ports, err := convertSpecgenPortsToCNIPorts(options.PortMappings)
if err != nil {
return nil, err
}
for name, netOpts := range options.Networks {
netOpts := netOpts
network := n.networks[name]
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, &netOpts)

results := make(map[string]types.StatusBlock, len(options.Networks))
for name, netOpts := range options.Networks {
netOpts := netOpts
network := n.networks[name]
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, &netOpts)

// If we have more than one static ip we need parse the ips via runtime config,
// make sure to add the ips capability to the first plugin otherwise it doesn't get the ips
if len(netOpts.StaticIPs) > 0 && !network.cniNet.Plugins[0].Network.Capabilities["ips"] {
caps := make(map[string]interface{})
caps["capabilities"] = map[string]bool{"ips": true}
network.cniNet.Plugins[0], retErr = libcni.InjectConf(network.cniNet.Plugins[0], caps)
// If we have more than one static ip we need parse the ips via runtime config,
// make sure to add the ips capability to the first plugin otherwise it doesn't get the ips
if len(netOpts.StaticIPs) > 0 && !network.cniNet.Plugins[0].Network.Capabilities["ips"] {
caps := make(map[string]interface{})
caps["capabilities"] = map[string]bool{"ips": true}
network.cniNet.Plugins[0], retErr = libcni.InjectConf(network.cniNet.Plugins[0], caps)
if retErr != nil {
return retErr
}
}

var res cnitypes.Result
res, retErr = n.cniConf.AddNetworkList(context.Background(), network.cniNet, rt)
// Add this network to teardown opts since it is now connected.
// Also add this if an errors was returned since we want to call teardown on this regardless.
teardownOpts.Networks[name] = netOpts
if retErr != nil {
return nil, retErr
return retErr
}
}

var res cnitypes.Result
res, retErr = n.cniConf.AddNetworkList(context.Background(), network.cniNet, rt)
// Add this network to teardown opts since it is now connected.
// Also add this if an errors was returned since we want to call teardown on this regardless.
teardownOpts.Networks[name] = netOpts
if retErr != nil {
return nil, retErr
logrus.Debugf("cni result for container %s network %s: %v", options.ContainerID, name, res)
var status types.StatusBlock
status, retErr = CNIResultToStatus(res)
if retErr != nil {
return retErr
}
results[name] = status
}
return nil
}

logrus.Debugf("cni result for container %s network %s: %v", options.ContainerID, name, res)
var status types.StatusBlock
status, retErr = CNIResultToStatus(res)
if retErr != nil {
return nil, retErr
}
results[name] = status
if n.rootlessNetns != nil {
err = n.rootlessNetns.Setup(len(options.Networks), setup)
} else {
err = setup()
}
return results, nil
return results, err
}

// CNIResultToStatus convert the cni result to status block
Expand Down Expand Up @@ -225,28 +235,39 @@ func (n *cniNetwork) teardown(namespacePath string, options types.TeardownOption
}

var multiErr *multierror.Error
for name, netOpts := range options.Networks {
netOpts := netOpts
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, &netOpts)

cniConfList, newRt, err := getCachedNetworkConfig(n.cniConf, name, rt)
if err == nil {
rt = newRt
} else {
logrus.Warnf("Failed to load cached network config: %v, falling back to loading network %s from disk", err, name)
network := n.networks[name]
if network == nil {
multiErr = multierror.Append(multiErr, fmt.Errorf("network %s: %w", name, types.ErrNoSuchNetwork))
continue
teardown := func() error {
for name, netOpts := range options.Networks {
netOpts := netOpts
rt := getRuntimeConfig(namespacePath, options.ContainerName, options.ContainerID, name, ports, &netOpts)

cniConfList, newRt, err := getCachedNetworkConfig(n.cniConf, name, rt)
if err == nil {
rt = newRt
} else {
logrus.Warnf("Failed to load cached network config: %v, falling back to loading network %s from disk", err, name)
network := n.networks[name]
if network == nil {
multiErr = multierror.Append(multiErr, fmt.Errorf("network %s: %w", name, types.ErrNoSuchNetwork))
continue
}
cniConfList = network.cniNet
}
cniConfList = network.cniNet
}

err = n.cniConf.DelNetworkList(context.Background(), cniConfList, rt)
if err != nil {
multiErr = multierror.Append(multiErr, err)
err = n.cniConf.DelNetworkList(context.Background(), cniConfList, rt)
if err != nil {
multiErr = multierror.Append(multiErr, err)
}
}
return nil
}

if n.rootlessNetns != nil {
err = n.rootlessNetns.Teardown(len(options.Networks), teardown)
} else {
err = teardown()
}
multiErr = multierror.Append(multiErr, err)

return multiErr.ErrorOrNil()
}

Expand All @@ -267,3 +288,10 @@ func getCachedNetworkConfig(cniConf *libcni.CNIConfig, name string, rt *libcni.R
}
return cniConfList, rt, nil
}

func (n *cniNetwork) RunInRootlessNetns(toRun func() error) error {
if n.rootlessNetns == nil {
return types.ErrNotRootlessNetns
}
return n.rootlessNetns.Run(n.lock, toRun)
}
8 changes: 8 additions & 0 deletions libnetwork/internal/rootlessnetns/netns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package rootlessnetns

type NetworkBackend int

const (
Netavark NetworkBackend = iota
CNI
)
28 changes: 28 additions & 0 deletions libnetwork/internal/rootlessnetns/netns_freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package rootlessnetns

import (
"errors"

"github.com/containers/common/pkg/config"
"github.com/containers/storage/pkg/lockfile"
)

var ErrNotSupported = errors.New("rootless netns only supported on linux")

type Netns struct{}

func New(dir string, backend NetworkBackend, conf *config.Config) (*Netns, error) {
return nil, ErrNotSupported
}

func (n *Netns) Setup(nets int, toRun func() error) error {
return ErrNotSupported
}

func (n *Netns) Teardown(nets int, toRun func() error) error {
return ErrNotSupported
}

func (n *Netns) Run(lock *lockfile.LockFile, toRun func() error) error {
return ErrNotSupported
}
Loading

0 comments on commit b647eb3

Please sign in to comment.