Skip to content

fix(network): match each gateway to its subnet for dual-stack#5010

Open
mayur-tolexo wants to merge 1 commit into
containerd:mainfrom
mayur-tolexo:fix/network-dual-stack-gateway
Open

fix(network): match each gateway to its subnet for dual-stack#5010
mayur-tolexo wants to merge 1 commit into
containerd:mainfrom
mayur-tolexo:fix/network-dual-stack-gateway

Conversation

@mayur-tolexo

@mayur-tolexo mayur-tolexo commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

A single --gateway gets applied to every --subnet, so creating a dual-stack network and passing both gateways fails — the IPv6 gateway ends up checked against the IPv4 subnet:

$ nerdctl network create --driver bridge --ipv6 \
    --subnet 172.20.0.0/16 --subnet fd00:dead:beef:1::/64 \
    --gateway 172.20.0.1 --gateway fd00:dead:beef:1::1 mynet-dual
FATA[0000] no matching subnet "172.20.0.0/16" for gateway "fd00:dead:beef:1::1"

This makes --gateway repeatable (--subnet already is) and matches each gateway to the subnet it belongs to, so the v4 gateway goes with the v4 subnet and the v6 one with the v6 subnet. A gateway that isn't inside any subnet still errors out.

Same command after the change:

$ nerdctl network create --driver bridge --ipv6 \
    --subnet 172.20.0.0/16 --subnet fd00:dead:beef:1::/64 \
    --gateway 172.20.0.1 --gateway fd00:dead:beef:1::1 mynet-dual
ab70041865f6e3a9bddf26b62c398179a24d4ded8d6b7907c904bbc051f7a341

$ nerdctl network inspect mynet-dual | jq ".[0].IPAM.Config"
[
  {
    "Subnet": "172.20.0.0/16",
    "Gateway": "172.20.0.1"
  },
  {
    "Subnet": "fd00:dead:beef:1::/64",
    "Gateway": "fd00:dead:beef:1::1"
  }
]

--ip-range has the same single-value-per-network limitation on dual-stack (Docker takes it per subnet too). I kept this PR to --gateway to stay focused on #4951; happy to do --ip-range the same way as a follow-up.

Closes #4951.

@mayur-tolexo

Copy link
Copy Markdown
Contributor Author

Ran the integration test locally (containerd v1.7.24, linux/arm64). The new dual-stack subtest plus the existing network create cases pass:

$ go test ./cmd/nerdctl/network/ -run "^TestNetworkCreate$" -count=1 -v
=== RUN   TestNetworkCreate
=== RUN   TestNetworkCreate/vanilla
=== RUN   TestNetworkCreate/with_MTU
=== RUN   TestNetworkCreate/with_ipv6
=== RUN   TestNetworkCreate/dual-stack_with_explicit_gateways
=== RUN   TestNetworkCreate/internal_enabled
--- PASS: TestNetworkCreate (0.00s)
PASS
ok  	github.com/containerd/nerdctl/v2/cmd/nerdctl/network	1.139s

@mayur-tolexo mayur-tolexo marked this pull request as draft June 25, 2026 15:36
@mayur-tolexo mayur-tolexo force-pushed the fix/network-dual-stack-gateway branch from 3217eb9 to d8307e8 Compare June 25, 2026 15:38
@mayur-tolexo

Copy link
Copy Markdown
Contributor Author

Checked this against Docker (29.4.0) to make sure the dual-stack behaviour lines up.

Docker:

$ docker network create --driver bridge --ipv6 \
    --subnet 10.83.0.0/16 --subnet fd00:cafe:1234::/64 \
    --gateway 10.83.0.1 --gateway fd00:cafe:1234::1 dt2
6d450fe9fbe6...
$ docker network inspect dt2 --format "{{json .IPAM.Config}}" | jq
[
  { "Subnet": "10.83.0.0/16", "Gateway": "10.83.0.1" },
  { "Subnet": "fd00:cafe:1234::/64", "Gateway": "fd00:cafe:1234::1" }
]

nerdctl with the same flags now gives the same result:

$ nerdctl network create --driver bridge --ipv6 \
    --subnet 10.83.0.0/16 --subnet fd00:cafe:1234::/64 \
    --gateway 10.83.0.1 --gateway fd00:cafe:1234::1 dt2
$ nerdctl network inspect dt2 --format "{{json .IPAM.Config}}" | jq
[
  { "Subnet": "10.83.0.0/16", "Gateway": "10.83.0.1" },
  { "Subnet": "fd00:cafe:1234::/64", "Gateway": "fd00:cafe:1234::1" }
]

One thing worth noting: Docker matches each gateway to whichever subnet contains it, not by position. Passing the gateways in reverse order still pairs them correctly, and this PR does the same (it is subnet.Contains(gw), not index based):

# both docker and nerdctl, gateways given v6-first
... --subnet 10.84.0.0/16 --subnet fd00:cafe:5678::/64 \
    --gateway fd00:cafe:5678::1 --gateway 10.84.0.1
-> 10.84.0.0/16          : 10.84.0.1
   fd00:cafe:5678::/64   : fd00:cafe:5678::1

And a gateway that is not inside any subnet errors the same way on both:

docker:  no matching subnet for gateway 192.168.1.1
nerdctl: no matching subnet for gateway "192.168.1.1"

@mayur-tolexo mayur-tolexo marked this pull request as ready for review June 25, 2026 17:17
Comment thread cmd/nerdctl/network/network_create.go Outdated
cmd.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options")
cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
cmd.Flags().String("gateway", "", `Gateway for the master subnet`)
cmd.Flags().StringArray("gateway", nil, `Gateway for the subnet; repeat for a dual-stack network, e.g. "10.5.0.1" and "fd00::1"`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this compatible with Docker?
If yes, the description should correspond to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it matches Docker. docker network create --gateway is repeatable and pairs each gateway with the subnet that contains it (verified on Docker 29.4.0, including reverse order and the no-matching-subnet error). I updated the flag help and the docs to Docker's wording, "IPv4 or IPv6 Gateway for the master subnet".

},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you can use the Tigron frameworks exit code constants

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, using expect.ExitCodeSuccess now.

Comment thread pkg/netutil/netutil_unix.go Outdated
gateway := ""
for j, g := range gateways {
gw := net.ParseIP(g)
if gw == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you could refactor the code like this for more readability

if gw := net.ParseIP(g); gw != nil && subnet.Contains(gw) {
    gateway, used[j] = g, true
				break
} else {
   return nil, findIPv4, fmt.Errorf("failed to parse gateway 
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pulled the parse out of the loop into a single up-front pass for readability. Kept it separate from the Contains check on purpose though: a valid gateway that is not in this subnet still belongs to another subnet on a dual-stack net, so an else error there would reject correct input.

Comment thread pkg/netutil/netutil_unix.go Outdated
}
// A gateway that matched no subnet is an error.
for j, g := range gateways {
if !used[j] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why do we do this check here can't we kind of refactor this logic to the above group somehow i feel like this part is not kind of needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This one has to stay outside the loop, a gateway only counts as "no matching subnet" once every subnet has been checked. Docker errors the same way, so I kept it and tightened the comment.

}

// Windows is single-subnet, so use at most one gateway.
gatewayStr := ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this used somewhere ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, just below at parseIPAMRange(subnet, gatewayStr, ipRangeStr). Windows is single-subnet so it takes at most the first gateway.

network create paired a single --gateway with every subnet, so on a
dual-stack network the IPv6 gateway was checked against the IPv4 subnet
and creation failed with "no matching subnet". Accept --gateway more than
once and match each gateway to the subnet that contains it.

Signed-off-by: Mayur Das <mayur.das@neevcloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nerdctl network create fails when creating dual-stack bridge network with explicit IPv4 and IPv6 gateways

3 participants