Allow configured address with no configured subnet

Signed-off-by: Rob Murray <rob.murray@docker.com>
This commit is contained in:
Rob Murray
2025-11-23 15:33:29 +00:00
parent 7e14b4d931
commit 84a251d039
4 changed files with 67 additions and 56 deletions

View File

@@ -540,8 +540,8 @@ func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *n
errs = normalizeEndpointIPAMConfig(errs, ipamConfig)
if nw != nil {
_, _, v4Configs, v6Configs := nw.IpamConfig()
errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Configs, v6Configs)
v4Info, v6Info := nw.IpamInfo()
errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Info, v6Info)
}
if sysctls, ok := epConfig.DriverOpts[netlabel.EndpointSysctls]; ok {
@@ -598,36 +598,28 @@ func normalizeEndpointIPAMConfig(errs []error, cfg *networktypes.EndpointIPAMCon
}
// validateIPAMConfigIsInRange checks whether static IP addresses are valid in a specific network.
func validateIPAMConfigIsInRange(errs []error, cfg *networktypes.EndpointIPAMConfig, v4Subnets, v6Subnets []*libnetwork.IpamConf) []error {
if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
func validateIPAMConfigIsInRange(errs []error, cfg *networktypes.EndpointIPAMConfig, v4Info, v6Info []*libnetwork.IpamInfo) []error {
if err := validateEndpointIPAddress(cfg.IPv4Address, v4Info); err != nil {
errs = append(errs, err)
}
if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
if err := validateEndpointIPAddress(cfg.IPv6Address, v6Info); err != nil {
errs = append(errs, err)
}
return errs
}
func validateEndpointIPAddress(epAddr netip.Addr, ipamSubnets []*libnetwork.IpamConf) error {
func validateEndpointIPAddress(epAddr netip.Addr, ipamInfo []*libnetwork.IpamInfo) error {
if !epAddr.IsValid() {
return nil
}
var staticSubnet bool
for _, subnet := range ipamSubnets {
if subnet.IsStatic() {
staticSubnet = true
if subnet.Contains(epAddr) {
for _, subnet := range ipamInfo {
if subnet.Pool.Contains(epAddr.AsSlice()) {
return nil
}
}
}
if staticSubnet {
return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
}
return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
return fmt.Errorf("no configured subnet contains IP address %s", epAddr)
}
// cleanOperationalData resets the operational data from the passed endpoint settings

View File

@@ -3,6 +3,7 @@ package daemon
import (
"encoding/json"
"errors"
"net"
"net/netip"
"testing"
@@ -10,6 +11,7 @@ import (
networktypes "github.com/moby/moby/api/types/network"
"github.com/moby/moby/v2/daemon/container"
"github.com/moby/moby/v2/daemon/libnetwork"
"github.com/moby/moby/v2/daemon/libnetwork/driverapi"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@@ -57,12 +59,12 @@ func buildNetwork(t *testing.T, config map[string]any) *libnetwork.Network {
return nw
}
func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
func TestEndpointIPAMInfoWithOutOfRangeAddrs(t *testing.T) {
tests := []struct {
name string
ipamConfig *networktypes.EndpointIPAMConfig
v4Subnets []*libnetwork.IpamConf
v6Subnets []*libnetwork.IpamConf
v4Pool string
v6Pool string
expectedErrors []string
}{
{
@@ -72,12 +74,8 @@ func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
IPv6Address: netip.MustParseAddr("2a01:d2:af:420b:25c1:1816:bb33:855c"),
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254"), netip.MustParseAddr("fe80::42:a8ff:fe33:6230")},
},
v4Subnets: []*libnetwork.IpamConf{
{PreferredPool: "192.168.100.0/24"},
},
v6Subnets: []*libnetwork.IpamConf{
{PreferredPool: "2a01:d2:af:420b:25c1:1816:bb33::/112"},
},
v4Pool: "192.168.100.0/24",
v6Pool: "2a01:d2:af:420b:25c1:1816:bb33::/112",
},
{
name: "static addresses out of range",
@@ -85,39 +83,28 @@ func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
IPv4Address: netip.MustParseAddr("192.168.100.10"),
IPv6Address: netip.MustParseAddr("2a01:d2:af:420b:25c1:1816:bb33:855c"),
},
v4Subnets: []*libnetwork.IpamConf{
{PreferredPool: "192.168.255.0/24"},
},
v6Subnets: []*libnetwork.IpamConf{
{PreferredPool: "2001:db8::/112"},
},
v4Pool: "192.168.255.0/24",
v6Pool: "2001:db8::/112",
expectedErrors: []string{
"no configured subnet or ip-range contain the IP address 192.168.100.10",
"no configured subnet or ip-range contain the IP address 2a01:d2:af:420b:25c1:1816:bb33:855c",
},
},
{
name: "static addresses with dynamic network subnets",
ipamConfig: &networktypes.EndpointIPAMConfig{
IPv4Address: netip.MustParseAddr("192.168.100.10"),
IPv6Address: netip.MustParseAddr("2a01:d2:af:420b:25c1:1816:bb33:855c"),
},
v4Subnets: []*libnetwork.IpamConf{
{},
},
v6Subnets: []*libnetwork.IpamConf{
{},
},
expectedErrors: []string{
"user specified IP address is supported only when connecting to networks with user configured subnets",
"user specified IP address is supported only when connecting to networks with user configured subnets",
"no configured subnet contains IP address 192.168.100.10",
"no configured subnet contains IP address 2a01:d2:af:420b:25c1:1816:bb33:855c",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
errs := validateIPAMConfigIsInRange(nil, tc.ipamConfig, tc.v4Subnets, tc.v6Subnets)
_, v4Pool, err := net.ParseCIDR(tc.v4Pool)
assert.NilError(t, err)
v4Info := []*libnetwork.IpamInfo{
{IPAMData: driverapi.IPAMData{Pool: v4Pool}},
}
_, v6Pool, err := net.ParseCIDR(tc.v6Pool)
assert.NilError(t, err)
v6Info := []*libnetwork.IpamInfo{
{IPAMData: driverapi.IPAMData{Pool: v6Pool}},
}
errs := validateIPAMConfigIsInRange(nil, tc.ipamConfig, v4Info, v6Info)
if tc.expectedErrors == nil {
assert.NilError(t, errors.Join(errs...))
return
@@ -125,7 +112,7 @@ func TestEndpointIPAMConfigWithOutOfRangeAddrs(t *testing.T) {
assert.Check(t, len(errs) == len(tc.expectedErrors), "errs: %+v", errs)
err := errors.Join(errs...)
err = errors.Join(errs...)
for _, expected := range tc.expectedErrors {
assert.Check(t, is.ErrorContains(err, expected))
}

View File

@@ -1357,10 +1357,10 @@ func (s *DockerNetworkSuite) TestDockerNetworkUnsupportedRequiredIP(c *testing.T
out, _, err := dockerCmdWithError("run", "-d", "--ip", "172.28.99.88", "--net", "n0", "busybox", "top")
assert.Assert(c, err != nil, "out: %s", out)
assert.Assert(c, is.Contains(out, "user specified IP address is supported only when connecting to networks with user configured subnets"))
assert.Assert(c, is.Contains(out, "no configured subnet contains IP address 172.28.99.88"))
out, _, err = dockerCmdWithError("run", "-d", "--ip6", "2001:db8:1234::9988", "--net", "n0", "busybox", "top")
assert.Assert(c, err != nil, "out: %s", out)
assert.Assert(c, is.Contains(out, "user specified IP address is supported only when connecting to networks with user configured subnets"))
assert.Assert(c, is.Contains(out, "no configured subnet contains IP address 2001:db8:1234::9988"))
cli.DockerCmd(c, "network", "rm", "n0")
assertNwNotAvailable(c, "n0")
}

View File

@@ -2058,3 +2058,35 @@ func TestDNSNamesForNonSwarmScopedNetworks(t *testing.T) {
container.WithAutoRemove)
assert.Equal(t, res.ExitCode, 0, "exit code: %d, expected 0; stdout:\n%s", res.ExitCode, res.Stdout)
}
// Check that when a network is created with no --subnet, a container can be
// started with a --ip in the subnet allocated from the default pools.
//
// Regression test for https://github.com/moby/moby/issues/51569
func TestSetIPWithNoConfiguredSubnet(t *testing.T) {
ctx := setupTest(t)
c := testEnv.APIClient()
const bridgeName = "subnet-from-pools"
network.CreateNoError(ctx, t, c, bridgeName, network.WithIPv6())
defer network.RemoveNoError(ctx, t, c, bridgeName)
insp := network.InspectNoError(ctx, t, c, bridgeName, client.NetworkInspectOptions{})
assert.Assert(t, is.Len(insp.Network.IPAM.Config, 2))
ip4 := insp.Network.IPAM.Config[0].Subnet.Addr().Next().Next().String()
ip6 := insp.Network.IPAM.Config[1].Subnet.Addr().Next().Next().String()
if insp.Network.IPAM.Config[0].Subnet.Addr().Is6() {
ip4, ip6 = ip6, ip4
}
res := container.RunAttach(ctx, t, c,
container.WithCmd("ip", "addr", "show", "eth0"),
container.WithNetworkMode(bridgeName),
container.WithIPv4(bridgeName, ip4),
container.WithIPv6(bridgeName, ip6),
)
if assert.Check(t, is.Equal(res.ExitCode, 0)) {
assert.Check(t, is.Contains(res.Stdout.String(), ip4))
assert.Check(t, is.Contains(res.Stdout.String(), ip6))
}
}