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) errs = normalizeEndpointIPAMConfig(errs, ipamConfig)
if nw != nil { if nw != nil {
_, _, v4Configs, v6Configs := nw.IpamConfig() v4Info, v6Info := nw.IpamInfo()
errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Configs, v6Configs) errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Info, v6Info)
} }
if sysctls, ok := epConfig.DriverOpts[netlabel.EndpointSysctls]; ok { 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. // validateIPAMConfigIsInRange checks whether static IP addresses are valid in a specific network.
func validateIPAMConfigIsInRange(errs []error, cfg *networktypes.EndpointIPAMConfig, v4Subnets, v6Subnets []*libnetwork.IpamConf) []error { func validateIPAMConfigIsInRange(errs []error, cfg *networktypes.EndpointIPAMConfig, v4Info, v6Info []*libnetwork.IpamInfo) []error {
if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil { if err := validateEndpointIPAddress(cfg.IPv4Address, v4Info); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil { if err := validateEndpointIPAddress(cfg.IPv6Address, v6Info); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
return errs return errs
} }
func validateEndpointIPAddress(epAddr netip.Addr, ipamSubnets []*libnetwork.IpamConf) error { func validateEndpointIPAddress(epAddr netip.Addr, ipamInfo []*libnetwork.IpamInfo) error {
if !epAddr.IsValid() { if !epAddr.IsValid() {
return nil return nil
} }
var staticSubnet bool for _, subnet := range ipamInfo {
for _, subnet := range ipamSubnets { if subnet.Pool.Contains(epAddr.AsSlice()) {
if subnet.IsStatic() {
staticSubnet = true
if subnet.Contains(epAddr) {
return nil return nil
} }
} }
}
if staticSubnet { return fmt.Errorf("no configured subnet contains IP address %s", epAddr)
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")
} }
// cleanOperationalData resets the operational data from the passed endpoint settings // cleanOperationalData resets the operational data from the passed endpoint settings

View File

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

View File

@@ -2058,3 +2058,35 @@ func TestDNSNamesForNonSwarmScopedNetworks(t *testing.T) {
container.WithAutoRemove) container.WithAutoRemove)
assert.Equal(t, res.ExitCode, 0, "exit code: %d, expected 0; stdout:\n%s", res.ExitCode, res.Stdout) 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))
}
}