From 84a251d039d1964899ba453a943f95c7ceb0bc75 Mon Sep 17 00:00:00 2001 From: Rob Murray Date: Sun, 23 Nov 2025 15:33:29 +0000 Subject: [PATCH] Allow configured address with no configured subnet Signed-off-by: Rob Murray --- daemon/container_operations.go | 28 ++++----- daemon/container_operations_test.go | 59 ++++++++----------- .../docker_cli_network_unix_test.go | 4 +- integration/networking/bridge_linux_test.go | 32 ++++++++++ 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/daemon/container_operations.go b/daemon/container_operations.go index 1ed84bb04e..c7795ecc76 100644 --- a/daemon/container_operations.go +++ b/daemon/container_operations.go @@ -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) { - return nil - } + 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 diff --git a/daemon/container_operations_test.go b/daemon/container_operations_test.go index bf58594003..03438aa16a 100644 --- a/daemon/container_operations_test.go +++ b/daemon/container_operations_test.go @@ -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)) } diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go index c27422f784..2539179a46 100644 --- a/integration-cli/docker_cli_network_unix_test.go +++ b/integration-cli/docker_cli_network_unix_test.go @@ -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") } diff --git a/integration/networking/bridge_linux_test.go b/integration/networking/bridge_linux_test.go index 705da61d4b..6b1bceb95e 100644 --- a/integration/networking/bridge_linux_test.go +++ b/integration/networking/bridge_linux_test.go @@ -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)) + } +}