Skip to content

Commit

Permalink
Add support for Linux Network Devices
Browse files Browse the repository at this point in the history
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: I190d5c444f03e65bbbfe5b4bc90809c0ad0a2017
Signed-off-by: Antonio Ojea <aojea@google.com>
  • Loading branch information
aojea committed Feb 10, 2025
1 parent 771984c commit f53d263
Show file tree
Hide file tree
Showing 11 changed files with 546 additions and 0 deletions.
3 changes: 3 additions & 0 deletions features.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ var featuresCommand = cli.Command{
Enabled: &t,
},
},
NetDevices: &features.NetDevices{
Enabled: &t,
},
},
PotentiallyUnsafeConfigAnnotations: []string{
"bundle",
Expand Down
3 changes: 3 additions & 0 deletions libcontainer/configs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions libcontainer/configs/netdevices.go
Original file line number Diff line number Diff line change
@@ -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"`
}
40 changes: 40 additions & 0 deletions libcontainer/configs/validate/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func Validate(config *configs.Config) error {
cgroupsCheck,
rootfs,
network,
netdevices,
uts,
security,
namespaces,
Expand Down Expand Up @@ -70,6 +71,45 @@ 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.Namespaces.IsPrivate(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 {
Expand Down
169 changes: 169 additions & 0 deletions libcontainer/configs/validate/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package validate
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/opencontainers/runc/libcontainer/configs"
Expand Down Expand Up @@ -877,3 +878,171 @@ 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 host network",
isErr: true,
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)
}
})
}
}
11 changes: 11 additions & 0 deletions libcontainer/factory_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,17 @@ 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
if nsPath := config.Namespaces.PathOf(configs.NEWNET); nsPath != "" {
for name, netDevice := range config.NetDevices {
err := netnsAttach(name, nsPath, *netDevice)
if err != nil {
return nil, err
}
}
}

c := &Container{
id: id,
stateDir: stateDir,
Expand Down
Loading

0 comments on commit f53d263

Please sign in to comment.