Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support IPv6 for nerdctl network #1558

Merged
merged 1 commit into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,50 @@ jobs:
- name: "Run integration tests"
run: docker run -t --rm --privileged test-integration

test-integration-ipv6:
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment to explain why we need (and why we can safely(?) use) --network host

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


test-integration-rootless:
runs-on: "ubuntu-${{ matrix.ubuntu }}"
timeout-minutes: 60
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need reconfiguring sysctl and /etc/docker/daemon.json ?

Copy link
Contributor Author

@yuchanns yuchanns Oct 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Fun fact 😂
image

When the parameter --test.ipv6 is enabled, those non-IPv6 tests will not run. I do this because when it comes to nerdctl, IPv6 and non-IPv6 tests can not run together. (integration-tests-ipv6 uses host network that results in some problems with non-IPv6 tests. This may caused by running nerdctl tests nested inside docker and qemu).

I'd like to take your advice if there is a better idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehow the integration-tests-ipv6 fails to run nested inside docker and qemu, despite sysctl and daemon being configured. Later @Zheaoli figured out a solution by using network=host to run it successfully. However, this leads to non-IPv6 test failure. So I have to separate the IPv6 test.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, it should be able to run nested, but nested networks are complex and hard to debug, maybe you are missing certain sysctl or something, who knows


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 @@ -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"]
yuchanns marked this conversation as resolved.
Show resolved Hide resolved

FROM base AS demo
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) {
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
yuchanns marked this conversation as resolved.
Show resolved Hide resolved
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 IP6 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
3 changes: 3 additions & 0 deletions pkg/labels/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading
Loading