diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a1b4d7b64..74b433a175 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -104,6 +104,50 @@ jobs: - name: "Run integration tests" run: docker run -t --rm --privileged test-integration + test-integration-ipv6: + runs-on: "ubuntu-${{ matrix.ubuntu }}" + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + # ubuntu-20.04: cgroup v1, ubuntu-22.04: cgroup v2 + include: + - ubuntu: 22.04 + containerd: v1.7.7 + env: + UBUNTU_VERSION: "${{ matrix.ubuntu }}" + CONTAINERD_VERSION: "${{ matrix.containerd }}" + steps: + - uses: actions/checkout@v4.1.1 + with: + fetch-depth: 1 + - name: Enable ipv4 and ipv6 forwarding + run: | + sudo sysctl -w net.ipv6.conf.all.forwarding=1 + sudo sysctl -w net.ipv4.ip_forward=1 + - name: Enable IPv6 for Docker + run: | + sudo mkdir -p /etc/docker + echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker + - name: "Prepare integration test environment" + run: DOCKER_BUILDKIT=1 docker build -t test-integration-ipv6 --target test-integration-ipv6 --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . + - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" + run: | + sudo systemctl disable --now snapd.service snapd.socket + sudo apt-get purge -y snapd + sudo losetup -Dv + sudo losetup -lv + - name: "Register QEMU (tonistiigi/binfmt)" + run: docker run --privileged --rm tonistiigi/binfmt --install all + - name: "Run integration tests" + # The nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config. + # Therefore it's hard to debug why the IPv6 tests fail in such an isolation layer. + # On the other side, using the host network is easier at configuration. + # Besides, each job is running on a different instance, which means using host network here + # is safe and has no side effects on others. + run: docker run --network host -t --rm --privileged test-integration-ipv6 + test-integration-rootless: runs-on: "ubuntu-${{ matrix.ubuntu }}" timeout-minutes: 60 @@ -200,6 +244,8 @@ jobs: sudo apt-get install -y expect - name: "Ensure that the integration test suite is compatible with Docker" run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon + - name: "Ensure that the IPv6 integration test suite is compatible with Docker" + run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon -test.ipv6 test-integration-windows: # A "larger" runner is used for enabling Hyper-V containers diff --git a/Dockerfile b/Dockerfile index bdfbf14bbb..1626cc56d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -341,4 +341,8 @@ FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port-slirp4netns.conf /home/rootless/.config/systemd/user/containerd.service.d/port-slirp4netns.conf RUN chown -R rootless:rootless /home/rootless/.config +FROM test-integration AS test-integration-ipv6 +CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=github.com/containerd/nerdctl/cmd/nerdctl/...", \ + "--", "-timeout=30m", "-args", "-test.kill-daemon", "-test.ipv6"] + FROM base AS demo diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index 5f2d60aeb7..44720eea3a 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -120,8 +120,8 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringSlice("dns-option", nil, "Set DNS options") // publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman) cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host") - // FIXME: not support IPV6 yet cmd.Flags().String("ip", "", "IPv4 address to assign to the container") + cmd.Flags().String("ip6", "", "IPv6 address to assign to the container") cmd.Flags().StringP("hostname", "h", "", "Container host name") cmd.Flags().String("mac-address", "", "MAC address to assign to the container") // #endregion diff --git a/cmd/nerdctl/container_run_network.go b/cmd/nerdctl/container_run_network.go index 8fc178c4bf..e2d25a047e 100644 --- a/cmd/nerdctl/container_run_network.go +++ b/cmd/nerdctl/container_run_network.go @@ -77,6 +77,13 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) { } netOpts.IPAddress = ipAddress + // --ip6= + ip6Address, err := cmd.Flags().GetString("ip6") + if err != nil { + return netOpts, err + } + netOpts.IP6Address = ip6Address + // -h/--hostname= hostName, err := cmd.Flags().GetString("hostname") if err != nil { diff --git a/cmd/nerdctl/container_run_network_linux_test.go b/cmd/nerdctl/container_run_network_linux_test.go index 9f0046ad14..e8b3d375b9 100644 --- a/cmd/nerdctl/container_run_network_linux_test.go +++ b/cmd/nerdctl/container_run_network_linux_test.go @@ -19,6 +19,7 @@ package main import ( "fmt" "io" + "net" "regexp" "runtime" "strings" @@ -491,3 +492,73 @@ func TestRunContainerWithMACAddress(t *testing.T) { } } } + +func TestRunContainerWithStaticIP6(t *testing.T) { + if rootlessutil.IsRootless() { + t.Skip("Static IP6 assignment is not supported rootless mode yet.") + } + networkName := "test-network" + networkSubnet := "2001:db8:5::/64" + _, subnet, err := net.ParseCIDR(networkSubnet) + assert.Assert(t, err == nil) + base := testutil.NewBaseWithIPv6Compatible(t) + base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK() + t.Cleanup(func() { + base.Cmd("network", "rm", networkName).Run() + }) + testCases := []struct { + ip string + shouldSuccess bool + checkTheIPAddress bool + }{ + { + ip: "", + shouldSuccess: true, + checkTheIPAddress: false, + }, + { + ip: "2001:db8:5::6", + shouldSuccess: true, + checkTheIPAddress: true, + }, + { + ip: "2001:db8:4::6", + shouldSuccess: false, + checkTheIPAddress: false, + }, + } + tID := testutil.Identifier(t) + for i, tc := range testCases { + i := i + tc := tc + tcName := fmt.Sprintf("%+v", tc) + t.Run(tcName, func(t *testing.T) { + testContainerName := fmt.Sprintf("%s-%d", tID, i) + base := testutil.NewBaseWithIPv6Compatible(t) + args := []string{ + "run", "--rm", "--name", testContainerName, "--network", networkName, + } + if tc.ip != "" { + args = append(args, "--ip6", tc.ip) + } + args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...) + cmd := base.Cmd(args...) + if !tc.shouldSuccess { + cmd.AssertFail() + return + } + cmd.AssertOutWithFunc(func(stdout string) error { + ip := findIPv6(stdout) + if !subnet.Contains(ip) { + return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) + } + if tc.checkTheIPAddress { + if ip.String() != tc.ip { + return fmt.Errorf("expected ip %s, got %s", tc.ip, ip) + } + } + return nil + }) + }) + } +} diff --git a/cmd/nerdctl/network_create.go b/cmd/nerdctl/network_create.go index c7452277bd..b5c4f1e81e 100644 --- a/cmd/nerdctl/network_create.go +++ b/cmd/nerdctl/network_create.go @@ -44,10 +44,11 @@ func newNetworkCreateCommand() *cobra.Command { networkCreateCommand.Flags().String("ipam-driver", "default", "IP Address Management Driver") networkCreateCommand.RegisterFlagCompletionFunc("ipam-driver", shellCompleteIPAMDrivers) networkCreateCommand.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options") - networkCreateCommand.Flags().String("subnet", "", `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) + networkCreateCommand.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`) networkCreateCommand.Flags().String("gateway", "", `Gateway for the master subnet`) networkCreateCommand.Flags().String("ip-range", "", `Allocate container ip from a sub-range`) networkCreateCommand.Flags().StringArray("label", nil, "Set metadata for a network") + networkCreateCommand.Flags().Bool("ipv6", false, "Enable IPv6 networking") return networkCreateCommand } @@ -79,7 +80,7 @@ func networkCreateAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - subnetStr, err := cmd.Flags().GetString("subnet") + subnets, err := cmd.Flags().GetStringArray("subnet") if err != nil { return err } @@ -96,6 +97,10 @@ func networkCreateAction(cmd *cobra.Command, args []string) error { return err } labels = strutil.DedupeStrSlice(labels) + ipv6, err := cmd.Flags().GetBool("ipv6") + if err != nil { + return err + } return network.Create(types.NetworkCreateOptions{ GOptions: globalOptions, @@ -105,10 +110,11 @@ func networkCreateAction(cmd *cobra.Command, args []string) error { Options: strutil.ConvertKVStringsToMap(opts), IPAMDriver: ipamDriver, IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts), - Subnet: subnetStr, + Subnets: subnets, Gateway: gatewayStr, IPRange: ipRangeStr, Labels: labels, + IPv6: ipv6, }, }, cmd.OutOrStdout()) } diff --git a/cmd/nerdctl/network_create_linux_test.go b/cmd/nerdctl/network_create_linux_test.go index d055c10bb2..58b8fcecae 100644 --- a/cmd/nerdctl/network_create_linux_test.go +++ b/cmd/nerdctl/network_create_linux_test.go @@ -17,6 +17,9 @@ package main import ( + "fmt" + "net" + "strings" "testing" "github.com/containerd/nerdctl/pkg/testutil" @@ -54,3 +57,40 @@ func TestNetworkCreate(t *testing.T) { base.Cmd("run", "--rm", "--net", testNetwork+"-1", testutil.CommonImage, "ip", "route").AssertNoOut(net.IPAM.Config[0].Subnet) } + +func TestNetworkCreateIPv6(t *testing.T) { + base := testutil.NewBaseWithIPv6Compatible(t) + testNetwork := testutil.Identifier(t) + + subnetStr := "2001:db8:8::/64" + _, subnet, err := net.ParseCIDR(subnetStr) + assert.Assert(t, err == nil) + + base.Cmd("network", "create", "--ipv6", "--subnet", subnetStr, testNetwork).AssertOK() + t.Cleanup(func() { + base.Cmd("network", "rm", testNetwork).Run() + }) + + base.Cmd("run", "--rm", "--net", testNetwork, testutil.CommonImage, "ip", "addr", "show", "dev", "eth0").AssertOutWithFunc(func(stdout string) error { + ip := findIPv6(stdout) + if subnet.Contains(ip) { + return nil + } + return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) + }) +} + +func findIPv6(output string) net.IP { + var ipv6 string + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.Contains(line, "inet6") { + fields := strings.Fields(line) + if len(fields) > 1 { + ipv6 = strings.Split(fields[1], "/")[0] + break + } + } + } + return net.ParseIP(ipv6) +} diff --git a/docs/command-reference.md b/docs/command-reference.md index 610bde2e8d..3a5fa0d32f 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -183,6 +183,7 @@ Network flags: - :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip). `ip` could be a special string `host-gateway`, - which will be resolved to the `host-gateway-ip` in nerdctl.toml or global flag. - :whale: `--ip`: Specific static IP address(es) to use +- :whale: `--ip6`: Specific static IP6 address(es) to use. Should be used with user networks - :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not check if manually specified MAC addresses are unique. Supports network type `bridge` and `macvlan` @@ -375,7 +376,7 @@ IPFS flags: Unimplemented `docker run` flags: `--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`, - `--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--ip6`, `--isolation`, `--no-healthcheck`, + `--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`, `--link*`, `--mac-address`, `--publish-all`, `--sig-proxy`, `--storage-opt`, `--userns`, `--volume-driver` @@ -992,8 +993,9 @@ Flags: - :whale: `--gateway`: Gateway for the master subnet - :whale: `--ip-range`: Allocate container ip from a sub-range - :whale: `--label`: Set metadata on a network +- :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet. -Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--ipv6`, `--scope` +Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--scope` ### :whale: nerdctl network ls diff --git a/pkg/api/types/container_network_types.go b/pkg/api/types/container_network_types.go index c4476662b9..28e9a54102 100644 --- a/pkg/api/types/container_network_types.go +++ b/pkg/api/types/container_network_types.go @@ -28,6 +28,8 @@ type NetworkOptions struct { MACAddress string // IPAddress set specific static IP address(es) to use IPAddress string + // IP6Address set specific static IP6 address(es) to use + IP6Address string // Hostname set container host name Hostname string // DNSServers set custom DNS servers diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index e240987728..ca40bbe4c4 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -193,6 +193,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.hostname = netLabelOpts.Hostname internalLabels.ports = netLabelOpts.PortMappings internalLabels.ipAddress = netLabelOpts.IPAddress + internalLabels.ip6Address = netLabelOpts.IP6Address internalLabels.networks = netLabelOpts.NetworkSlice internalLabels.macAddress = netLabelOpts.MACAddress @@ -505,6 +506,7 @@ type internalLabels struct { // network networks []string ipAddress string + ip6Address string ports []gocni.PortMapping macAddress string // volume @@ -561,6 +563,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.IPAddress] = internalLabels.ipAddress } + if internalLabels.ip6Address != "" { + m[labels.IP6Address] = internalLabels.ip6Address + } + m[labels.Platform], err = platformutil.NormalizeString(internalLabels.platform) if err != nil { return nil, err diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index 4c0cc65d02..8a03243347 100644 --- a/pkg/cmd/network/create.go +++ b/pkg/cmd/network/create.go @@ -26,10 +26,11 @@ import ( ) func Create(options types.NetworkCreateOptions, stdout io.Writer) error { - if options.CreateOptions.Subnet == "" { + if len(options.CreateOptions.Subnets) == 0 { if options.CreateOptions.Gateway != "" || options.CreateOptions.IPRange != "" { return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually") } + options.CreateOptions.Subnets = []string{""} } e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath) diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 58c747a559..e11e96a995 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -60,6 +60,9 @@ const ( // IPAddress is the static IP address of the container assigned by the user IPAddress = Prefix + "ip" + // IP6Address is the static IP6 address of the container assigned by the user + IP6Address = Prefix + "ip6" + // LogURI is the log URI LogURI = Prefix + "log-uri" diff --git a/pkg/netutil/cni_plugin_unix.go b/pkg/netutil/cni_plugin_unix.go index 535a003847..588f339c66 100644 --- a/pkg/netutil/cni_plugin_unix.go +++ b/pkg/netutil/cni_plugin_unix.go @@ -31,12 +31,14 @@ type bridgeConfig struct { PromiscMode bool `json:"promiscMode,omitempty"` Vlan int `json:"vlan,omitempty"` IPAM map[string]interface{} `json:"ipam"` + Capabilities map[string]bool `json:"capabilities,omitempty"` } func newBridgePlugin(bridgeName string) *bridgeConfig { return &bridgeConfig{ - PluginType: "bridge", - BrName: bridgeName, + PluginType: "bridge", + BrName: bridgeName, + Capabilities: map[string]bool{}, } } @@ -46,16 +48,18 @@ func (*bridgeConfig) GetPluginType() string { // vlanConfig describes the macvlan/ipvlan config type vlanConfig struct { - PluginType string `json:"type"` - Master string `json:"master"` - Mode string `json:"mode,omitempty"` - MTU int `json:"mtu,omitempty"` - IPAM map[string]interface{} `json:"ipam"` + PluginType string `json:"type"` + Master string `json:"master"` + Mode string `json:"mode,omitempty"` + MTU int `json:"mtu,omitempty"` + IPAM map[string]interface{} `json:"ipam"` + Capabilities map[string]bool `json:"capabilities,omitempty"` } func newVLANPlugin(pluginType string) *vlanConfig { return &vlanConfig{ - PluginType: pluginType, + PluginType: pluginType, + Capabilities: map[string]bool{}, } } diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index 962bcc665b..f6e0079cd9 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -231,10 +231,11 @@ type CreateOptions struct { Options map[string]string IPAMDriver string IPAMOptions map[string]string - Subnet string + Subnets []string Gateway string IPRange string Labels []string + IPv6 bool } func (e *CNIEnv) CreateNetwork(opts CreateOptions) (*NetworkConfig, error) { //nolint:revive @@ -249,11 +250,11 @@ func (e *CNIEnv) CreateNetwork(opts CreateOptions) (*NetworkConfig, error) { //n } fn := func() error { - ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnet, opts.Gateway, opts.IPRange, opts.IPAMOptions) + ipam, err := e.generateIPAM(opts.IPAMDriver, opts.Subnets, opts.Gateway, opts.IPRange, opts.IPAMOptions, opts.IPv6) if err != nil { return err } - plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options) + plugins, err := e.generateCNIPlugins(opts.Driver, opts.Name, ipam, opts.Options, opts.IPv6) if err != nil { return err } @@ -352,7 +353,7 @@ func (e *CNIEnv) createDefaultNetworkConfig() error { opts := CreateOptions{ Name: DefaultNetworkName, Driver: DefaultNetworkName, - Subnet: DefaultCIDR, + Subnets: []string{DefaultCIDR}, IPAMDriver: "default", Labels: []string{fmt.Sprintf("%s=true", labels.NerdctlDefaultNetwork)}, } diff --git a/pkg/netutil/netutil_unix.go b/pkg/netutil/netutil_unix.go index b75bf62a6c..d149a1e9eb 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -87,7 +87,7 @@ func (n *NetworkConfig) clean() error { return nil } -func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool) ([]CNIPlugin, error) { var ( plugins []CNIPlugin err error @@ -123,6 +123,9 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] bridge.IsGW = true bridge.IPMasq = iPMasq bridge.HairpinMode = true + if ipv6 { + bridge.Capabilities["ips"] = true + } plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} plugins = fixUpIsolation(e, name, plugins) case "macvlan", "ipvlan": @@ -160,6 +163,9 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] vlan.Master = master vlan.Mode = mode vlan.IPAM = ipam + if ipv6 { + vlan.Capabilities["ips"] = true + } plugins = []CNIPlugin{vlan} default: return nil, fmt.Errorf("unsupported cni driver %q", driver) @@ -167,24 +173,23 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool) (map[string]interface{}, error) { var ipamConfig interface{} switch driver { case "default", "host-local": - subnet, err := e.parseSubnet(subnetStr) - if err != nil { - return nil, err + ipamConf := newHostLocalIPAMConfig() + ipamConf.Routes = []IPAMRoute{ + {Dst: "0.0.0.0/0"}, } - ipamRange, err := parseIPAMRange(subnet, gatewayStr, ipRangeStr) + ranges, findIPv4, err := e.parseIPAMRanges(subnets, gatewayStr, ipRangeStr, ipv6) if err != nil { return nil, err } - - ipamConf := newHostLocalIPAMConfig() - ipamConf.Routes = []IPAMRoute{ - {Dst: "0.0.0.0/0"}, + ipamConf.Ranges = append(ipamConf.Ranges, ranges...) + if !findIPv4 { + ranges, _, _ = e.parseIPAMRanges([]string{""}, gatewayStr, ipRangeStr, ipv6) + ipamConf.Ranges = append(ipamConf.Ranges, ranges...) } - ipamConf.Ranges = append(ipamConf.Ranges, []IPAMRange{*ipamRange}) ipamConfig = ipamConf case "dhcp": ipamConf := newDHCPIPAMConfig() @@ -205,6 +210,30 @@ func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr s return ipam, nil } +func (e *CNIEnv) parseIPAMRanges(subnets []string, gateway, ipRange string, ipv6 bool) ([][]IPAMRange, bool, error) { + findIPv4 := false + ranges := make([][]IPAMRange, 0, len(subnets)) + for i := range subnets { + subnet, err := e.parseSubnet(subnets[i]) + if err != nil { + return nil, findIPv4, err + } + // if ipv6 flag is not set, subnets of ipv6 should be excluded + if !ipv6 && subnet.IP.To4() == nil { + continue + } + if !findIPv4 && subnet.IP.To4() != nil { + findIPv4 = true + } + ipamRange, err := parseIPAMRange(subnet, gateway, ipRange) + if err != nil { + return nil, findIPv4, err + } + ranges = append(ranges, []IPAMRange{*ipamRange}) + } + return ranges, findIPv4, nil +} + func fixUpIsolation(e *CNIEnv, name string, plugins []CNIPlugin) []CNIPlugin { isolationPath := filepath.Join(e.Path, "isolation") if _, err := exec.LookPath(isolationPath); err == nil { diff --git a/pkg/netutil/netutil_windows.go b/pkg/netutil/netutil_windows.go index f053115259..e0956bf730 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -58,7 +58,7 @@ func (n *NetworkConfig) clean() error { return nil } -func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string) ([]CNIPlugin, error) { +func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string]interface{}, opts map[string]string, ipv6 bool) ([]CNIPlugin, error) { var plugins []CNIPlugin switch driver { case "nat": @@ -71,8 +71,15 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] return plugins, nil } -func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr string, opts map[string]string) (map[string]interface{}, error) { - subnet, err := e.parseSubnet(subnetStr) +func (e *CNIEnv) generateIPAM(driver string, subnets []string, gatewayStr, ipRangeStr string, opts map[string]string, ipv6 bool) (map[string]interface{}, error) { + switch driver { + case "default": + default: + return nil, fmt.Errorf("unsupported ipam driver %q", driver) + } + + ipamConfig := newWindowsIPAMConfig() + subnet, err := e.parseSubnet(subnets[0]) if err != nil { return nil, err } @@ -80,18 +87,8 @@ func (e *CNIEnv) generateIPAM(driver string, subnetStr, gatewayStr, ipRangeStr s if err != nil { return nil, err } - - var ipamConfig interface{} - switch driver { - case "default": - ipamConf := newWindowsIPAMConfig() - ipamConf.Subnet = ipamRange.Subnet - ipamConf.Routes = append(ipamConf.Routes, IPAMRoute{Gateway: ipamRange.Gateway}) - ipamConfig = ipamConf - default: - return nil, fmt.Errorf("unsupported ipam driver %q", driver) - } - + ipamConfig.Subnet = ipamRange.Subnet + ipamConfig.Routes = append(ipamConfig.Routes, IPAMRoute{Gateway: ipamRange.Gateway}) ipam, err := structToMap(ipamConfig) if err != nil { return nil, err diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 2b5a6aa956..222993c709 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -188,7 +188,11 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin } if macAddress, ok := o.state.Annotations[labels.MACAddress]; ok { - o.contianerMAC = macAddress + o.containerMAC = macAddress + } + + if ip6Address, ok := o.state.Annotations[labels.IP6Address]; ok { + o.containerIP6 = ip6Address } if rootlessutil.IsRootlessChild() { @@ -226,7 +230,8 @@ type handlerOpts struct { bypassClient b4nndclient.Client extraHosts map[string]string // host:ip containerIP string - contianerMAC string + containerMAC string + containerIP6 string } // hookSpec is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/containerd/command/oci-hook.go#L59-L64 @@ -353,14 +358,31 @@ func getIPAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { } func getMACAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { - if opts.contianerMAC != "" { + if opts.containerMAC != "" { return []gocni.NamespaceOpts{ gocni.WithLabels(map[string]string{ // allow loose CNI argument verification // FYI: https://github.com/containernetworking/cni/issues/560 "IgnoreUnknown": "1", }), - gocni.WithArgs("MAC", opts.contianerMAC), + gocni.WithArgs("MAC", opts.containerMAC), + }, nil + } + return nil, nil +} + +func getIP6AddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { + if opts.containerIP6 != "" { + if rootlessutil.IsRootlessChild() { + log.L.Debug("container IP6 assignment is not fully supported in rootless mode. The IP6 is not accessible from the host (but still accessible from other containers).") + } + return []gocni.NamespaceOpts{ + gocni.WithLabels(map[string]string{ + // allow loose CNI argument verification + // FYI: https://github.com/containernetworking/cni/issues/560 + "IgnoreUnknown": "1", + }), + gocni.WithCapability("ips", []string{opts.containerIP6}), }, nil } return nil, nil @@ -391,10 +413,15 @@ func onCreateRuntime(opts *handlerOpts) error { if err != nil { return err } + ip6AddressOpts, err := getIP6AddressOpts(opts) + if err != nil { + return err + } var namespaceOpts []gocni.NamespaceOpts namespaceOpts = append(namespaceOpts, portMapOpts...) namespaceOpts = append(namespaceOpts, ipAddressOpts...) namespaceOpts = append(namespaceOpts, macAddressOpts...) + namespaceOpts = append(namespaceOpts, ip6AddressOpts...) hsMeta := hostsstore.Meta{ Namespace: opts.state.Annotations[labels.Namespace], ID: opts.state.ID, @@ -478,10 +505,15 @@ func onPostStop(opts *handlerOpts) error { if err != nil { return err } + ip6AddressOpts, err := getIP6AddressOpts(opts) + if err != nil { + return err + } var namespaceOpts []gocni.NamespaceOpts namespaceOpts = append(namespaceOpts, portMapOpts...) namespaceOpts = append(namespaceOpts, ipAddressOpts...) namespaceOpts = append(namespaceOpts, macAddressOpts...) + namespaceOpts = append(namespaceOpts, ip6AddressOpts...) if err := opts.cni.Remove(ctx, opts.fullID, "", namespaceOpts...); err != nil { log.L.WithError(err).Errorf("failed to call cni.Remove") return err diff --git a/pkg/testutil/testutil.go b/pkg/testutil/testutil.go index 04d55b85f8..7947b15612 100644 --- a/pkg/testutil/testutil.go +++ b/pkg/testutil/testutil.go @@ -47,6 +47,8 @@ type Base struct { T testing.TB Target Target DaemonIsKillable bool + EnableIPv6 bool + IPv6Compatible bool Binary string Args []string Env []string @@ -482,11 +484,13 @@ const ( var ( flagTestTarget Target flagTestKillDaemon bool + flagTestIPv6 bool ) func M(m *testing.M) { flag.StringVar(&flagTestTarget, "test.target", Nerdctl, "target to test") flag.BoolVar(&flagTestKillDaemon, "test.kill-daemon", false, "enable tests that kill the daemon") + flag.BoolVar(&flagTestIPv6, "test.ipv6", false, "enable tests on IPv6") flag.Parse() fmt.Fprintf(os.Stderr, "test target: %q\n", flagTestTarget) os.Exit(m.Run()) @@ -499,6 +503,10 @@ func GetTarget() string { return flagTestTarget } +func GetEnableIPv6() bool { + return flagTestIPv6 +} + func GetDaemonIsKillable() bool { return flagTestKillDaemon } @@ -618,18 +626,29 @@ func NewBaseWithNamespace(t *testing.T, ns string) *Base { if ns == "" || ns == "default" || ns == Namespace { t.Fatalf(`the other base namespace cannot be "%s"`, ns) } - return newBase(t, ns) + return newBase(t, ns, false) +} + +func NewBaseWithIPv6Compatible(t *testing.T) *Base { + return newBase(t, Namespace, true) } func NewBase(t *testing.T) *Base { - return newBase(t, Namespace) + return newBase(t, Namespace, false) } -func newBase(t *testing.T, ns string) *Base { +func newBase(t *testing.T, ns string, ipv6Compatible bool) *Base { base := &Base{ T: t, Target: GetTarget(), DaemonIsKillable: GetDaemonIsKillable(), + EnableIPv6: GetEnableIPv6(), + IPv6Compatible: ipv6Compatible, + } + if base.EnableIPv6 && !base.IPv6Compatible { + t.Skip("runner skips non-IPv6 complatible tests in the IPv6 environment") + } else if !base.EnableIPv6 && base.IPv6Compatible { + t.Skip("runner skips IPv6 compatible tests in the non-IPv6 environment") } var err error switch base.Target {