Skip to content

Commit

Permalink
Support IPv6 for nerdctl network
Browse files Browse the repository at this point in the history
Co-authored-by: Zheao Li <me@manjusaka.me>
Signed-off-by: Hanchin Hsieh <me@yuchanns.xyz>
  • Loading branch information
yuchanns and Zheaoli committed Oct 28, 2023
1 parent e605881 commit a8e3cb3
Show file tree
Hide file tree
Showing 19 changed files with 324 additions and 54 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,51 @@ 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: 20.04
containerd: v1.6.24
- ubuntu: 20.04
containerd: v1.7.7
- ubuntu: 22.04
containerd: v1.7.7
- ubuntu: 22.04
containerd: main
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"
run: docker run --network host -t --rm --privileged test-integration-ipv6

test-integration-rootless:
runs-on: "ubuntu-${{ matrix.ubuntu }}"
timeout-minutes: 60
Expand Down Expand Up @@ -200,6 +245,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
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,7 @@ COPY ./Dockerfile.d/home_rootless_.config_systemd_user_containerd.service.d_port
RUN chown -R rootless:rootless /home/rootless/.config

FROM base AS demo

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"]
4 changes: 2 additions & 2 deletions cmd/nerdctl/compose_up_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ networks:
default:
ipam:
config:
- subnet: 10.1.100.0/24
- subnet: 10.1.101.0/24
`, testutil.AlpineImage)

comp := testutil.NewComposeDir(t, dockerComposeYAML)
Expand All @@ -378,7 +378,7 @@ networks:
base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()

base.Cmd("inspect", "-f", `{{json .NetworkSettings.Networks }}`, serviceparser.DefaultContainerName(projectName, "foo", "1")).AssertOutContains("10.1.100.")
base.Cmd("inspect", "-f", `{{json .NetworkSettings.Networks }}`, serviceparser.DefaultContainerName(projectName, "foo", "1")).AssertOutContains("10.1.101.")
}

func TestComposeUpRemoveOrphans(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/nerdctl/container_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions cmd/nerdctl/container_run_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) {
}
netOpts.IPAddress = ipAddress

// --ip6=<container static IP6>
ip6Address, err := cmd.Flags().GetString("ip6")
if err != nil {
return netOpts, err
}
netOpts.IP6Address = ip6Address

// -h/--hostname=<container hostname>
hostName, err := cmd.Flags().GetString("hostname")
if err != nil {
Expand Down
71 changes: 71 additions & 0 deletions cmd/nerdctl/container_run_network_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"fmt"
"io"
"net"
"regexp"
"runtime"
"strings"
Expand Down Expand Up @@ -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
})
})
}
}
12 changes: 9 additions & 3 deletions cmd/nerdctl/network_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -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())
}
40 changes: 40 additions & 0 deletions cmd/nerdctl/network_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package main

import (
"fmt"
"net"
"strings"
"testing"

"github.com/containerd/nerdctl/pkg/testutil"
Expand Down Expand Up @@ -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)
}
6 changes: 4 additions & 2 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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`

Expand Down Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions pkg/api/types/container_network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type NetworkOptions struct {
MACAddress string
// IPAddress set specific static IP address(es) to use
IPAddress string
// IP6Address set specific static IPv6 address(es) to use
IP6Address string
// Hostname set container host name
Hostname string
// DNSServers set custom DNS servers
Expand Down
6 changes: 6 additions & 0 deletions pkg/cmd/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -505,6 +506,7 @@ type internalLabels struct {
// network
networks []string
ipAddress string
ip6Address string
ports []gocni.PortMapping
macAddress string
// volume
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/network/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,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{
Expand Down
Loading

0 comments on commit a8e3cb3

Please sign in to comment.