Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libnetwork: add rootlessnetns package #1761

Merged
merged 14 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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