From 8d180e9658a2a4de9b87ab32e0c20f6b52955b6a Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Thu, 6 Feb 2025 08:43:32 +0000 Subject: [PATCH] Add support for Linux Network Devices Implement support for passing Linux Network Devices to the container network namespace. The network device is passed during the creation of the container, before the process is started. It implements the logic defined in the OCI runtime specification. Signed-off-by: Antonio Ojea --- features.go | 3 + go.mod | 2 +- libcontainer/configs/config.go | 3 + libcontainer/configs/netdevices.go | 7 + libcontainer/configs/validate/validator.go | 38 +++ .../configs/validate/validator_test.go | 166 ++++++++++++++ libcontainer/network_linux.go | 132 +++++++++++ libcontainer/process_linux.go | 28 +++ libcontainer/specconv/spec_linux.go | 8 + libcontainer/specconv/spec_linux_test.go | 61 +++++ tests/integration/checkpoint.bats | 95 ++++++++ tests/integration/netdev.bats | 216 ++++++++++++++++++ tests/integration/userns.bats | 36 +++ 13 files changed, 794 insertions(+), 1 deletion(-) create mode 100644 libcontainer/configs/netdevices.go create mode 100644 tests/integration/netdev.bats diff --git a/features.go b/features.go index b636466bf..c5dff4a2d 100644 --- a/features.go +++ b/features.go @@ -63,6 +63,9 @@ var featuresCommand = cli.Command{ Enabled: &t, }, }, + NetDevices: &features.NetDevices{ + Enabled: &t, + }, }, PotentiallyUnsafeConfigAnnotations: []string{ "bundle", diff --git a/go.mod b/go.mod index 34c3e60a0..3544a1b6a 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli v1.22.17 github.com/vishvananda/netlink v1.3.1 + github.com/vishvananda/netns v0.0.5 golang.org/x/net v0.41.0 golang.org/x/sys v0.33.0 google.golang.org/protobuf v1.36.6 @@ -30,5 +31,4 @@ require ( github.com/cilium/ebpf v0.17.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/vishvananda/netns v0.0.5 // indirect ) diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index f88eb8b84..411012549 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -119,6 +119,9 @@ type Config struct { // The device nodes that should be automatically created within the container upon container start. Note, make sure that the node is marked as allowed in the cgroup as well! Devices []*devices.Device `json:"devices"` + // NetDevices are key-value pairs, keyed by network device name, moved to the container's network namespace. + NetDevices map[string]*LinuxNetDevice `json:"netDevices,omitempty"` + MountLabel string `json:"mount_label,omitempty"` // Hostname optionally sets the container's hostname if provided. diff --git a/libcontainer/configs/netdevices.go b/libcontainer/configs/netdevices.go new file mode 100644 index 000000000..09c38649e --- /dev/null +++ b/libcontainer/configs/netdevices.go @@ -0,0 +1,7 @@ +package configs + +// LinuxNetDevice represents a single network device to be added to the container's network namespace. +type LinuxNetDevice struct { + // Name of the device in the container namespace. + Name string `json:"name,omitempty"` +} diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index e0052900f..a9d03a2e2 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -24,6 +24,7 @@ func Validate(config *configs.Config) error { cgroupsCheck, rootfs, network, + netdevices, uts, security, namespaces, @@ -70,6 +71,43 @@ func rootfs(config *configs.Config) error { return nil } +// https://elixir.bootlin.com/linux/v6.12/source/net/core/dev.c#L1066 +func devValidName(name string) bool { + if len(name) == 0 || len(name) > unix.IFNAMSIZ { + return false + } + if name == "." || name == ".." { + return false + } + if strings.ContainsAny(name, "/: ") { + return false + } + return true +} + +func netdevices(config *configs.Config) error { + if len(config.NetDevices) == 0 { + return nil + } + if !config.Namespaces.Contains(configs.NEWNET) { + return errors.New("unable to move network devices without a NET namespace") + } + + if config.RootlessEUID || config.RootlessCgroups { + return errors.New("network devices are not supported for rootless containers") + } + + for name, netdev := range config.NetDevices { + if !devValidName(name) { + return fmt.Errorf("invalid network device name %q", name) + } + if netdev.Name != "" && !devValidName(netdev.Name) { + return fmt.Errorf("invalid network device name %q", netdev.Name) + } + } + return nil +} + func network(config *configs.Config) error { if !config.Namespaces.Contains(configs.NEWNET) { if len(config.Networks) > 0 || len(config.Routes) > 0 { diff --git a/libcontainer/configs/validate/validator_test.go b/libcontainer/configs/validate/validator_test.go index d157feea5..99e189c65 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -3,6 +3,7 @@ package validate import ( "os" "path/filepath" + "strings" "testing" "github.com/opencontainers/runc/libcontainer/configs" @@ -877,3 +878,168 @@ func TestValidateIOPriority(t *testing.T) { } } } + +func TestValidateNetDevices(t *testing.T) { + testCases := []struct { + name string + isErr bool + config *configs.Config + }{ + { + name: "network device with configured network namespace", + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + Path: "/var/run/netns/blue", + }, + }, + ), + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": {}, + }, + }, + }, + { + name: "network device rename", + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + Path: "/var/run/netns/blue", + }, + }, + ), + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": { + Name: "c0", + }, + }, + }, + }, + { + name: "network device network namespace created by runc", + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + }, + }, + ), + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": {}, + }, + }, + }, + { + name: "network device network namespace empty", + isErr: true, + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{}, + ), + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": {}, + }, + }, + }, + { + name: "network device rootless EUID", + isErr: true, + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + Path: "/var/run/netns/blue", + }, + }, + ), + RootlessEUID: true, + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": {}, + }, + }, + }, + { + name: "network device rootless", + isErr: true, + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + Path: "/var/run/netns/blue", + }, + }, + ), + RootlessCgroups: true, + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": {}, + }, + }, + }, + { + name: "network device bad name", + isErr: true, + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{ + { + Type: configs.NEWNET, + Path: "/var/run/netns/blue", + }, + }, + ), + NetDevices: map[string]*configs.LinuxNetDevice{ + "eth0": { + Name: "eth0/", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + config := tc.config + config.Rootfs = "/var" + + err := Validate(config) + if tc.isErr && err == nil { + t.Error("expected error, got nil") + } + + if !tc.isErr && err != nil { + t.Error(err) + } + }) + } +} + +func TestDevValidName(t *testing.T) { + testCases := []struct { + name string + valid bool + }{ + {name: "", valid: false}, + {name: "a", valid: true}, + {name: strings.Repeat("a", unix.IFNAMSIZ), valid: true}, + {name: strings.Repeat("a", unix.IFNAMSIZ+1), valid: false}, + {name: ".", valid: false}, + {name: "..", valid: false}, + {name: "dev/null", valid: false}, + {name: "valid:name", valid: false}, + {name: "valid name", valid: false}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if devValidName(tc.name) != tc.valid { + t.Fatalf("name %q, expected valid: %v", tc.name, tc.valid) + } + }) + } +} diff --git a/libcontainer/network_linux.go b/libcontainer/network_linux.go index 8915548b3..c645fbeb3 100644 --- a/libcontainer/network_linux.go +++ b/libcontainer/network_linux.go @@ -2,6 +2,7 @@ package libcontainer import ( "bytes" + "errors" "fmt" "os" "path/filepath" @@ -9,7 +10,12 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/types" + "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" + "github.com/vishvananda/netns" + + "golang.org/x/sys/unix" ) var strategies = map[string]networkStrategy{ @@ -98,3 +104,129 @@ func (l *loopback) attach(n *configs.Network) (err error) { func (l *loopback) detach(n *configs.Network) (err error) { return nil } + +// devChangeNetNamespace allows to move a device given by name to a network namespace given by nsPath +// and optionally change the device name. +// The device name will be kept the same if device.Name is the zero value. +// This function ensures that the move and rename operations occur atomically. +// It preserves existing interface attributes, including global IP addresses. +func devChangeNetNamespace(name string, nsPath string, device configs.LinuxNetDevice) error { + logrus.Debugf("attaching network device %s with attrs %+v to network namespace %s", name, device, nsPath) + link, err := netlink.LinkByName(name) + // recover same behavior on vishvananda/netlink@1.2.1 and do not fail when the kernel returns NLM_F_DUMP_INTR. + if err != nil && !errors.Is(err, netlink.ErrDumpInterrupted) { + return fmt.Errorf("link not found for interface %s on runtime namespace: %w", name, err) + } + + // Set the interface link state to DOWN before modifying attributes like namespace or name. + // This prevents potential conflicts or disruptions on the host network during the transition, + // particularly if other host components depend on this specific interface or its properties. + err = netlink.LinkSetDown(link) + if err != nil { + return fmt.Errorf("fail to set link down: %w", err) + } + + // Get the existing IP addresses on the interface. + addresses, err := netlink.AddrList(link, netlink.FAMILY_ALL) + // recover same behavior on vishvananda/netlink@1.2.1 and do not fail when the kernel returns NLM_F_DUMP_INTR. + if err != nil && !errors.Is(err, netlink.ErrDumpInterrupted) { + return fmt.Errorf("fail to get ip addresses: %w", err) + } + + // Do interface rename and namespace change in the same operation to avoid + // possible conflicts with the interface name. + // NLM_F_REQUEST: "It must be set on all request messages." + // NLM_F_ACK: "Request for an acknowledgment on success." + // netlink(7) man page: https://man7.org/linux/man-pages/man7/netlink.7.html + flags := unix.NLM_F_REQUEST | unix.NLM_F_ACK + req := nl.NewNetlinkRequest(unix.RTM_NEWLINK, flags) + + // Get a netlink socket in current namespace + nlSock, err := nl.GetNetlinkSocketAt(netns.None(), netns.None(), unix.NETLINK_ROUTE) + if err != nil { + return fmt.Errorf("could not get network namespace handle: %w", err) + } + defer nlSock.Close() + + req.Sockets = map[int]*nl.SocketHandle{ + unix.NETLINK_ROUTE: {Socket: nlSock}, + } + + // Set the interface index. + msg := nl.NewIfInfomsg(unix.AF_UNSPEC) + msg.Index = int32(link.Attrs().Index) + req.AddData(msg) + + // Set the interface name, also rename if requested. + newName := name + if device.Name != "" { + newName = device.Name + } + nameData := nl.NewRtAttr(unix.IFLA_IFNAME, nl.ZeroTerminated(newName)) + req.AddData(nameData) + + // Get the new network namespace. + ns, err := netns.GetFromPath(nsPath) + if err != nil { + return fmt.Errorf("could not get network namespace from path %s for network device %s : %w", nsPath, name, err) + } + defer ns.Close() + + val := nl.Uint32Attr(uint32(ns)) + attr := nl.NewRtAttr(unix.IFLA_NET_NS_FD, val) + req.AddData(attr) + + _, err = req.Execute(unix.NETLINK_ROUTE, 0) + // recover same behavior on vishvananda/netlink@1.2.1 and do not fail when the kernel returns NLM_F_DUMP_INTR. + if err != nil && !errors.Is(err, netlink.ErrDumpInterrupted) { + return fmt.Errorf("fail to move network device %s to network namespace %s: %w", name, nsPath, err) + } + + // To avoid us the husle with goroutines when joining a netns, + // we let the library create the socket in the namespace for us. + nhNs, err := netlink.NewHandleAt(ns) + if err != nil { + return err + } + defer nhNs.Close() + + nsLink, err := nhNs.LinkByName(newName) + // recover same behavior on vishvananda/netlink@1.2.1 and do not fail when the kernel returns NLM_F_DUMP_INTR. + if err != nil && !errors.Is(err, netlink.ErrDumpInterrupted) { + return fmt.Errorf("link not found for interface %s on namespace %s : %w", newName, nsPath, err) + } + + // Re-add the original IP addresses to the interface in the new namespace. + // The kernel removes IP addresses when an interface is moved between network namespaces. + for _, address := range addresses { + logrus.Debugf("processing address %s from network device %s", address.String(), name) + // Only move permanent IP addresses configured by the user, dynamic addresses are excluded because + // their validity may rely on the original network namespace's context and they may have limited + // lifetimes and are not guaranteed to be available in a new namespace. + // Ref: https://www.ietf.org/rfc/rfc3549.txt + if address.Flags&unix.IFA_F_PERMANENT == 0 { + logrus.Debugf("skipping address %s from network device %s: not a permanent address", address.String(), name) + continue + } + // Only move IP addresses with global scope because those are not host-specific, auto-configured, + // or have limited network scope, making them unsuitable inside the container namespace. + // Ref: https://www.ietf.org/rfc/rfc3549.txt + if address.Scope != unix.RT_SCOPE_UNIVERSE { + logrus.Debugf("skipping address %s from network device %s: not an address with global scope", address.String(), name) + continue + } + // Remove the interface attribute of the original address + // to avoid issues when the interface is renamed. + err = nhNs.AddrAdd(nsLink, &netlink.Addr{IPNet: address.IPNet}) + if err != nil { + return fmt.Errorf("fail to set up address %s on namespace %s: %w", address.String(), nsPath, err) + } + } + + err = nhNs.LinkSetUp(nsLink) + if err != nil { + return fmt.Errorf("fail to set up interface %s on namespace %s: %w", nsLink.Attrs().Name, nsPath, err) + } + + return nil +} diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go index 3fcc79df4..82f121fb2 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -649,6 +649,10 @@ func (p *initProcess) start() (retErr error) { return fmt.Errorf("error creating network interfaces: %w", err) } + if err := p.setupNetworkDevices(); err != nil { + return fmt.Errorf("error creating network interfaces: %w", err) + } + // initConfig.SpecState is only needed to run hooks that are executed // inside a container, i.e. CreateContainer and StartContainer. if p.config.Config.HasHook(configs.CreateContainer, configs.StartContainer) { @@ -836,6 +840,30 @@ func (p *initProcess) createNetworkInterfaces() error { return nil } +// setupNetworkDevices sets up and initializes any defined network interface inside the container. +func (p *initProcess) setupNetworkDevices() error { + // host network pods does not move network devices. + if !p.config.Config.Namespaces.Contains(configs.NEWNET) { + return nil + } + // the container init process has already joined the provided net namespace, + // so we can use the process's net ns path directly. + nsPath := fmt.Sprintf("/proc/%d/ns/net", p.pid()) + + // If moving any of the network devices fails, we return an error immediately. + // The runtime spec requires that the kernel handles moving back any devices + // that were successfully moved before the failure occurred. + // See: https://github.com/opencontainers/runtime-spec/blob/27cb0027fd92ef81eda1ea3a8153b8337f56d94a/config-linux.md#namespace-lifecycle-and-container-termination + for name, netDevice := range p.config.Config.NetDevices { + err := devChangeNetNamespace(name, nsPath, *netDevice) + if err != nil { + return fmt.Errorf("move netDevice %s to namespace %s: %w", name, nsPath, err) + } + } + + return nil +} + func pidGetFd(pid, srcFd int) (*os.File, error) { pidFd, err := unix.PidfdOpen(pid, 0) if err != nil { diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index c6bf02c98..ec417ed37 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -480,6 +480,14 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { } } + for name, netdev := range spec.Linux.NetDevices { + if config.NetDevices == nil { + config.NetDevices = make(map[string]*configs.LinuxNetDevice) + } + config.NetDevices[name] = &configs.LinuxNetDevice{ + Name: netdev.Name, + } + } } // Set the host UID that should own the container's cgroup. diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 0f8d1c18d..772194962 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -956,3 +956,64 @@ func TestCreateDevices(t *testing.T) { t.Errorf("device /dev/ram0 not found in config devices; got %v", conf.Devices) } } + +func TestCreateNetDevices(t *testing.T) { + testCases := []struct { + name string + netDevices map[string]specs.LinuxNetDevice + }{ + { + name: "no network devices", + }, + { + name: "one network devices", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": {}, + }, + }, + { + name: "multiple network devices", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": {}, + "eth2": {}, + }, + }, + { + name: "multiple network devices and rename", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": {}, + "eth2": { + Name: "ctr_eth2", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + spec := Example() + spec.Linux.NetDevices = tc.netDevices + opts := &CreateOpts{ + CgroupName: "ContainerID", + UseSystemdCgroup: false, + Spec: spec, + } + config, err := CreateLibcontainerConfig(opts) + if err != nil { + t.Errorf("Couldn't create libcontainer config: %v", err) + } + if len(config.NetDevices) != len(opts.Spec.Linux.NetDevices) { + t.Fatalf("expected %d network devices and got %d", len(config.NetDevices), len(opts.Spec.Linux.NetDevices)) + } + for name, netdev := range config.NetDevices { + ctrNetDev, ok := config.NetDevices[name] + if !ok { + t.Fatalf("network device %s not found in the configuration", name) + } + if ctrNetDev.Name != netdev.Name { + t.Fatalf("expected %s got %s", ctrNetDev.Name, netdev.Name) + } + } + }) + } +} diff --git a/tests/integration/checkpoint.bats b/tests/integration/checkpoint.bats index 3db340613..eb7557f5a 100644 --- a/tests/integration/checkpoint.bats +++ b/tests/integration/checkpoint.bats @@ -2,14 +2,34 @@ load helpers +function create_netns() { + # Create a temporary name for the test network namespace. + tmp=$(mktemp -u) + ns_name=$(basename "$tmp") + + # Create the network namespace. + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') +} + +function delete_netns() { + # Delete the namespace only if the ns_name variable is set. + [ -v ns_name ] && ip netns del "$ns_name" +} + function setup() { # XXX: currently criu require root containers. requires criu root setup_busybox + + # Create a dummy interface to move to the container. + ip link add dummy0 type dummy } function teardown() { + ip link del dev dummy0 + delete_netns teardown_bundle } @@ -122,6 +142,52 @@ function simple_cr() { done } +function simple_cr_with_netdevice() { + # Set custom parameters to the netdevice to validate those are respected + mtu_value=1789 + mac_address="00:11:22:33:44:55" + global_ip="169.254.169.77/32" + + ip link set mtu "$mtu_value" dev dummy0 + ip link set address "$mac_address" dev dummy0 + ip address add "$global_ip" dev dummy0 + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + update_config ' .linux.netDevices |= {"dummy0": {} }' + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox_netdevice + [ "$status" -eq 0 ] + + testcontainer test_busybox_netdevice running + run runc exec test_busybox_netdevice ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *" $global_ip "* ]] + [[ "$output" == *"ether $mac_address "* ]] + [[ "$output" == *"mtu $mtu_value "* ]] + + for _ in $(seq 2); do + # checkpoint the running container + runc "$@" checkpoint --work-path ./work-dir test_busybox_netdevice + [ "$status" -eq 0 ] + + # after checkpoint busybox is no longer running + testcontainer test_busybox_netdevice checkpointed + + # restore from checkpoint + runc "$@" restore -d --work-path ./work-dir --console-socket "$CONSOLE_SOCKET" test_busybox_netdevice + [ "$status" -eq 0 ] + + # busybox should be back up and running + testcontainer test_busybox_netdevice running + run runc exec test_busybox_netdevice ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *" $global_ip "* ]] + [[ "$output" == *"ether $mac_address "* ]] + [[ "$output" == *"mtu $mtu_value "* ]] + done +} + @test "checkpoint and restore" { simple_cr } @@ -151,6 +217,35 @@ function simple_cr() { simple_cr } +@test "checkpoint and restore with netdevice" { + simple_cr_with_netdevice +} + +@test "checkpoint and restore with netdevice (bind mount, destination is symlink)" { + mkdir -p rootfs/real/conf + ln -s /real/conf rootfs/conf + update_config ' .mounts += [{ + source: ".", + destination: "/conf", + options: ["bind"] + }]' + simple_cr_with_netdevice +} + +@test "checkpoint and restore with netdevice (with --debug)" { + simple_cr_with_netdevice --debug +} + +@test "checkpoint and restore with netdevice (cgroupns)" { + # cgroupv2 already enables cgroupns so this case was tested above already + requires cgroups_v1 cgroupns + + # enable CGROUPNS + update_config '.linux.namespaces += [{"type": "cgroup"}]' + + simple_cr_with_netdevice +} + @test "checkpoint --pre-dump (bad --parent-path)" { runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox [ "$status" -eq 0 ] diff --git a/tests/integration/netdev.bats b/tests/integration/netdev.bats new file mode 100644 index 000000000..6eec8c88c --- /dev/null +++ b/tests/integration/netdev.bats @@ -0,0 +1,216 @@ +#!/usr/bin/env bats + +load helpers + +function create_netns() { + # Create a temporary name for the test network namespace. + tmp=$(mktemp -u) + ns_name=$(basename "$tmp") + + # Create the network namespace. + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') +} + +function delete_netns() { + # Delete the namespace only if the ns_name variable is set. + [ -v ns_name ] && ip netns del "$ns_name" +} + +function setup() { + requires root + setup_busybox + + # Create a dummy interface to move to the container. + ip link add dummy0 type dummy +} + +function teardown() { + ip link del dev dummy0 + delete_netns + teardown_bundle +} + +@test "move network device to container network namespace" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + runc run test_busybox + [ "$status" -eq 0 ] +} + +@test "move network device to container network namespace and restore it back" { + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + update_config ' .linux.netDevices |= {"dummy0": {} }' + + runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox + [ "$status" -eq 0 ] + + # The network namespace owner controls the lifecycle of the interface. + # The interface should remain on the namespace after the container was killed. + runc delete --force test_busybox + + # Move back the interface to the root namespace (pid 1). + ip netns exec "$ns_name" ip link set dev dummy0 netns 1 + + # Verify the interface is back in the root network namespace. + ip address show dev dummy0 +} + +@test "move network device to precreated container network namespace" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + + # Verify the interface is still present in the network namespace. + ip netns exec "$ns_name" ip address show dev dummy0 +} + +@test "move network device to precreated container network namespace and set ip address with global scope" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + global_ip="169.254.169.77/32" + + # Set the interface down to avoid possible network problems. + # Set a custom address to the interface. + ip link set down dev dummy0 + ip address add "$global_ip" dev dummy0 + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"$global_ip "* ]] + + # Verify the interface is still present in the network namespace. + run ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"$global_ip "* ]] +} + +@test "move network device to precreated container network namespace and set ip address without global scope" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + non_global_ip="127.0.0.33" + + # Set the interface down to avoid possible network problems. + # Set a custom address to the interface. + ip link set down dev dummy0 + ip address add "$non_global_ip" dev dummy0 + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" != *" $non_global_ip "* ]] + + # Verify the interface is still present in the network namespace. + ip netns exec "$ns_name" ip address show dev dummy0 +} + +@test "move network device to precreated container network namespace and set mtu" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + mtu_value=1789 + # Cet a custom mtu to the interface. + ip link set mtu "$mtu_value" dev dummy0 + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"mtu $mtu_value "* ]] + + # Verify the interface is still present in the network namespace. + run ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"mtu $mtu_value "* ]] +} + +@test "move network device to precreated container network namespace and set mac address" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + mac_address="00:11:22:33:44:55" + # set a custom mac address to the interface + ip link set address "$mac_address" dev dummy0 + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"ether $mac_address "* ]] + + # Verify the interface is still present in the network namespace. + run ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"ether $mac_address "* ]] +} + +@test "move network device to precreated container network namespace and rename" { + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" } } + | .process.args |= ["ip", "address", "show", "dev", "ctr_dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + + # Verify the interface is still present in the network namespace. + ip netns exec "$ns_name" ip address show dev ctr_dummy0 +} + +@test "move network device to precreated container network namespace and rename and set mtu and mac and ip address" { + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" } } + | .process.args |= ["ip", "address", "show", "dev", "ctr_dummy0"]' + + # Tell runc which network namespace to use. + create_netns + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + mtu_value=1789 + mac_address="00:11:22:33:44:55" + global_ip="169.254.169.77/32" + + # Set a custom mtu to the interface. + ip link set mtu "$mtu_value" dev dummy0 + # Set a custom mac address to the interface. + ip link set address "$mac_address" dev dummy0 + # Set a custom ip address to the interface. + ip address add "$global_ip" dev dummy0 + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *" $global_ip "* ]] + [[ "$output" == *"ether $mac_address "* ]] + [[ "$output" == *"mtu $mtu_value "* ]] + + # Verify the interface is still present in the network namespace. + run ip netns exec "$ns_name" ip address show dev ctr_dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *" $global_ip "* ]] + [[ "$output" == *"ether $mac_address "* ]] + [[ "$output" == *"mtu $mtu_value "* ]] +} diff --git a/tests/integration/userns.bats b/tests/integration/userns.bats index 1300dff9f..16afa30f8 100644 --- a/tests/integration/userns.bats +++ b/tests/integration/userns.bats @@ -246,3 +246,39 @@ function teardown() { [ "$status" -eq 0 ] [[ "$output" == "$netns_id" ]] } + +@test "userns with network interface" { + requires root + + # Create a dummy interface to move to the container. + ip link add dummy0 type dummy + + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + runc run test_busybox + [ "$status" -eq 0 ] + + # The interface is virtual and should not exist because + # is deleted during the namespace cleanup. + run ip link del dummy0 + [ "$status" -ne 0 ] +} + +@test "userns with network interface renamed" { + requires root + + # Create a dummy interface to move to the container. + ip link add dummy0 type dummy + + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" } } + | .process.args |= ["ip", "address", "show", "dev", "ctr_dummy0"]' + + runc run test_busybox + [ "$status" -eq 0 ] + + # The interface is virtual and should not exist because + # is deleted during the namespace cleanup. + run ip link del dummy0 + [ "$status" -ne 0 ] +}