From 2de6e47535824339369730d93e76749fc923482f Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Thu, 6 Feb 2025 08:34:57 +0000 Subject: [PATCH 1/2] TEMP vendor linx netdevices OCI spec Change-Id: Id54c22cb5652c77b4a3c3bd4e46e96388be1cc11 Signed-off-by: Antonio Ojea --- .../opencontainers/runtime-spec/specs-go/config.go | 8 ++++++++ .../runtime-spec/specs-go/features/features.go | 8 ++++++++ .../opencontainers/runtime-spec/specs-go/version.go | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go b/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go index 1aa0693b57d..a5dec3c7f44 100644 --- a/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go +++ b/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go @@ -236,6 +236,8 @@ type Linux struct { Namespaces []LinuxNamespace `json:"namespaces,omitempty"` // Devices are a list of device nodes that are created for the container Devices []LinuxDevice `json:"devices,omitempty"` + // NetDevices are key-value pairs, keyed by network device name, moved to the container's network namespace. + NetDevices map[string]LinuxNetDevice `json:"netDevices,omitempty"` // Seccomp specifies the seccomp security settings for the container. Seccomp *LinuxSeccomp `json:"seccomp,omitempty"` // RootfsPropagation is the rootfs mount propagation mode for the container. @@ -491,6 +493,12 @@ type LinuxDevice struct { GID *uint32 `json:"gid,omitempty"` } +// 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"` +} + // LinuxDeviceCgroup represents a device rule for the devices specified to // the device controller type LinuxDeviceCgroup struct { diff --git a/vendor/github.com/opencontainers/runtime-spec/specs-go/features/features.go b/vendor/github.com/opencontainers/runtime-spec/specs-go/features/features.go index 949f532b65a..d8eb169dc39 100644 --- a/vendor/github.com/opencontainers/runtime-spec/specs-go/features/features.go +++ b/vendor/github.com/opencontainers/runtime-spec/specs-go/features/features.go @@ -48,6 +48,7 @@ type Linux struct { Selinux *Selinux `json:"selinux,omitempty"` IntelRdt *IntelRdt `json:"intelRdt,omitempty"` MountExtensions *MountExtensions `json:"mountExtensions,omitempty"` + NetDevices *NetDevices `json:"netDevices,omitempty"` } // Cgroup represents the "cgroup" field. @@ -143,3 +144,10 @@ type IDMap struct { // Nil value means "unknown", not "false". Enabled *bool `json:"enabled,omitempty"` } + +// NetDevices represents the "netDevices" field. +type NetDevices struct { + // Enabled is true if network devices support is compiled in. + // Nil value means "unknown", not "false". + Enabled *bool `json:"enabled,omitempty"` +} diff --git a/vendor/github.com/opencontainers/runtime-spec/specs-go/version.go b/vendor/github.com/opencontainers/runtime-spec/specs-go/version.go index 23234a9c583..b0a00466b61 100644 --- a/vendor/github.com/opencontainers/runtime-spec/specs-go/version.go +++ b/vendor/github.com/opencontainers/runtime-spec/specs-go/version.go @@ -11,7 +11,7 @@ const ( VersionPatch = 1 // VersionDev indicates development branch. Releases will be empty string. - VersionDev = "" + VersionDev = "+dev" ) // Version is the specification version that the package types support. From 686939e94519f227d8f216d6a93048c9fee6ca1f Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Thu, 6 Feb 2025 08:43:32 +0000 Subject: [PATCH 2/2] 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. Change-Id: Ifd4a0c5aa9a964f061325ca06525b74e25c29fe9 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 | 168 +++++++++++ libcontainer/network_linux.go | 115 ++++++++ libcontainer/process_linux.go | 26 ++ libcontainer/specconv/spec_linux.go | 8 + libcontainer/specconv/spec_linux_test.go | 61 ++++ tests/integration/netdev.bats | 262 ++++++++++++++++++ tests/integration/userns.bats | 34 +++ 12 files changed, 726 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 b636466bfe4..c5dff4a2d17 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 0844bd46fdc..0b9806df2af 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/urfave/cli v1.22.16 github.com/vishvananda/netlink v1.3.0 + github.com/vishvananda/netns v0.0.4 golang.org/x/net v0.38.0 golang.org/x/sys v0.31.0 google.golang.org/protobuf v1.36.6 @@ -39,5 +40,4 @@ require ( github.com/cilium/ebpf v0.17.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/vishvananda/netns v0.0.4 // indirect ) diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index 346fb9e5e3c..da634d8dc67 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"` + 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 00000000000..09c38649e4e --- /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 e0052900f43..cf99628315c 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 d157feea5bc..f35d6a6a293 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,170 @@ func TestValidateIOPriority(t *testing.T) { } } } + +func TestValidateNetDevices(t *testing.T) { + testCases := []struct { + name string + isErr bool + config *configs.Config + }{ + { + name: "network device", + 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, + Path: "", + }, + }, + ), + 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 { + tc := tc + 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 8915548b3bc..a4af405f94c 100644 --- a/libcontainer/network_linux.go +++ b/libcontainer/network_linux.go @@ -9,7 +9,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 +103,113 @@ func (l *loopback) attach(n *configs.Network) (err error) { func (l *loopback) detach(n *configs.Network) (err error) { return nil } + +// devChangeNetNamespace allows to change the device name from a network namespace and optionally replace the existing name. +// This function ensures that the move and rename operations occur simultaneously. +// It preserves existing interface attributes, including 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) + if err != nil { + return fmt.Errorf("link not found for interface %s on runtime namespace: %w", name, err) + } + + // set the interface down to change the attributes safely + err = netlink.LinkSetDown(link) + if err != nil { + return fmt.Errorf("fail to set link down: %w", err) + } + + // get the existing IP addresses + addresses, err := netlink.AddrList(link, netlink.FAMILY_ALL) + if err != nil { + 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. + flags := unix.NLM_F_REQUEST | unix.NLM_F_ACK + req := nl.NewNetlinkRequest(unix.RTM_NEWLINK, flags) + + // Get a netlink socket in current namespace + s, 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 s.Close() + + req.Sockets = map[int]*nl.SocketHandle{ + unix.NETLINK_ROUTE: {Socket: s}, + } + + // set the interface index + msg := nl.NewIfInfomsg(unix.AF_UNSPEC) + msg.Index = int32(link.Attrs().Index) + req.AddData(msg) + + // set the interface name, rename if requested + newName := name + if device.Name != "" { + newName = device.Name + } + nameData := nl.NewRtAttr(unix.IFLA_IFNAME, nl.ZeroTerminated(newName)) + req.AddData(nameData) + + // set 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) + if err != nil { + return fmt.Errorf("fail to move network device %s to network namespace %s: %w", name, nsPath, err) + } + + // to avoid golang problem with goroutines we create the socket in the + // namespace and use it directly + nhNs, err := netlink.NewHandleAt(ns) + if err != nil { + return err + } + defer nhNs.Close() + + nsLink, err := nhNs.LinkByName(newName) + if err != nil { + return fmt.Errorf("link not found for interface %s on namespace %s : %w", newName, nsPath, err) + } + + for _, address := range addresses { + // 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 { + 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 { + 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 5ac574bdeff..c69796ca5ae 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,28 @@ 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 + } + // get the namespace defined by the config and fall back + // to the one created by runc to run the container process. + nsPath := p.config.Config.Namespaces.PathOf(configs.NEWNET) + if nsPath == "" { + nsPath = fmt.Sprintf("/proc/%d/ns/net", p.pid()) + } + for name, netDevice := range p.config.Config.NetDevices { + err := devChangeNetNamespace(name, nsPath, *netDevice) + if err != nil { + return 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 37068da7f8f..fe0be933503 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -479,6 +479,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 66359f79e6c..055c9616e41 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/netdev.bats b/tests/integration/netdev.bats new file mode 100644 index 00000000000..0645af51b71 --- /dev/null +++ b/tests/integration/netdev.bats @@ -0,0 +1,262 @@ +#!/usr/bin/env bats + +load helpers + +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 + 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" { + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + 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 + ip netns exec "$ns_name" ip link set dev dummy0 netns 1 + [ "$status" -eq 0 ] + + runc delete --force test_busybox + + # verify the interface is back in the root network namespace + ip address show dev dummy0 + [ "$status" -eq 0 ] +} + +@test "move network device to precreated container network namespace" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + 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 + [ "$status" -eq 0 ] + + ip netns del "$ns_name" +} + +@test "move network device to precreated container network namespace and set ip address" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # set a custom address to the interface + # set the interface down to avoid network problems + ip link set down dev dummy0 + ip address add 169.254.169.77 dev dummy0 + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"169.254.169.77"* ]] + + # verify the interface is still present in the network namespace + ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"169.254.169.77"* ]] + + ip netns del "$ns_name" +} + +@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"]' + + # set a custom address to the interface + # set the interface down to avoid network problems + ip link set down dev dummy0 + ip address add 127.0.0.33 dev dummy0 + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" != *"127.0.0.33"* ]] + + # verify the interface is still present in the network namespace + ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + + ip netns del "$ns_name" +} + +@test "move network device to precreated container network namespace and set mtu" { + update_config ' .linux.netDevices |= {"dummy0": {} } + | .process.args |= ["ip", "address", "show", "dev", "dummy0"]' + + # set a custom mtu to the interface + ip link set mtu 1789 dev dummy0 + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"mtu 1789"* ]] + + # verify the interface is still present in the network namespace + ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"mtu 1789"* ]] + + ip netns del "$ns_name" +} + +@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"]' + + # set a custom mac address to the interface + ip link set address 00:11:22:33:44:55 dev dummy0 + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"ether 00:11:22:33:44:55"* ]] + + # verify the interface is still present in the network namespace + ip netns exec "$ns_name" ip address show dev dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"ether 00:11:22:33:44:55"* ]] + + ip netns del "$ns_name" +} + +@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"]' + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + 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 + [ "$status" -eq 0 ] + + ip netns del "$ns_name" +} + +@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"]' + + # set a custom mtu to the interface + ip link set mtu 1789 dev dummy0 + # set a custom mac address to the interface + ip link set address 00:11:22:33:44:55 dev dummy0 + # set a custom ip address to the interface + ip address add 169.254.169.78 dev dummy0 + + # create a temporary name for the test network namespace + tmp=$(mktemp) + rm -f "$tmp" + ns_name=$(basename "$tmp") + # create network namespace + ip netns add "$ns_name" + ns_path=$(ip netns add "$ns_name" 2>&1 | sed -e 's/.*"\(.*\)".*/\1/') + + # tell runc which network namespace to use + update_config '(.. | select(.type? == "network")) .path |= "'"$ns_path"'"' + + runc run test_busybox + [ "$status" -eq 0 ] + [[ "$output" == *"169.254.169.78"* ]] + [[ "$output" == *"ether 00:11:22:33:44:55"* ]] + [[ "$output" == *"mtu 1789"* ]] + + # verify the interface is still present in the network namespace + ip netns exec "$ns_name" ip address show dev ctr_dummy0 + [ "$status" -eq 0 ] + [[ "$output" == *"169.254.169.78"* ]] + [[ "$output" == *"ether 00:11:22:33:44:55"* ]] + [[ "$output" == *"mtu 1789"* ]] + + ip netns del "$ns_name" +} diff --git a/tests/integration/userns.bats b/tests/integration/userns.bats index 1300dff9f7c..d00761244d4 100644 --- a/tests/integration/userns.bats +++ b/tests/integration/userns.bats @@ -246,3 +246,37 @@ 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 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 deleted during the namespace cleanup + run ip link del dummy0 + [ "$status" -ne 0 ] +}