From 37c3d174d7c7dffa3b01f2b16542cd96f3c6eddc Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 19 Nov 2024 11:51:30 +0000 Subject: [PATCH 1/2] TEMP vendor proposal for OCI runtime spec for netdevices Vendor the OCI spec with the Network Devices support Signed-off-by: Antonio Ojea --- .../runtime-spec/specs-go/config.go | 24 ++++++++++++++++++- .../specs-go/features/features.go | 8 +++++++ .../runtime-spec/specs-go/version.go | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) 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 d1236ba7213..e9045e9a983 100644 --- a/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go +++ b/vendor/github.com/opencontainers/runtime-spec/specs-go/config.go @@ -94,10 +94,12 @@ type Process struct { SelinuxLabel string `json:"selinuxLabel,omitempty" platform:"linux"` // IOPriority contains the I/O priority settings for the cgroup. IOPriority *LinuxIOPriority `json:"ioPriority,omitempty" platform:"linux"` + // ExecCPUAffinity specifies CPU affinity for exec processes. + ExecCPUAffinity *CPUAffinity `json:"execCPUAffinity,omitempty" platform:"linux"` } // LinuxCapabilities specifies the list of allowed capabilities that are kept for a process. -// http://man7.org/linux/man-pages/man7/capabilities.7.html +// https://man7.org/linux/man-pages/man7/capabilities.7.html type LinuxCapabilities struct { // Bounding is the set of capabilities checked by the kernel. Bounding []string `json:"bounding,omitempty" platform:"linux"` @@ -127,6 +129,12 @@ const ( IOPRIO_CLASS_IDLE IOPriorityClass = "IOPRIO_CLASS_IDLE" ) +// CPUAffinity specifies process' CPU affinity. +type CPUAffinity struct { + Initial string `json:"initial,omitempty"` + Final string `json:"final,omitempty"` +} + // Box specifies dimensions of a rectangle. Used for specifying the size of a console. type Box struct { // Height is the vertical dimension of a box. @@ -228,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. @@ -483,6 +493,18 @@ 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"` + // Addresses is the list of IP addresses, IPv4 or IPv6, in CIDR format in the container namespace + Addresses []string `json:"addresses,omitempty"` + // HardwareAddress represents the hardware address (e.g. MAC Address) of the device's network interface + HardwareAddress string `json:"hardwareAddress,omitempty"` + // MTU Maximum Transfer Unit of the network device in the container namespace + MTU uint32 `json:"mtu,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 503971e058b..f6c15f6c35c 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 = 0 // VersionDev indicates development branch. Releases will be empty string. - VersionDev = "" + VersionDev = "+dev" ) // Version is the specification version that the package types support. From d114afe2aa023bc124a67ea8747409cda1455aeb Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Tue, 19 Nov 2024 16:10:22 +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: I1306a783b84ead7b03eea679941bd4db9dc1e353 Signed-off-by: Antonio Ojea --- features.go | 3 + libcontainer/configs/config.go | 3 + libcontainer/configs/netdevices.go | 13 ++ libcontainer/configs/validate/validator.go | 55 ++++++ .../configs/validate/validator_test.go | 171 ++++++++++++++++++ libcontainer/factory_linux.go | 12 ++ libcontainer/network_linux.go | 140 ++++++++++++++ libcontainer/specconv/spec_linux.go | 11 ++ libcontainer/specconv/spec_linux_test.go | 108 +++++++++++ libcontainer/state_linux.go | 17 ++ tests/integration/netdev.bats | 119 ++++++++++++ 11 files changed, 652 insertions(+) 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/libcontainer/configs/config.go b/libcontainer/configs/config.go index 22fe0f9b4c1..b27a2dc781e 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -115,6 +115,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"` // 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..da1336a5f4e --- /dev/null +++ b/libcontainer/configs/netdevices.go @@ -0,0 +1,13 @@ +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"` + // Address is the IP address and Prefix in the container namespace in CIDR fornat + Addresses []string `json:"addresses,omitempty"` + // HardwareAddres represents a physical hardware address. + HardwareAddress string `json:"hardwareAddress,omitempty"` + // MTU Maximum Transfer Unit of the network device in the container namespace + MTU uint32 `json:"mtu,omitempty"` +} diff --git a/libcontainer/configs/validate/validator.go b/libcontainer/configs/validate/validator.go index 37ece0aebbd..023e9a51bdd 100644 --- a/libcontainer/configs/validate/validator.go +++ b/libcontainer/configs/validate/validator.go @@ -3,6 +3,8 @@ package validate import ( "errors" "fmt" + "net" + "net/netip" "os" "path/filepath" "strings" @@ -24,6 +26,7 @@ func Validate(config *configs.Config) error { cgroupsCheck, rootfs, network, + netdevices, uts, security, namespaces, @@ -70,6 +73,58 @@ 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.Contains(name, "/") || strings.Contains(name, ":") || strings.Contains(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 private NET namespace") + } + path := config.Namespaces.PathOf(configs.NEWNET) + if path == "" { + return errors.New("unable to move network devices without a private 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 != "" { + if !devValidName(netdev.Name) { + return fmt.Errorf("invalid network device name %q", netdev.Name) + } + } + for _, address := range netdev.Addresses { + if _, err := netip.ParsePrefix(address); err != nil { + return fmt.Errorf("invalid network IP address %q", address) + } + } + if netdev.HardwareAddress != "" { + if _, err := net.ParseMAC(netdev.HardwareAddress); err != nil { + return fmt.Errorf("invalid hardware address %q", netdev.HardwareAddress) + } + } + } + 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 b0b740a122d..575838604b2 100644 --- a/libcontainer/configs/validate/validator_test.go +++ b/libcontainer/configs/validate/validator_test.go @@ -871,3 +871,174 @@ 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", + Addresses: []string{"192.168.2.34/24", "2001:db8::2/64"}, + HardwareAddress: "82:06:8c:49:7a:4a", + MTU: 1500, + }, + }, + }, + }, + { + name: "network device host network", + isErr: true, + config: &configs.Config{ + Namespaces: configs.Namespaces( + []configs.Namespace{}, + ), + 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", + }, + }, + ), + 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/", + }, + }, + }, + }, + { + name: "network device wrong ip", + 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", + Addresses: []string{"wrongip"}, + }, + }, + }, + }, + { + name: "network device wrong mac", + 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", + Addresses: []string{"192.168.1.1/24"}, + HardwareAddress: "wrongmac!", + }, + }, + }, + }, + } + + 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) + } + }) + } +} diff --git a/libcontainer/factory_linux.go b/libcontainer/factory_linux.go index b13f8bf9bb3..6e549e00ce0 100644 --- a/libcontainer/factory_linux.go +++ b/libcontainer/factory_linux.go @@ -90,6 +90,18 @@ func Create(root, id string, config *configs.Config) (*Container, error) { if err := os.Mkdir(stateDir, 0o711); err != nil { return nil, err } + + // move the specified devices to the container network namespace + nsPath := config.Namespaces.PathOf(configs.NEWNET) + if nsPath != "" { + for name, netDevice := range config.NetDevices { + err := netnsAttach(name, nsPath, *netDevice) + if err != nil { + return nil, err + } + } + } + c := &Container{ id: id, stateDir: stateDir, diff --git a/libcontainer/network_linux.go b/libcontainer/network_linux.go index 8915548b3bc..615b26a29b5 100644 --- a/libcontainer/network_linux.go +++ b/libcontainer/network_linux.go @@ -3,13 +3,16 @@ package libcontainer import ( "bytes" "fmt" + "net" "os" "path/filepath" "strconv" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/types" + "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" ) var strategies = map[string]networkStrategy{ @@ -98,3 +101,140 @@ func (l *loopback) attach(n *configs.Network) (err error) { func (l *loopback) detach(n *configs.Network) (err error) { return nil } + +// netnsAttach takes the network device referenced by name in the current network namespace +// and moves to the network namespace passed as a parameter. It also configure the +// network device inside the new network namespace with the passed parameters. +func netnsAttach(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) + } + attrs := netlink.NewLinkAttrs() + attrs.Index = link.Attrs().Index + + attrs.Name = name + if device.Name != "" { + attrs.Name = device.Name + } + + attrs.MTU = link.Attrs().MTU + if device.MTU > 0 { + attrs.MTU = int(device.MTU) + } + + attrs.HardwareAddr = link.Attrs().HardwareAddr + if device.HardwareAddress != "" { + attrs.HardwareAddr, err = net.ParseMAC(device.HardwareAddress) + if err != nil { + return err + } + } + + 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) + } + + attrs.Namespace = netlink.NsFd(ns) + + // set the interface down before we change the address inside the network namespace + err = netlink.LinkSetDown(link) + if err != nil { + return err + } + + dev := &netlink.Device{ + LinkAttrs: attrs, + } + + err = netlink.LinkModify(dev) + if err != nil { + return fmt.Errorf("could not modify network device %s : %w", name, 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 + } + + nsLink, err := nhNs.LinkByName(dev.Name) + if err != nil { + return fmt.Errorf("link not found for interface %s on namespace %s: %w", dev.Name, nsPath, err) + } + + err = nhNs.LinkSetUp(nsLink) + if err != nil { + return fmt.Errorf("failt to set up interface %s on namespace %s: %w", nsLink.Attrs().Name, nsPath, err) + } + + for _, address := range device.Addresses { + addr, err := netlink.ParseAddr(address) + if err != nil { + return err + } + + err = nhNs.AddrAdd(nsLink, addr) + if err != nil { + return err + } + } + return nil +} + +// netnsDettach takes the network device referenced by name in the passed network namespace +// and moves to the root network namespace, restoring the original name. It also sets down +// the network device to avoid conflict with existing network configuraiton. +func netnsDettach(name string, nsPath string, device configs.LinuxNetDevice) error { + logrus.Debugf("dettaching network device %s with attrs %#v to network namespace %s", name, device, nsPath) + 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) + } + // 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 fmt.Errorf("could not get network namespace handle: %w", err) + } + + devName := device.Name + if devName == "" { + devName = name + } + + nsLink, err := nhNs.LinkByName(devName) + if err != nil { + return fmt.Errorf("link not found for interface %s on namespace %s: %w", device.Name, nsPath, err) + } + + // set the device down to avoid network conflicts + // when it is restored to the original namespace + err = nhNs.LinkSetDown(nsLink) + if err != nil { + return err + } + + // restore the original name if it was renamed + if device.Name != name { + err = nhNs.LinkSetName(nsLink, name) + if err != nil { + return err + } + } + + rootNs, err := netns.Get() + if err != nil { + return err + } + defer rootNs.Close() + + err = nhNs.LinkSetNsFd(nsLink, int(netlink.NsFd(rootNs))) + if err != nil { + return fmt.Errorf("failed to restore original network namespace: %w", err) + } + return nil +} diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index e7c6faae347..ab87b3062e7 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -472,6 +472,17 @@ 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, + Addresses: netdev.Addresses, + HardwareAddress: netdev.HardwareAddress, + MTU: netdev.MTU, + } + } } // 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 8c7fb774f97..9ca627f330b 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -2,6 +2,7 @@ package specconv import ( "os" + "reflect" "strings" "testing" @@ -956,3 +957,110 @@ 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", + }, + }, + }, + { + name: "multiple network devices and addresses", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": { + Addresses: []string{"192.168.1.2/24", "fd00:1:2::9/64"}, + }, + "eth2": { + Name: "ctr_eth2", + }, + }, + }, + { + name: "multiple network devices and hardware address", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": { + Addresses: []string{"192.168.1.2/24", "fd00:1:2::9/64"}, + HardwareAddress: "e2:85:68:80:43:7a ", + }, + "eth2": { + Name: "ctr_eth2", + }, + }, + }, + { + name: "multiple network devices and mtu", + netDevices: map[string]specs.LinuxNetDevice{ + "eth1": { + Addresses: []string{"192.168.1.2/24", "fd00:1:2::9/64"}, + HardwareAddress: "e2:85:68:80:43:7a ", + }, + "eth2": { + Name: "ctr_eth2", + MTU: 1725, + }, + }, + }, + } + + 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) + } + if !reflect.DeepEqual(ctrNetDev.Addresses, netdev.Addresses) { + t.Fatalf("expected %v got %v", ctrNetDev.Addresses, netdev.Addresses) + } + if ctrNetDev.HardwareAddress != netdev.HardwareAddress { + t.Fatalf("expected %s got %s", ctrNetDev.HardwareAddress, netdev.HardwareAddress) + } + if ctrNetDev.MTU != netdev.MTU { + t.Fatalf("expected %d got %d", ctrNetDev.MTU, netdev.MTU) + } + } + }) + } + +} diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go index ad96f0801ea..d129908fbd1 100644 --- a/libcontainer/state_linux.go +++ b/libcontainer/state_linux.go @@ -7,6 +7,7 @@ import ( "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runtime-spec/specs-go" + "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) @@ -47,6 +48,22 @@ func destroy(c *Container) error { // Likely to fail when c.config.RootlessCgroups is true _ = signalAllProcesses(c.cgroupManager, unix.SIGKILL) } + + // restore network devices + nsPath := c.config.Namespaces.PathOf(configs.NEWNET) + + if nsPath != "" { + for name, netDevice := range c.config.NetDevices { + err := netnsDettach(name, nsPath, *netDevice) + if err != nil { + // don't fail on the interface detachment to avoid problems with the container shutdown. + // In the worst case the OS will handle the cleanup, hardware interfaces will be back on the + // root namespace and virtual devices will be destroyed. + logrus.WithError(err).Warnf("failed to restore network device %s from network namespace %s", name, nsPath) + } + } + } + if err := c.cgroupManager.Destroy(); err != nil { return fmt.Errorf("unable to remove container's cgroup: %w", err) } diff --git a/tests/integration/netdev.bats b/tests/integration/netdev.bats new file mode 100644 index 00000000000..5686e2aa9d0 --- /dev/null +++ b/tests/integration/netdev.bats @@ -0,0 +1,119 @@ +#!/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 + ip link set up dev dummy0 + ip addr add 169.254.169.13/32 dev dummy0 +} + +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"]' + + # 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 ] + + ip netns del "$ns_name" +} + +@test "move network device to 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 ] + + ip netns del "$ns_name" +} + +@test "move network device to container network namespace and change ipv4 address" { + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" , "addresses" : [ "10.0.0.2/24" ]} } + | .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 ] + [[ "$output" == *"10.0.0.2/24"* ]] + + ip netns del "$ns_name" +} + +@test "move network device to container network namespace and change ipv6 address" { + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" , "addresses" : [ "10.0.0.2/24" , "2001:db8::2/64" ]} } + | .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 ] + [[ "$output" == *"2001:db8::2/64"* ]] + + ip netns del "$ns_name" +} + +@test "network device on root namespace fails" { + update_config ' .linux.netDevices |= {"dummy0": {} }' + runc run test_busybox + [ "$status" -ne 0 ] + [[ "$output" == *"unable to move network devices without a private NET namespace"* ]] +} + +@test "network device bad address fails" { + update_config '(.. | select(.type? == "network")) .path |= "'fake_net_ns'"' + update_config ' .linux.netDevices |= { "dummy0": { "name" : "ctr_dummy0" , "addresses" : [ "wrong_ip" ]} }' + + runc run test_busybox + [ "$status" -ne 0 ] + [[ "$output" == *"invalid network IP address"* ]] +}