mirror of
https://github.com/opencontainers/runc.git
synced 2025-09-26 19:41:35 +08:00
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:

committed by
Antonio Ojea

parent
889c7b272f
commit
8d180e9658
@@ -63,6 +63,9 @@ var featuresCommand = cli.Command{
|
||||
Enabled: &t,
|
||||
},
|
||||
},
|
||||
NetDevices: &features.NetDevices{
|
||||
Enabled: &t,
|
||||
},
|
||||
},
|
||||
PotentiallyUnsafeConfigAnnotations: []string{
|
||||
"bundle",
|
||||
|
2
go.mod
2
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
|
||||
)
|
||||
|
@@ -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.
|
||||
|
7
libcontainer/configs/netdevices.go
Normal file
7
libcontainer/configs/netdevices.go
Normal 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"`
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -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 ]
|
||||
|
216
tests/integration/netdev.bats
Normal file
216
tests/integration/netdev.bats
Normal 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 "* ]]
|
||||
}
|
@@ -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 ]
|
||||
}
|
||||
|
Reference in New Issue
Block a user