From 99fd8ae89941e19c5765e36fbde452d2b91df0ba Mon Sep 17 00:00:00 2001 From: yuchanns Date: Sun, 19 Feb 2023 01:57:23 +0800 Subject: [PATCH] Support IPv6 for network Signed-off-by: Hanchin Hsieh --- cmd/nerdctl/container_run.go | 10 ++- cmd/nerdctl/container_run_network.go | 42 ++++++----- .../container_run_network_linux_test.go | 70 +++++++++++++++++++ cmd/nerdctl/network_create.go | 12 +++- cmd/nerdctl/network_create_linux_test.go | 40 +++++++++++ docs/command-reference.md | 2 + pkg/cmd/network/create.go | 3 +- pkg/labels/labels.go | 2 + pkg/netutil/cni_plugin_unix.go | 20 +++--- pkg/netutil/netutil.go | 9 +-- pkg/netutil/netutil_unix.go | 51 +++++++++++--- pkg/netutil/netutil_windows.go | 27 ++++--- pkg/ocihook/ocihook.go | 32 +++++++++ 13 files changed, 257 insertions(+), 63 deletions(-) diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index 6475f0e453f..66433c9c11d 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -144,8 +144,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 @@ -608,12 +608,13 @@ func createContainer(ctx context.Context, cmd *cobra.Command, client *containerd } } - netOpts, netSlice, ipAddress, ports, macAddress, err := generateNetOpts(cmd, globalOptions, dataStore, stateDir, globalOptions.Namespace, id) + netOpts, netSlice, ipAddress, ports, macAddress, ip6Address, err := generateNetOpts(cmd, globalOptions, dataStore, stateDir, globalOptions.Namespace, id) if err != nil { return nil, nil, err } internalLabels.networks = netSlice internalLabels.ipAddress = ipAddress + internalLabels.ip6Address = ip6Address internalLabels.ports = ports internalLabels.macAddress = macAddress opts = append(opts, netOpts...) @@ -1056,6 +1057,7 @@ type internalLabels struct { // network networks []string ipAddress string + ip6Address string ports []gocni.PortMapping macAddress string // volumn @@ -1133,6 +1135,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO m[labels.PIDContainer] = internalLabels.pidContainer } + if internalLabels.ip6Address != "" { + m[labels.IP6Address] = internalLabels.ip6Address + } + return containerd.WithAdditionalContainerLabels(m), nil } diff --git a/cmd/nerdctl/container_run_network.go b/cmd/nerdctl/container_run_network.go index f03407d1f77..f911aaf9ee1 100644 --- a/cmd/nerdctl/container_run_network.go +++ b/cmd/nerdctl/container_run_network.go @@ -113,19 +113,23 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C } } -func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, error) { +func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, dataStore, stateDir, ns, id string) ([]oci.SpecOpts, []string, string, []gocni.PortMapping, string, string, error) { opts := []oci.SpecOpts{} portSlice, err := cmd.Flags().GetStringSlice("publish") if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } ipAddress, err := cmd.Flags().GetString("ip") if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err + } + ip6Address, err := cmd.Flags().GetString("ip6") + if err != nil { + return nil, nil, "", nil, "", "", err } netSlice, err := getNetworkSlice(cmd) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } if (len(netSlice) == 0) && (ipAddress != "") { @@ -134,13 +138,13 @@ func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOption macAddress, err := getMACAddress(cmd, netSlice) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } ports := make([]gocni.PortMapping, 0) netType, err := nettype.Detect(netSlice) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } switch netType { @@ -150,51 +154,51 @@ func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOption // not work but run command will success case nettype.Host: if macAddress != "" { - return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode") + return nil, nil, "", nil, "", "", errors.New("conflicting options: mac-address and the network mode") } opts = append(opts, oci.WithHostNamespace(specs.NetworkNamespace), oci.WithHostHostsFile, oci.WithHostResolvconf) case nettype.CNI: // We only verify flags and generate resolv.conf here. // The actual network is configured in the oci hook. if err := verifyCNINetwork(cmd, netSlice, macAddress, globalOptions); err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } if runtime.GOOS == "linux" { resolvConfPath := filepath.Join(stateDir, "resolv.conf") if err := buildResolvConf(cmd, resolvConfPath); err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } // the content of /etc/hosts is created in OCI Hook etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath)) for _, p := range portSlice { pm, err := portutil.ParseFlagP(p) if err != nil { - return nil, nil, "", pm, "", err + return nil, nil, "", pm, "", "", err } ports = append(ports, pm...) } } case nettype.Container: if macAddress != "" { - return nil, nil, "", nil, "", errors.New("conflicting options: mac-address and the network mode") + return nil, nil, "", nil, "", "", errors.New("conflicting options: mac-address and the network mode") } if err := verifyContainerNetwork(cmd, netSlice); err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } network := strings.Split(netSlice[0], ":") if len(network) != 2 { - return nil, nil, "", nil, "", fmt.Errorf("invalid network: %s, should be \"container:\"", netSlice[0]) + return nil, nil, "", nil, "", "", fmt.Errorf("invalid network: %s, should be \"container:\"", netSlice[0]) } containerName := network[1] client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } defer cancel() @@ -240,15 +244,15 @@ func generateNetOpts(cmd *cobra.Command, globalOptions types.GlobalCommandOption } n, err := walker.Walk(ctx, containerName) if err != nil { - return nil, nil, "", nil, "", err + return nil, nil, "", nil, "", "", err } if n == 0 { - return nil, nil, "", nil, "", fmt.Errorf("no such container: %s", containerName) + return nil, nil, "", nil, "", "", fmt.Errorf("no such container: %s", containerName) } default: - return nil, nil, "", nil, "", fmt.Errorf("unexpected network type %v", netType) + return nil, nil, "", nil, "", "", fmt.Errorf("unexpected network type %v", netType) } - return opts, netSlice, ipAddress, ports, macAddress, nil + return opts, netSlice, ipAddress, ports, macAddress, ip6Address, nil } func verifyCNINetwork(cmd *cobra.Command, netSlice []string, macAddress string, globalOptions types.GlobalCommandOptions) error { diff --git a/cmd/nerdctl/container_run_network_linux_test.go b/cmd/nerdctl/container_run_network_linux_test.go index 0bf6bfcdb6d..b374013e438 100644 --- a/cmd/nerdctl/container_run_network_linux_test.go +++ b/cmd/nerdctl/container_run_network_linux_test.go @@ -575,3 +575,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.NewBase(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.NewBase(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 c7452277bdf..b5c4f1e81e3 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 d055c10bb28..3fe1aab9763 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.NewBase(t) + testNetwork := testutil.Identifier(t) + + subnetStr := "2001:db8:1::/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 2ed189b9f34..d4b9b442b84 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -179,6 +179,7 @@ Network flags: - :whale: `-h, --hostname`: Container host name - :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip) - :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` @@ -949,6 +950,7 @@ 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` diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go index f592a5ee62b..46cbaa9f992 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 5d4d5431acd..03a2eb5860a 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -95,6 +95,8 @@ const ( // Boolean value which can be parsed with strconv.ParseBool() is required. // (like "nerdctl/default-network=true" or "nerdctl/default-network=false") NerdctlDefaultNetwork = Prefix + "default-network" + + IP6Address = Prefix + "ip6" ) var ShellCompletions = []string{ diff --git a/pkg/netutil/cni_plugin_unix.go b/pkg/netutil/cni_plugin_unix.go index 535a003847e..588f339c669 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 6b0243eaf4d..d4062ccae04 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -226,10 +226,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 @@ -244,11 +245,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 } @@ -353,7 +354,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 2a221ca8bcf..404e57ec5cc 100644 --- a/pkg/netutil/netutil_unix.go +++ b/pkg/netutil/netutil_unix.go @@ -81,7 +81,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 @@ -111,6 +111,9 @@ func (e *CNIEnv) generateCNIPlugins(driver string, name string, ipam map[string] bridge.IsGW = true bridge.IPMasq = true bridge.HairpinMode = true + if ipv6 { + bridge.Capabilities["ips"] = true + } plugins = []CNIPlugin{bridge, newPortMapPlugin(), newFirewallPlugin(), newTuningPlugin()} plugins = fixUpIsolation(e, name, plugins) case "macvlan", "ipvlan": @@ -148,6 +151,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) @@ -155,24 +161,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() @@ -193,6 +198,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 31d28140fc4..f2553c97764 100644 --- a/pkg/netutil/netutil_windows.go +++ b/pkg/netutil/netutil_windows.go @@ -53,7 +53,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": @@ -66,8 +66,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 } @@ -75,18 +82,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 8c9554a15af..7c7487f762e 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -185,6 +185,10 @@ func newHandlerOpts(state *specs.State, dataStore, cniPath, cniNetconfPath strin o.contianerMAC = macAddress } + if ip6Address, ok := o.state.Annotations[labels.IP6Address]; ok { + o.containerIP6 = ip6Address + } + if rootlessutil.IsRootlessChild() { o.rootlessKitClient, err = rootlessutil.NewRootlessKitClient() if err != nil { @@ -221,6 +225,7 @@ type handlerOpts struct { extraHosts map[string]string // host:ip containerIP string contianerMAC string + containerIP6 string } // hookSpec is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/containerd/command/oci-hook.go#L59-L64 @@ -362,6 +367,23 @@ func getMACAddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { return nil, nil } +func getIP6AddressOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { + if opts.containerIP6 != "" { + if rootlessutil.IsRootlessChild() { + logrus.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 +} + func onCreateRuntime(opts *handlerOpts) error { loadAppArmor() @@ -387,10 +409,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, @@ -485,10 +512,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 { logrus.WithError(err).Errorf("failed to call cni.Remove") return err