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 <aojea@google.com>
This commit is contained in:
Antonio Ojea
2025-02-06 08:43:32 +00:00
committed by Antonio Ojea
parent 889c7b272f
commit 8d180e9658
13 changed files with 794 additions and 1 deletions

View File

@@ -63,6 +63,9 @@ var featuresCommand = cli.Command{
Enabled: &t,
},
},
NetDevices: &features.NetDevices{
Enabled: &t,
},
},
PotentiallyUnsafeConfigAnnotations: []string{
"bundle",

2
go.mod
View File

@@ -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
)

View File

@@ -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.

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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)
}
})
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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)
}
}
})
}
}

View File

@@ -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 ]

View File

@@ -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 "* ]]
}

View File

@@ -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 ]
}