Skip to content
Open
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
32 changes: 19 additions & 13 deletions cmd/nerdctl/network/network_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func createCommand() *cobra.Command {
cmd.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
cmd.Flags().StringArray("gateway", nil, "IPv4 or IPv6 Gateway for the master subnet")
cmd.Flags().StringArray("ip-range", nil, `Allocate container ip from a sub-range`)
cmd.Flags().StringArray("aux-address", nil, "Auxiliary IPv4 or IPv6 addresses used by Network driver, as name=IP pairs. The IPs are reserved and never assigned to containers")
cmd.Flags().StringArray("label", nil, "Set metadata for a network")
cmd.Flags().Bool("ipv4", true, "Enable IPv4 networking (set to false together with --ipv6 for an IPv6-only network)")
cmd.Flags().Bool("ipv6", false, "Enable IPv6 networking")
Expand Down Expand Up @@ -93,6 +94,10 @@ func createAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
auxAddresses, err := cmd.Flags().GetStringArray("aux-address")
if err != nil {
return err
}
labels, err := cmd.Flags().GetStringArray("label")
if err != nil {
return err
Expand All @@ -112,18 +117,19 @@ func createAction(cmd *cobra.Command, args []string) error {
}

return network.Create(types.NetworkCreateOptions{
GOptions: globalOptions,
Name: name,
Driver: driver,
Options: strutil.ConvertKVStringsToMap(opts),
IPAMDriver: ipamDriver,
IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
Subnets: subnets,
Gateway: gateways,
IPRange: ipRanges,
Labels: labels,
IPv6: ipv6,
IPv4: &ipv4,
Internal: internal,
GOptions: globalOptions,
Name: name,
Driver: driver,
Options: strutil.ConvertKVStringsToMap(opts),
IPAMDriver: ipamDriver,
IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
Subnets: subnets,
Gateway: gateways,
IPRange: ipRanges,
AuxAddresses: auxAddresses,
Labels: labels,
IPv6: ipv6,
IPv4: &ipv4,
Internal: internal,
}, cmd.OutOrStdout())
}
70 changes: 70 additions & 0 deletions cmd/nerdctl/network/network_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,76 @@ func TestNetworkCreate(t *testing.T) {
}
},
},
{
Description: "with aux-address",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.0.0/24",
"--gateway", "10.6.0.1",
"--aux-address", "router=10.6.0.5",
"--aux-address", "dns=10.6.0.6",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("network", "inspect", data.Identifier())
},
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
return &test.Expected{
ExitCode: expect.ExitCodeSuccess,
Output: func(stdout string, t tig.T) {
netw := nerdtest.InspectNetwork(helpers, data.Identifier())
var aux map[string]string
for _, c := range netw.IPAM.Config {
if c.Subnet == "10.6.0.0/24" {
aux = c.AuxiliaryAddresses
}
}
assert.Equal(t, aux["router"], "10.6.0.5")
assert.Equal(t, aux["dns"], "10.6.0.6")
},
}
},
},
{
Description: "aux-address is reserved",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.1.0/24",
"--aux-address", "reserved=10.6.1.5",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
// The reserved address is carved out of the range, so requesting
// it explicitly must fail just as it does on Docker.
return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.1.5", testutil.CommonImage, "true")
},
Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil),
},
{
Description: "an un-reserved address is allocatable",
Setup: func(data test.Data, helpers test.Helpers) {
helpers.Ensure("network", "create", data.Identifier(),
"--subnet", "10.6.2.0/24",
"--aux-address", "reserved=10.6.2.5",
)
},
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("network", "rm", data.Identifier())
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
// Positive control: an address in the same subnet that is not
// reserved allocates fine, so the failure above is specific to the
// reserved IP rather than an unrelated --ip problem.
return helpers.Command("run", "--rm", "--net", data.Identifier(), "--ip", "10.6.2.7", testutil.CommonImage, "true")
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
}

testCase.Run(t)
Expand Down
3 changes: 2 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1279,12 +1279,13 @@ Flags:
- :whale: `--subnet`: Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"
- :whale: `--gateway`: IPv4 or IPv6 Gateway for the master subnet
- :whale: `--ip-range`: Allocate container ip from a sub-range
- :whale: `--aux-address`: Auxiliary IPv4 or IPv6 addresses, as `name=IP` pairs. Each IP is reserved and never assigned to a container. Repeatable, and matched to the subnet that contains it.
- :whale: `--label`: Set metadata on a network
- :whale: `--ipv4`: Enable IPv4. Enabled by default; set to false with `--ipv6` and an IPv6 subnet for an IPv6-only network. `--ipv4=false` is not supported on Windows.
- :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet.
- :whale: `--internal`: Restrict external access to the network.

Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--scope`
Unimplemented `docker network create` flags: `--attachable`, `--config-from`, `--config-only`, `--ingress`, `--scope`

### :whale: nerdctl network ls

Expand Down
7 changes: 5 additions & 2 deletions pkg/api/types/network_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ type NetworkCreateOptions struct {
Subnets []string
Gateway []string
IPRange []string
Labels []string
IPv6 bool
// AuxAddresses holds "name=IP" auxiliary addresses (docker --aux-address).
// Each IP is reserved so IPAM never hands it out to a container.
AuxAddresses []string
Labels []string
IPv6 bool
// IPv4 enables IPv4 on the network. A nil value defaults to enabled, so a
// directly-constructed NetworkCreateOptions keeps IPv4 on; setting it to
// false together with IPv6 yields an IPv6-only network.
Expand Down
10 changes: 10 additions & 0 deletions pkg/cmd/network/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ func Create(options types.NetworkCreateOptions, stdout io.Writer) error {
return fmt.Errorf("IPv6-only network requires an IPv6 subnet, specify --subnet manually")
}
if len(options.Subnets) == 0 {
// Docker matches each aux-address to a subnet that contains it, so
// without any subnet there is nothing to match. Surface the same
// "no matching subnet for aux-address <ip>" error Docker returns.
aux, err := netutil.ParseAuxAddresses(options.AuxAddresses)
if err != nil {
return err
}
for _, ip := range aux {
return fmt.Errorf("no matching subnet for aux-address %s", ip)
}
if len(options.Gateway) > 0 || len(options.IPRange) > 0 {
return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually")
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/cmd/network/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package network

import (
"io"
"testing"

"gotest.tools/v3/assert"

"github.com/containerd/nerdctl/v2/pkg/api/types"
)

// TestCreateAuxAddressWithoutSubnet verifies that an aux-address given without
// any subnet is rejected the same way Docker rejects it, before any CNI setup.
func TestCreateAuxAddressWithoutSubnet(t *testing.T) {
err := Create(types.NetworkCreateOptions{
AuxAddresses: []string{"host=10.9.0.5"},
}, io.Discard)
assert.ErrorContains(t, err, "no matching subnet for aux-address 10.9.0.5")
}
22 changes: 18 additions & 4 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -1052,9 +1052,10 @@ func getUlimitsFromNative(sp *specs.Spec) ([]*units.Ulimit, error) {
}

type IPAMConfig struct {
Subnet string `json:"Subnet,omitempty"`
Gateway string `json:"Gateway,omitempty"`
IPRange string `json:"IPRange,omitempty"`
Subnet string `json:"Subnet,omitempty"`
Gateway string `json:"Gateway,omitempty"`
IPRange string `json:"IPRange,omitempty"`
AuxiliaryAddresses map[string]string `json:"AuxiliaryAddresses,omitempty"`
}

type IPAM struct {
Expand Down Expand Up @@ -1196,7 +1197,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
res.Name = sCNI.Name
for _, plugin := range sCNI.Plugins {
for _, ranges := range plugin.Ipam.Ranges {
res.IPAM.Config = append(res.IPAM.Config, ranges...)
// A range-set normally describes one subnet; an aux-address
// reservation splits it into several sub-ranges whose first entry
// carries the subnet, gateway, ip-range and aux-addresses. Report the
// first entry per distinct subnet so a split subnet collapses to one
// IPAM.Config like Docker, without dropping entries for different
// subnets in the same set.
seen := make(map[string]struct{}, len(ranges))
for _, r := range ranges {
if _, ok := seen[r.Subnet]; ok {
continue
}
seen[r.Subnet] = struct{}{}
res.IPAM.Config = append(res.IPAM.Config, r)
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions pkg/inspecttypes/dockercompat/dockercompat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,25 @@ func TestGetUlimitsFromNative(t *testing.T) {
}
}

func TestNetworkFromNative(t *testing.T) {
// The first range-set is one subnet split into sub-ranges by an aux-address
// reservation and must collapse to a single IPAM.Config carrying the aux; the
// second set holds two distinct subnets that must both be kept; the empty set
// contributes nothing.
cni := `{"name":"testnet","plugins":[{"ipam":{"ranges":[` +
`[{"Subnet":"10.6.0.0/24","Gateway":"10.6.0.1","AuxiliaryAddresses":{"router":"10.6.0.5"}},{"Subnet":"10.6.0.0/24"}],` +
`[{"Subnet":"10.7.0.0/24"},{"Subnet":"10.8.0.0/24"}],` +
`[]` +
`]}}]}`
got, err := NetworkFromNative(&native.Network{CNI: []byte(cni)})
assert.NilError(t, err)
assert.DeepEqual(t, []IPAMConfig{
{Subnet: "10.6.0.0/24", Gateway: "10.6.0.1", AuxiliaryAddresses: map[string]string{"router": "10.6.0.5"}},
{Subnet: "10.7.0.0/24"},
{Subnet: "10.8.0.0/24"},
}, got.IPAM.Config)
}

func TestNetworkSettingsFromNative(t *testing.T) {
tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw")
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions pkg/netutil/cni_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ type IPAMRange struct {
RangeEnd string `json:"rangeEnd,omitempty"`
Gateway string `json:"gateway,omitempty"`
IPRange string `json:"ipRange,omitempty"`
// AuxiliaryAddresses records the reserved name=IP pairs for this subnet.
// host-local does not read it (reservation is done by splitting the range
// around each IP); it is nerdctl bookkeeping so `network inspect` can
// report AuxiliaryAddresses the way Docker does.
AuxiliaryAddresses map[string]string `json:"auxiliaryAddresses,omitempty"`

@AkihiroSuda AkihiroSuda Jul 14, 2026

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.

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.

}

type IPAMRoute struct {
Expand Down
Loading