Skip to content

Commit 4201370

Browse files
authored
Merge pull request #1558 from yuchanns/1547
Support IPv6 for nerdctl network
2 parents 85a3d88 + a6a2d91 commit 4201370

18 files changed

+322
-52
lines changed

.github/workflows/test.yml

+46
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,50 @@ jobs:
104104
- name: "Run integration tests"
105105
run: docker run -t --rm --privileged test-integration
106106

107+
test-integration-ipv6:
108+
runs-on: "ubuntu-${{ matrix.ubuntu }}"
109+
timeout-minutes: 40
110+
strategy:
111+
fail-fast: false
112+
matrix:
113+
# ubuntu-20.04: cgroup v1, ubuntu-22.04: cgroup v2
114+
include:
115+
- ubuntu: 22.04
116+
containerd: v1.7.7
117+
env:
118+
UBUNTU_VERSION: "${{ matrix.ubuntu }}"
119+
CONTAINERD_VERSION: "${{ matrix.containerd }}"
120+
steps:
121+
- uses: actions/[email protected]
122+
with:
123+
fetch-depth: 1
124+
- name: Enable ipv4 and ipv6 forwarding
125+
run: |
126+
sudo sysctl -w net.ipv6.conf.all.forwarding=1
127+
sudo sysctl -w net.ipv4.ip_forward=1
128+
- name: Enable IPv6 for Docker
129+
run: |
130+
sudo mkdir -p /etc/docker
131+
echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json
132+
sudo systemctl restart docker
133+
- name: "Prepare integration test environment"
134+
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} .
135+
- name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)"
136+
run: |
137+
sudo systemctl disable --now snapd.service snapd.socket
138+
sudo apt-get purge -y snapd
139+
sudo losetup -Dv
140+
sudo losetup -lv
141+
- name: "Register QEMU (tonistiigi/binfmt)"
142+
run: docker run --privileged --rm tonistiigi/binfmt --install all
143+
- name: "Run integration tests"
144+
# The nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config.
145+
# Therefore it's hard to debug why the IPv6 tests fail in such an isolation layer.
146+
# On the other side, using the host network is easier at configuration.
147+
# Besides, each job is running on a different instance, which means using host network here
148+
# is safe and has no side effects on others.
149+
run: docker run --network host -t --rm --privileged test-integration-ipv6
150+
107151
test-integration-rootless:
108152
runs-on: "ubuntu-${{ matrix.ubuntu }}"
109153
timeout-minutes: 60
@@ -200,6 +244,8 @@ jobs:
200244
sudo apt-get install -y expect
201245
- name: "Ensure that the integration test suite is compatible with Docker"
202246
run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon
247+
- name: "Ensure that the IPv6 integration test suite is compatible with Docker"
248+
run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon -test.ipv6
203249

204250
test-integration-windows:
205251
# A "larger" runner is used for enabling Hyper-V containers

Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -341,4 +341,8 @@ FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns
341341
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
342342
RUN chown -R rootless:rootless /home/rootless/.config
343343

344+
FROM test-integration AS test-integration-ipv6
345+
CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=github.com/containerd/nerdctl/cmd/nerdctl/...", \
346+
"--", "-timeout=30m", "-args", "-test.kill-daemon", "-test.ipv6"]
347+
344348
FROM base AS demo

cmd/nerdctl/container_run.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ func setCreateFlags(cmd *cobra.Command) {
120120
cmd.Flags().StringSlice("dns-option", nil, "Set DNS options")
121121
// publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman)
122122
cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host")
123-
// FIXME: not support IPV6 yet
124123
cmd.Flags().String("ip", "", "IPv4 address to assign to the container")
124+
cmd.Flags().String("ip6", "", "IPv6 address to assign to the container")
125125
cmd.Flags().StringP("hostname", "h", "", "Container host name")
126126
cmd.Flags().String("mac-address", "", "MAC address to assign to the container")
127127
// #endregion

cmd/nerdctl/container_run_network.go

+7
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) {
7777
}
7878
netOpts.IPAddress = ipAddress
7979

80+
// --ip6=<container static IP6>
81+
ip6Address, err := cmd.Flags().GetString("ip6")
82+
if err != nil {
83+
return netOpts, err
84+
}
85+
netOpts.IP6Address = ip6Address
86+
8087
// -h/--hostname=<container hostname>
8188
hostName, err := cmd.Flags().GetString("hostname")
8289
if err != nil {

cmd/nerdctl/container_run_network_linux_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"fmt"
2121
"io"
22+
"net"
2223
"regexp"
2324
"runtime"
2425
"strings"
@@ -491,3 +492,73 @@ func TestRunContainerWithMACAddress(t *testing.T) {
491492
}
492493
}
493494
}
495+
496+
func TestRunContainerWithStaticIP6(t *testing.T) {
497+
if rootlessutil.IsRootless() {
498+
t.Skip("Static IP6 assignment is not supported rootless mode yet.")
499+
}
500+
networkName := "test-network"
501+
networkSubnet := "2001:db8:5::/64"
502+
_, subnet, err := net.ParseCIDR(networkSubnet)
503+
assert.Assert(t, err == nil)
504+
base := testutil.NewBaseWithIPv6Compatible(t)
505+
base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK()
506+
t.Cleanup(func() {
507+
base.Cmd("network", "rm", networkName).Run()
508+
})
509+
testCases := []struct {
510+
ip string
511+
shouldSuccess bool
512+
checkTheIPAddress bool
513+
}{
514+
{
515+
ip: "",
516+
shouldSuccess: true,
517+
checkTheIPAddress: false,
518+
},
519+
{
520+
ip: "2001:db8:5::6",
521+
shouldSuccess: true,
522+
checkTheIPAddress: true,
523+
},
524+
{
525+
ip: "2001:db8:4::6",
526+
shouldSuccess: false,
527+
checkTheIPAddress: false,
528+
},
529+
}
530+
tID := testutil.Identifier(t)
531+
for i, tc := range testCases {
532+
i := i
533+
tc := tc
534+
tcName := fmt.Sprintf("%+v", tc)
535+
t.Run(tcName, func(t *testing.T) {
536+
testContainerName := fmt.Sprintf("%s-%d", tID, i)
537+
base := testutil.NewBaseWithIPv6Compatible(t)
538+
args := []string{
539+
"run", "--rm", "--name", testContainerName, "--network", networkName,
540+
}
541+
if tc.ip != "" {
542+
args = append(args, "--ip6", tc.ip)
543+
}
544+
args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...)
545+
cmd := base.Cmd(args...)
546+
if !tc.shouldSuccess {
547+
cmd.AssertFail()
548+
return
549+
}
550+
cmd.AssertOutWithFunc(func(stdout string) error {
551+
ip := findIPv6(stdout)
552+
if !subnet.Contains(ip) {
553+
return fmt.Errorf("expected subnet %s include ip %s", subnet, ip)
554+
}
555+
if tc.checkTheIPAddress {
556+
if ip.String() != tc.ip {
557+
return fmt.Errorf("expected ip %s, got %s", tc.ip, ip)
558+
}
559+
}
560+
return nil
561+
})
562+
})
563+
}
564+
}

cmd/nerdctl/network_create.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ func newNetworkCreateCommand() *cobra.Command {
4444
networkCreateCommand.Flags().String("ipam-driver", "default", "IP Address Management Driver")
4545
networkCreateCommand.RegisterFlagCompletionFunc("ipam-driver", shellCompleteIPAMDrivers)
4646
networkCreateCommand.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options")
47-
networkCreateCommand.Flags().String("subnet", "", `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
47+
networkCreateCommand.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
4848
networkCreateCommand.Flags().String("gateway", "", `Gateway for the master subnet`)
4949
networkCreateCommand.Flags().String("ip-range", "", `Allocate container ip from a sub-range`)
5050
networkCreateCommand.Flags().StringArray("label", nil, "Set metadata for a network")
51+
networkCreateCommand.Flags().Bool("ipv6", false, "Enable IPv6 networking")
5152
return networkCreateCommand
5253
}
5354

@@ -79,7 +80,7 @@ func networkCreateAction(cmd *cobra.Command, args []string) error {
7980
if err != nil {
8081
return err
8182
}
82-
subnetStr, err := cmd.Flags().GetString("subnet")
83+
subnets, err := cmd.Flags().GetStringArray("subnet")
8384
if err != nil {
8485
return err
8586
}
@@ -96,6 +97,10 @@ func networkCreateAction(cmd *cobra.Command, args []string) error {
9697
return err
9798
}
9899
labels = strutil.DedupeStrSlice(labels)
100+
ipv6, err := cmd.Flags().GetBool("ipv6")
101+
if err != nil {
102+
return err
103+
}
99104

100105
return network.Create(types.NetworkCreateOptions{
101106
GOptions: globalOptions,
@@ -105,10 +110,11 @@ func networkCreateAction(cmd *cobra.Command, args []string) error {
105110
Options: strutil.ConvertKVStringsToMap(opts),
106111
IPAMDriver: ipamDriver,
107112
IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
108-
Subnet: subnetStr,
113+
Subnets: subnets,
109114
Gateway: gatewayStr,
110115
IPRange: ipRangeStr,
111116
Labels: labels,
117+
IPv6: ipv6,
112118
},
113119
}, cmd.OutOrStdout())
114120
}

cmd/nerdctl/network_create_linux_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package main
1818

1919
import (
20+
"fmt"
21+
"net"
22+
"strings"
2023
"testing"
2124

2225
"github.com/containerd/nerdctl/pkg/testutil"
@@ -54,3 +57,40 @@ func TestNetworkCreate(t *testing.T) {
5457

5558
base.Cmd("run", "--rm", "--net", testNetwork+"-1", testutil.CommonImage, "ip", "route").AssertNoOut(net.IPAM.Config[0].Subnet)
5659
}
60+
61+
func TestNetworkCreateIPv6(t *testing.T) {
62+
base := testutil.NewBaseWithIPv6Compatible(t)
63+
testNetwork := testutil.Identifier(t)
64+
65+
subnetStr := "2001:db8:8::/64"
66+
_, subnet, err := net.ParseCIDR(subnetStr)
67+
assert.Assert(t, err == nil)
68+
69+
base.Cmd("network", "create", "--ipv6", "--subnet", subnetStr, testNetwork).AssertOK()
70+
t.Cleanup(func() {
71+
base.Cmd("network", "rm", testNetwork).Run()
72+
})
73+
74+
base.Cmd("run", "--rm", "--net", testNetwork, testutil.CommonImage, "ip", "addr", "show", "dev", "eth0").AssertOutWithFunc(func(stdout string) error {
75+
ip := findIPv6(stdout)
76+
if subnet.Contains(ip) {
77+
return nil
78+
}
79+
return fmt.Errorf("expected subnet %s include ip %s", subnet, ip)
80+
})
81+
}
82+
83+
func findIPv6(output string) net.IP {
84+
var ipv6 string
85+
lines := strings.Split(output, "\n")
86+
for _, line := range lines {
87+
if strings.Contains(line, "inet6") {
88+
fields := strings.Fields(line)
89+
if len(fields) > 1 {
90+
ipv6 = strings.Split(fields[1], "/")[0]
91+
break
92+
}
93+
}
94+
}
95+
return net.ParseIP(ipv6)
96+
}

docs/command-reference.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ Network flags:
183183
- :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip). `ip` could be a special string `host-gateway`,
184184
- which will be resolved to the `host-gateway-ip` in nerdctl.toml or global flag.
185185
- :whale: `--ip`: Specific static IP address(es) to use
186+
- :whale: `--ip6`: Specific static IP6 address(es) to use. Should be used with user networks
186187
- :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not
187188
check if manually specified MAC addresses are unique. Supports network
188189
type `bridge` and `macvlan`
@@ -375,7 +376,7 @@ IPFS flags:
375376

376377
Unimplemented `docker run` flags:
377378
`--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
378-
`--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--ip6`, `--isolation`, `--no-healthcheck`,
379+
`--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`,
379380
`--link*`, `--mac-address`, `--publish-all`, `--sig-proxy`, `--storage-opt`,
380381
`--userns`, `--volume-driver`
381382

@@ -992,8 +993,9 @@ Flags:
992993
- :whale: `--gateway`: Gateway for the master subnet
993994
- :whale: `--ip-range`: Allocate container ip from a sub-range
994995
- :whale: `--label`: Set metadata on a network
996+
- :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet.
995997

996-
Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--ipv6`, `--scope`
998+
Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--scope`
997999

9981000
### :whale: nerdctl network ls
9991001

pkg/api/types/container_network_types.go

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type NetworkOptions struct {
2828
MACAddress string
2929
// IPAddress set specific static IP address(es) to use
3030
IPAddress string
31+
// IP6Address set specific static IP6 address(es) to use
32+
IP6Address string
3133
// Hostname set container host name
3234
Hostname string
3335
// DNSServers set custom DNS servers

pkg/cmd/container/create.go

+6
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
193193
internalLabels.hostname = netLabelOpts.Hostname
194194
internalLabels.ports = netLabelOpts.PortMappings
195195
internalLabels.ipAddress = netLabelOpts.IPAddress
196+
internalLabels.ip6Address = netLabelOpts.IP6Address
196197
internalLabels.networks = netLabelOpts.NetworkSlice
197198
internalLabels.macAddress = netLabelOpts.MACAddress
198199

@@ -505,6 +506,7 @@ type internalLabels struct {
505506
// network
506507
networks []string
507508
ipAddress string
509+
ip6Address string
508510
ports []gocni.PortMapping
509511
macAddress string
510512
// volume
@@ -561,6 +563,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO
561563
m[labels.IPAddress] = internalLabels.ipAddress
562564
}
563565

566+
if internalLabels.ip6Address != "" {
567+
m[labels.IP6Address] = internalLabels.ip6Address
568+
}
569+
564570
m[labels.Platform], err = platformutil.NormalizeString(internalLabels.platform)
565571
if err != nil {
566572
return nil, err

pkg/cmd/network/create.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import (
2626
)
2727

2828
func Create(options types.NetworkCreateOptions, stdout io.Writer) error {
29-
if options.CreateOptions.Subnet == "" {
29+
if len(options.CreateOptions.Subnets) == 0 {
3030
if options.CreateOptions.Gateway != "" || options.CreateOptions.IPRange != "" {
3131
return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually")
3232
}
33+
options.CreateOptions.Subnets = []string{""}
3334
}
3435

3536
e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath)

pkg/labels/labels.go

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const (
6060
// IPAddress is the static IP address of the container assigned by the user
6161
IPAddress = Prefix + "ip"
6262

63+
// IP6Address is the static IP6 address of the container assigned by the user
64+
IP6Address = Prefix + "ip6"
65+
6366
// LogURI is the log URI
6467
LogURI = Prefix + "log-uri"
6568

0 commit comments

Comments
 (0)