Skip to content

Commit f4f5d02

Browse files
committed
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 <[email protected]>
1 parent 76e637a commit f4f5d02

11 files changed

+713
-0
lines changed

features.go

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ var featuresCommand = cli.Command{
6363
Enabled: &t,
6464
},
6565
},
66+
NetDevices: &features.NetDevices{
67+
Enabled: &t,
68+
},
6669
},
6770
PotentiallyUnsafeConfigAnnotations: []string{
6871
"bundle",

libcontainer/configs/config.go

+3
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ type Config struct {
115115
// 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!
116116
Devices []*devices.Device `json:"devices"`
117117

118+
// NetDevices are key-value pairs, keyed by network device name, moved to the container's network namespace.
119+
NetDevices map[string]*LinuxNetDevice `json:"netDevices"`
120+
118121
MountLabel string `json:"mount_label"`
119122

120123
// Hostname optionally sets the container's hostname if provided

libcontainer/configs/netdevices.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package configs
2+
3+
// LinuxNetDevice represents a single network device to be added to the container's network namespace
4+
type LinuxNetDevice struct {
5+
// Name of the device in the container namespace
6+
Name string `json:"name,omitempty"`
7+
// Address is the IP address and Prefix in the container namespace in CIDR fornat
8+
Addresses []string `json:"addresses,omitempty"`
9+
// HardwareAddres represents a physical hardware address.
10+
HardwareAddress string `json:"hardwareAddress,omitempty"`
11+
// MTU Maximum Transfer Unit of the network device in the container namespace
12+
MTU uint32 `json:"mtu,omitempty"`
13+
}

libcontainer/configs/validate/validator.go

+55
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package validate
33
import (
44
"errors"
55
"fmt"
6+
"net"
7+
"net/netip"
68
"os"
79
"path/filepath"
810
"strings"
@@ -24,6 +26,7 @@ func Validate(config *configs.Config) error {
2426
cgroupsCheck,
2527
rootfs,
2628
network,
29+
netdevices,
2730
uts,
2831
security,
2932
namespaces,
@@ -70,6 +73,58 @@ func rootfs(config *configs.Config) error {
7073
return nil
7174
}
7275

76+
// https://elixir.bootlin.com/linux/v6.12/source/net/core/dev.c#L1066
77+
func devValidName(name string) bool {
78+
if len(name) == 0 || len(name) > unix.IFNAMSIZ {
79+
return false
80+
}
81+
if (name == ".") || (name == "..") {
82+
return false
83+
}
84+
if strings.Contains(name, "/") || strings.Contains(name, ":") || strings.Contains(name, " ") {
85+
return false
86+
}
87+
return true
88+
}
89+
90+
func netdevices(config *configs.Config) error {
91+
if len(config.NetDevices) == 0 {
92+
return nil
93+
}
94+
if !config.Namespaces.Contains(configs.NEWNET) {
95+
return errors.New("unable to move network devices without a private NET namespace")
96+
}
97+
path := config.Namespaces.PathOf(configs.NEWNET)
98+
if path == "" {
99+
return errors.New("unable to move network devices without a private NET namespace")
100+
}
101+
if config.RootlessEUID || config.RootlessCgroups {
102+
return errors.New("network devices are not supported for rootless containers")
103+
}
104+
105+
for name, netdev := range config.NetDevices {
106+
if !devValidName(name) {
107+
return fmt.Errorf("invalid network device name %q", name)
108+
}
109+
if netdev.Name != "" {
110+
if !devValidName(netdev.Name) {
111+
return fmt.Errorf("invalid network device name %q", netdev.Name)
112+
}
113+
}
114+
for _, address := range netdev.Addresses {
115+
if _, err := netip.ParsePrefix(address); err != nil {
116+
return fmt.Errorf("invalid network IP address %q", address)
117+
}
118+
}
119+
if netdev.HardwareAddress != "" {
120+
if _, err := net.ParseMAC(netdev.HardwareAddress); err != nil {
121+
return fmt.Errorf("invalid hardware address %q", netdev.HardwareAddress)
122+
}
123+
}
124+
}
125+
return nil
126+
}
127+
73128
func network(config *configs.Config) error {
74129
if !config.Namespaces.Contains(configs.NEWNET) {
75130
if len(config.Networks) > 0 || len(config.Routes) > 0 {

libcontainer/configs/validate/validator_test.go

+171
Original file line numberDiff line numberDiff line change
@@ -871,3 +871,174 @@ func TestValidateIOPriority(t *testing.T) {
871871
}
872872
}
873873
}
874+
875+
func TestValidateNetDevices(t *testing.T) {
876+
testCases := []struct {
877+
name string
878+
isErr bool
879+
config *configs.Config
880+
}{
881+
{
882+
name: "network device",
883+
config: &configs.Config{
884+
Namespaces: configs.Namespaces(
885+
[]configs.Namespace{
886+
{
887+
Type: configs.NEWNET,
888+
Path: "/var/run/netns/blue",
889+
},
890+
},
891+
),
892+
NetDevices: map[string]*configs.LinuxNetDevice{
893+
"eth0": {},
894+
},
895+
},
896+
},
897+
{
898+
name: "network device rename",
899+
config: &configs.Config{
900+
Namespaces: configs.Namespaces(
901+
[]configs.Namespace{
902+
{
903+
Type: configs.NEWNET,
904+
Path: "/var/run/netns/blue",
905+
},
906+
},
907+
),
908+
NetDevices: map[string]*configs.LinuxNetDevice{
909+
"eth0": {
910+
Name: "c0",
911+
Addresses: []string{"192.168.2.34/24", "2001:db8::2/64"},
912+
HardwareAddress: "82:06:8c:49:7a:4a",
913+
MTU: 1500,
914+
},
915+
},
916+
},
917+
},
918+
{
919+
name: "network device host network",
920+
isErr: true,
921+
config: &configs.Config{
922+
Namespaces: configs.Namespaces(
923+
[]configs.Namespace{},
924+
),
925+
NetDevices: map[string]*configs.LinuxNetDevice{
926+
"eth0": {},
927+
},
928+
},
929+
},
930+
{
931+
name: "network device rootless",
932+
isErr: true,
933+
config: &configs.Config{
934+
Namespaces: configs.Namespaces(
935+
[]configs.Namespace{
936+
{
937+
Type: configs.NEWNET,
938+
Path: "/var/run/netns/blue",
939+
},
940+
},
941+
),
942+
RootlessEUID: true,
943+
NetDevices: map[string]*configs.LinuxNetDevice{
944+
"eth0": {},
945+
},
946+
},
947+
},
948+
{
949+
name: "network device rootless",
950+
isErr: true,
951+
config: &configs.Config{
952+
Namespaces: configs.Namespaces(
953+
[]configs.Namespace{
954+
{
955+
Type: configs.NEWNET,
956+
Path: "/var/run/netns/blue",
957+
},
958+
},
959+
),
960+
RootlessCgroups: true,
961+
NetDevices: map[string]*configs.LinuxNetDevice{
962+
"eth0": {},
963+
},
964+
},
965+
},
966+
{
967+
name: "network device bad name",
968+
isErr: true,
969+
config: &configs.Config{
970+
Namespaces: configs.Namespaces(
971+
[]configs.Namespace{
972+
{
973+
Type: configs.NEWNET,
974+
Path: "/var/run/netns/blue",
975+
},
976+
},
977+
),
978+
NetDevices: map[string]*configs.LinuxNetDevice{
979+
"eth0": {
980+
Name: "eth0/",
981+
},
982+
},
983+
},
984+
},
985+
{
986+
name: "network device wrong ip",
987+
isErr: true,
988+
config: &configs.Config{
989+
Namespaces: configs.Namespaces(
990+
[]configs.Namespace{
991+
{
992+
Type: configs.NEWNET,
993+
Path: "/var/run/netns/blue",
994+
},
995+
},
996+
),
997+
NetDevices: map[string]*configs.LinuxNetDevice{
998+
"eth0": {
999+
Name: "eth0",
1000+
Addresses: []string{"wrongip"},
1001+
},
1002+
},
1003+
},
1004+
},
1005+
{
1006+
name: "network device wrong mac",
1007+
isErr: true,
1008+
config: &configs.Config{
1009+
Namespaces: configs.Namespaces(
1010+
[]configs.Namespace{
1011+
{
1012+
Type: configs.NEWNET,
1013+
Path: "/var/run/netns/blue",
1014+
},
1015+
},
1016+
),
1017+
NetDevices: map[string]*configs.LinuxNetDevice{
1018+
"eth0": {
1019+
Name: "eth0",
1020+
Addresses: []string{"192.168.1.1/24"},
1021+
HardwareAddress: "wrongmac!",
1022+
},
1023+
},
1024+
},
1025+
},
1026+
}
1027+
1028+
for _, tc := range testCases {
1029+
tc := tc
1030+
t.Run(tc.name, func(t *testing.T) {
1031+
config := tc.config
1032+
config.Rootfs = "/var"
1033+
1034+
err := Validate(config)
1035+
if tc.isErr && err == nil {
1036+
t.Error("expected error, got nil")
1037+
}
1038+
1039+
if !tc.isErr && err != nil {
1040+
t.Error(err)
1041+
}
1042+
})
1043+
}
1044+
}

libcontainer/factory_linux.go

+12
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ func Create(root, id string, config *configs.Config) (*Container, error) {
9191
if err := os.Mkdir(stateDir, 0o711); err != nil {
9292
return nil, err
9393
}
94+
95+
// move the specified devices to the container network namespace
96+
nsPath := config.Namespaces.PathOf(configs.NEWNET)
97+
if nsPath != "" {
98+
for name, netDevice := range config.NetDevices {
99+
err := netnsAttach(name, nsPath, *netDevice)
100+
if err != nil {
101+
return nil, err
102+
}
103+
}
104+
}
105+
94106
c := &Container{
95107
id: id,
96108
stateDir: stateDir,

0 commit comments

Comments
 (0)