From ea0d934ff251870e209d0130d09a043cd84100cc Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 29 May 2025 18:51:11 -0700 Subject: [PATCH] Allow requesting networks with a custom prefix size from the default pools Signed-off-by: David Negstad --- .../cnmallocator/networkallocator.go | 5 +- .../ipams/defaultipam/address_space.go | 38 ++++++-- .../ipams/defaultipam/address_space_test.go | 91 +++++++++++++++++- .../libnetwork/ipams/defaultipam/allocator.go | 32 +++++-- .../ipams/defaultipam/allocator_test.go | 81 +++++++++++++--- daemon/libnetwork/network.go | 15 ++- daemon/network.go | 44 ++++++--- integration/daemon/daemon_linux_test.go | 23 ++--- integration/internal/network/network.go | 15 +++ .../network/bridge/bridge_linux_test.go | 92 +++++++++++++++++++ 10 files changed, 381 insertions(+), 55 deletions(-) diff --git a/daemon/libnetwork/cnmallocator/networkallocator.go b/daemon/libnetwork/cnmallocator/networkallocator.go index 9aaf54d15a..f374e840f3 100644 --- a/daemon/libnetwork/cnmallocator/networkallocator.go +++ b/daemon/libnetwork/cnmallocator/networkallocator.go @@ -949,7 +949,10 @@ func (na *cnmNetworkAllocator) allocatePools(n *api.Network) (map[netip.Prefix]s } } - if ic.Subnet == "" { + // The IPAM config contain an unspecified subnet if a network with a specific prefix size + // was requested from the default pools. Therefore it's important to update the value in the + // config with the actual allocated subnet if available. + if alloc.Pool.String() != "" { ic.Subnet = alloc.Pool.String() } diff --git a/daemon/libnetwork/ipams/defaultipam/address_space.go b/daemon/libnetwork/ipams/defaultipam/address_space.go index a1b3ec0805..2396c87222 100644 --- a/daemon/libnetwork/ipams/defaultipam/address_space.go +++ b/daemon/libnetwork/ipams/defaultipam/address_space.go @@ -133,7 +133,7 @@ func (aSpace *addrSpace) allocatePool(nw netip.Prefix) error { // with existing allocations and 'reserved' prefixes. // // This method is safe for concurrent use. -func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.Prefix, error) { +func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix, prefixSize int) (netip.Prefix, error) { aSpace.mu.Lock() defer aSpace.mu.Unlock() @@ -150,6 +150,29 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip. return subnet } + // Filter the pools to only those that match the requested subnet size (if one is specified). + var predefined []*ipamutils.NetworkToSplit + if prefixSize == 0 { + predefined = aSpace.predefined + } else { + for _, pdf := range aSpace.predefined { + if pdf.Base.Bits() > prefixSize || prefixSize > pdf.Base.Addr().BitLen() { + // The subnet size isn't valid for the pool + continue + } + + predefined = append(predefined, &ipamutils.NetworkToSplit{ + Base: pdf.Base, + Size: prefixSize, + }) + } + } + + if len(predefined) == 0 { + // If we don't have any valid predefined networks + return netip.Prefix{}, ipamapi.ErrInvalidPool + } + for { allocated := it.Get() if allocated == (netip.Prefix{}) { @@ -157,10 +180,11 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip. break } - if pdfID >= len(aSpace.predefined) { + if pdfID >= len(predefined) { + // We ran out of predefined networks. return netip.Prefix{}, ipamapi.ErrNoMoreSubnets } - pdf := aSpace.predefined[pdfID] + pdf := predefined[pdfID] if allocated.Overlaps(pdf.Base) { if allocated.Bits() <= pdf.Base.Bits() { @@ -240,7 +264,7 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip. prevAlloc = allocated } - if pdfID >= len(aSpace.predefined) { + if pdfID >= len(predefined) { return netip.Prefix{}, ipamapi.ErrNoMoreSubnets } @@ -248,7 +272,7 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip. // networks. Let's try two more times (once on the current 'pdf', and once // on the next network if any). if partialOverlap { - pdf := aSpace.predefined[pdfID] + pdf := predefined[pdfID] if next := netiputil.PrefixAfter(prevAlloc, pdf.Size); pdf.Overlaps(next) { return makeAlloc(next), nil @@ -268,8 +292,8 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip. // overlapped at all. // // Hence, we're sure 'pdfID' has never been subnetted yet. - if pdfID < len(aSpace.predefined) { - pdf := aSpace.predefined[pdfID] + if pdfID < len(predefined) { + pdf := predefined[pdfID] next := pdf.FirstPrefix() return makeAlloc(next), nil diff --git a/daemon/libnetwork/ipams/defaultipam/address_space_test.go b/daemon/libnetwork/ipams/defaultipam/address_space_test.go index 9c79c959f8..ff5787121b 100644 --- a/daemon/libnetwork/ipams/defaultipam/address_space_test.go +++ b/daemon/libnetwork/ipams/defaultipam/address_space_test.go @@ -32,6 +32,7 @@ func TestDynamicPoolAllocation(t *testing.T) { testcases := []struct { name string predefined []*ipamutils.NetworkToSplit + subnetSize int allocated []netip.Prefix reserved []netip.Prefix expPrefix netip.Prefix @@ -343,6 +344,92 @@ func TestDynamicPoolAllocation(t *testing.T) { }, expPrefix: netip.MustParsePrefix("192.168.0.0/24"), }, + { + name: "Smaller requested network subnet size than predefined", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16}, + }, + subnetSize: 24, + expPrefix: netip.MustParsePrefix("172.17.0.0/24"), + }, + { + name: "Larger requested network subnet size than predefined", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 20}, + }, + subnetSize: 16, + expPrefix: netip.MustParsePrefix("172.17.0.0/16"), + }, + { + name: "Invalid specified network subnet size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16}, + }, + subnetSize: 150, + expErr: ipamapi.ErrInvalidPool, + }, + { + name: "Partially allocated predefined pool with specified size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16}, + }, + allocated: []netip.Prefix{ + netip.MustParsePrefix("172.17.0.0/20"), + }, + subnetSize: 24, + expPrefix: netip.MustParsePrefix("172.17.16.0/24"), + }, + { + name: "Partially allocated predefined with gap and specified size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16}, + }, + allocated: []netip.Prefix{ + netip.MustParsePrefix("172.17.0.0/20"), + netip.MustParsePrefix("172.18.0.0/20"), + }, + subnetSize: 24, + expPrefix: netip.MustParsePrefix("172.17.16.0/24"), + }, + { + name: "Partially allocated avoids overlapping", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16}, + }, + allocated: []netip.Prefix{ + netip.MustParsePrefix("172.17.0.0/24"), + }, + expPrefix: netip.MustParsePrefix("172.18.0.0/16"), + }, + { + name: "Multiple predefined pools last one satisfies specified size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 24}, + {Base: netip.MustParsePrefix("10.0.0.0/12"), Size: 20}, + }, + subnetSize: 20, + expPrefix: netip.MustParsePrefix("10.0.0.0/20"), + }, + { + name: "Multiple predefined pools none matching specified size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/24"), Size: 24}, + {Base: netip.MustParsePrefix("172.18.0.0/20"), Size: 20}, + {Base: netip.MustParsePrefix("172.19.0.0/24"), Size: 24}, + }, + subnetSize: 16, + expErr: ipamapi.ErrInvalidPool, + }, + { + name: "Multiple predefined pools none valid for specified size", + predefined: []*ipamutils.NetworkToSplit{ + {Base: netip.MustParsePrefix("172.17.0.0/24"), Size: 24}, + {Base: netip.MustParsePrefix("172.18.0.0/20"), Size: 20}, + {Base: netip.MustParsePrefix("172.19.0.0/24"), Size: 23}, + }, + subnetSize: 33, + expErr: ipamapi.ErrInvalidPool, + }, } for _, tc := range testcases { @@ -351,7 +438,7 @@ func TestDynamicPoolAllocation(t *testing.T) { assert.NilError(t, err) as.allocated = tc.allocated - p, err := as.allocatePredefinedPool(tc.reserved) + p, err := as.allocatePredefinedPool(tc.reserved, tc.subnetSize) assert.Check(t, is.ErrorIs(err, tc.expErr)) assert.Check(t, is.Equal(p, tc.expPrefix)) @@ -465,7 +552,7 @@ func TestPoolAllocateAndRelease(t *testing.T) { // Allocate a pool for netname, check that a subnet is returned that // isn't already allocated, and doesn't overlap with a reserved range. alloc: func(netname string) { - subnet, err := as.allocatePredefinedPool(tc.reserved) + subnet, err := as.allocatePredefinedPool(tc.reserved, 0) assert.NilError(t, err) otherNetname, exists := subnetToNetname[subnet] diff --git a/daemon/libnetwork/ipams/defaultipam/allocator.go b/daemon/libnetwork/ipams/defaultipam/allocator.go index f0f469f70d..9e3c78d6e4 100644 --- a/daemon/libnetwork/ipams/defaultipam/allocator.go +++ b/daemon/libnetwork/ipams/defaultipam/allocator.go @@ -134,22 +134,42 @@ func (a *Allocator) RequestPool(req ipamapi.PoolRequest) (ipamapi.AllocatedPool, if err != nil { return ipamapi.AllocatedPool{}, err } + + k := PoolID{AddressSpace: req.AddressSpace} + + prefixLength := 0 + + if req.Pool != "" { + prefix, err := netip.ParsePrefix(req.Pool) + if err != nil { + return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidPool) + } + + if prefix.Addr().IsUnspecified() { + // If the prefix is unspecified, we're only interested in the prefix size. + // We'll attempt to use the specified size to allocate a subnet from the + // predefined pools. + req.Pool = "" + + if prefix.Bits() > 0 { + prefixLength = prefix.Bits() + } + } else { + k.Subnet = prefix + } + } + if req.Pool == "" && req.SubPool != "" { return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidSubPool) } - k := PoolID{AddressSpace: req.AddressSpace} if req.Pool == "" { - if k.Subnet, err = aSpace.allocatePredefinedPool(req.Exclude); err != nil { + if k.Subnet, err = aSpace.allocatePredefinedPool(req.Exclude, prefixLength); err != nil { return ipamapi.AllocatedPool{}, err } return ipamapi.AllocatedPool{PoolID: k.String(), Pool: k.Subnet}, nil } - if k.Subnet, err = netip.ParsePrefix(req.Pool); err != nil { - return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidPool) - } - if req.SubPool != "" { if k.ChildSubnet, err = netip.ParsePrefix(req.SubPool); err != nil { return ipamapi.AllocatedPool{}, types.InternalErrorf("invalid pool request: %v", ipamapi.ErrInvalidSubPool) diff --git a/daemon/libnetwork/ipams/defaultipam/allocator_test.go b/daemon/libnetwork/ipams/defaultipam/allocator_test.go index adde28db56..4cc51c5e7e 100644 --- a/daemon/libnetwork/ipams/defaultipam/allocator_test.go +++ b/daemon/libnetwork/ipams/defaultipam/allocator_test.go @@ -285,6 +285,61 @@ func TestPredefinedPool(t *testing.T) { } } +func TestPredefinedPoolWithPreferredSubnetSize(t *testing.T) { + a, err := NewAllocator(ipamutils.GetLocalScopeDefaultNetworks(), ipamutils.GetGlobalScopeDefaultNetworks()) + assert.NilError(t, err) + + alloc1, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/24"}) + if err != nil { + t.Fatal(err) + } + + alloc2, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace}) + if err != nil { + t.Fatal(err) + } + + if alloc1.Pool == alloc2.Pool { + t.Fatalf("Unexpected default network returned: %s = %s", alloc2.Pool, alloc1.Pool) + } + + if alloc1.Pool.Bits() != 24 { + t.Fatalf("Unexpected default network size: %s != 24", alloc1.Pool) + } + + if alloc2.Pool.Bits() == 24 { + t.Fatalf("Unexpected default network size: %s == 24", alloc2.Pool) + } + + // Release the second pool first + if err := a.ReleasePool(alloc2.PoolID); err != nil { + t.Fatal(err) + } + + _, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "/24"}) + if err == nil { + t.Fatal(err, "Expected failure requesting pool with unspecified address family") + } + + alloc4, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/25"}) + if err != nil { + t.Fatal(err) + } + + if alloc4.Pool.Bits() != 25 { + t.Fatalf("Unexpected default network size: %s != 25", alloc4.Pool) + } + + if err := a.ReleasePool(alloc4.PoolID); err != nil { + t.Fatal(err) + } + + // Check invalid subnet size requests + if _, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/AB"}); err == nil { + t.Fatalf("Expected failure requesting pool with invalid subnet size") + } +} + func TestRemoveSubnet(t *testing.T) { a, err := NewAllocator(ipamutils.GetLocalScopeDefaultNetworks(), ipamutils.GetGlobalScopeDefaultNetworks()) assert.NilError(t, err) @@ -755,14 +810,15 @@ func TestOverlappingRequests(t *testing.T) { {[]string{"10.0.0.0/8"}, "11.0.0.0/8", true}, {[]string{"74.0.0.0/7"}, "9.111.99.72/30", true}, {[]string{"110.192.0.0/10"}, "16.0.0.0/10", true}, + {[]string{"0.0.0.0/16"}, "0.0.0.0/16", true}, // two default allocations should succeed // Previously allocated network entirely contains request {[]string{"10.0.0.0/8"}, "10.0.0.0/8", false}, // exact overlap - {[]string{"0.0.0.0/1"}, "16.182.0.0/15", false}, + {[]string{"16.182.0.0/15"}, "16.182.0.0/16", false}, {[]string{"16.0.0.0/4"}, "17.11.66.0/23", false}, // Previously allocated network overlaps beginning of request - {[]string{"0.0.0.0/1"}, "0.0.0.0/0", false}, + {[]string{"16.182.0.0/16"}, "16.182.0.0/15", false}, {[]string{"64.0.0.0/6"}, "64.0.0.0/3", false}, {[]string{"112.0.0.0/6"}, "112.0.0.0/4", false}, @@ -774,18 +830,21 @@ func TestOverlappingRequests(t *testing.T) { // Previously allocated network entirely contained within request {[]string{"10.0.0.0/8"}, "10.0.0.0/6", false}, // non-canonical {[]string{"10.0.0.0/8"}, "8.0.0.0/6", false}, // canonical - {[]string{"25.173.144.0/20"}, "0.0.0.0/0", false}, + {[]string{"25.173.144.0/20"}, "25.173.143.0/16", false}, // IPv6 + {[]string{"::/0"}, "::/0", true}, // two default allocations should succeed + {[]string{"f000::/4"}, "::/0", true}, // default allocation shouldn't overlap explicit allocation + // Previously allocated network entirely contains request - {[]string{"::/0"}, "f656:3484:c878:a05:e540:a6ed:4d70:3740/123", false}, + {[]string{"f656::/0"}, "f656:3484:c878:a05:e540:a6ed:4d70:3740/123", false}, {[]string{"8000::/1"}, "8fe8:e7c4:5779::/49", false}, {[]string{"f000::/4"}, "ffc7:6000::/19", false}, // Previously allocated network overlaps beginning of request - {[]string{"::/2"}, "::/0", false}, - {[]string{"::/3"}, "::/1", false}, - {[]string{"::/6"}, "::/5", false}, + {[]string{"f656::/20"}, "f656::/16", false}, + {[]string{"8000::/32"}, "8000::/31", false}, + {[]string{"f000::/60"}, "f000::/20", false}, // Previously allocated network overlaps end of request {[]string{"c000::/2"}, "8000::/1", false}, @@ -793,7 +852,7 @@ func TestOverlappingRequests(t *testing.T) { {[]string{"cf80::/9"}, "c000::/4", false}, // Previously allocated network entirely contained within request - {[]string{"ff77:93f8::/29"}, "::/0", false}, + {[]string{"ff77:93f8::/29"}, "ff77:93f7::/28", false}, {[]string{"9287:2e20:5134:fab6:9061:a0c6:bfe3:9400/119"}, "8000::/1", false}, {[]string{"3ea1:bfa9:8691:d1c6:8c46:519b:db6d:e700/120"}, "3000::/4", false}, } @@ -805,15 +864,15 @@ func TestOverlappingRequests(t *testing.T) { // Set up some existing allocations. This should always succeed. for _, env := range tc.environment { _, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: env}) - assert.NilError(t, err) + assert.NilError(t, err, "error requesting pool %v, %v", localAddressSpace, env) } // Make the test allocation. _, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: tc.subnet}) if tc.ok { - assert.NilError(t, err) + assert.NilError(t, err, "error requesting pool %v, %v", localAddressSpace, tc.subnet) } else { - assert.Check(t, is.ErrorContains(err, "")) + assert.Check(t, is.ErrorContains(err, ""), "expected error requesting overlapping pool %v, %v", localAddressSpace, tc.subnet) } } } diff --git a/daemon/libnetwork/network.go b/daemon/libnetwork/network.go index e0263fbe34..cbbaa736c5 100644 --- a/daemon/libnetwork/network.go +++ b/daemon/libnetwork/network.go @@ -1533,10 +1533,21 @@ func (n *Network) ipamAllocateVersion(ipam ipamapi.Ipam, v6 bool, ipamConf []*Ip reserved = netutils.InferReservedNetworks(v6) } + // Determine if the preferred pool is unspecified (blank, or a 0.0.0.0 or :: address) + prefPool := cfg.PreferredPool + isDefaultPool := prefPool == "" + if !isDefaultPool { + if prefix, err := netip.ParsePrefix(prefPool); err != nil { + // This should never happen + return nil, types.InvalidParameterErrorf("invalid preferred address %q: %v", prefPool, err) + } else if prefix.Addr().IsUnspecified() { + isDefaultPool = true + } + } + // During network restore, if no subnet was specified in the original network-create // request, use the previously allocated subnet. - prefPool := cfg.PreferredPool - if prefPool == "" && len(ipamInfo) > i { + if isDefaultPool && len(ipamInfo) > i { prefPool = ipamInfo[i].Pool.String() } diff --git a/daemon/network.go b/daemon/network.go index 99dedc5dc3..531b41a2e3 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -812,27 +812,49 @@ func buildIPAMResources(nw *libnetwork.Network) networktypes.IPAM { var ipamConfig []networktypes.IPAMConfig ipamDriver, ipamOptions, ipv4Conf, ipv6Conf := nw.IpamConfig() + ipv4Info, ipv6Info := nw.IpamInfo() hasIPv4Config := false - for _, cfg := range ipv4Conf { - if cfg.PreferredPool == "" { - continue + if len(ipv4Info) > 0 { + // Only check ipv4 networks if there were any allocated + for i, cfg := range ipv4Conf { + if cfg.PreferredPool == "" { + continue + } + hasIPv4Config = true + subnet := ipv4Info[i].IPAMData.Pool + if subnet != nil { + cfg.PreferredPool = subnet.String() + } + if ipv4Info[i].IPAMData.Gateway != nil && cfg.Gateway == "" { + cfg.Gateway = ipv4Info[i].IPAMData.Gateway.IP.String() + } + + ipamConfig = append(ipamConfig, cfg.IPAMConfig()) } - hasIPv4Config = true - ipamConfig = append(ipamConfig, cfg.IPAMConfig()) } hasIPv6Config := false - for _, cfg := range ipv6Conf { - if cfg.PreferredPool == "" { - continue + if len(ipv6Info) > 0 { + // Only check ipv6 networks if there were any allocated + for i, cfg := range ipv6Conf { + if cfg.PreferredPool == "" { + continue + } + hasIPv6Config = true + subnet := ipv6Info[i].IPAMData.Pool + if subnet != nil { + cfg.PreferredPool = subnet.String() + } + + if ipv6Info[i].IPAMData.Gateway != nil && cfg.Gateway == "" { + cfg.Gateway = ipv6Info[i].IPAMData.Gateway.IP.String() + } + ipamConfig = append(ipamConfig, cfg.IPAMConfig()) } - hasIPv6Config = true - ipamConfig = append(ipamConfig, cfg.IPAMConfig()) } if !hasIPv4Config || !hasIPv6Config { - ipv4Info, ipv6Info := nw.IpamInfo() if !hasIPv4Config { for _, info := range ipv4Info { ipamConfig = append(ipamConfig, info.IPAMData.IPAMConfig()) diff --git a/integration/daemon/daemon_linux_test.go b/integration/daemon/daemon_linux_test.go index 3a8d741f67..a19e971c17 100644 --- a/integration/daemon/daemon_linux_test.go +++ b/integration/daemon/daemon_linux_test.go @@ -70,8 +70,8 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) { "--fixed-cidr-v6", "fdd1:8161:2d2c::/64", }, expIPAMConfig: []network.IPAMConfig{ - {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")}, - {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64")}, + {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")}, + {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")}, }, }, { @@ -123,7 +123,7 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) { "--fixed-cidr-v6", "fe80::/64", }, expIPAMConfig: []network.IPAMConfig{ - {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")}, + {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")}, {Subnet: netip.MustParsePrefix("fe80::/64"), IPRange: netip.MustParsePrefix("fe80::/64"), Gateway: llGwPlaceholder}, }, }, @@ -173,15 +173,8 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) { // The bridge's address/subnet should be ignored, this is a change // of fixed-cidr. expIPAMConfig: []network.IPAMConfig{ - {Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24")}, - {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64")}, - // No Gateway is configured, because the address could not be learnt from the - // bridge. An address will have been allocated but, because there's config (the - // fixed-cidr), inspect shows just the config. (Surprisingly, when there's no - // config at all, the inspect output still says its showing config but actually - // shows the running state.) When the daemon is restarted, after a gateway - // address has been assigned to the bridge, that address will become config - so - // a Gateway address will show up in the inspect output. + {Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.177.1")}, + {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:1::1")}, }, }, { @@ -225,8 +218,8 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) { "--fixed-cidr-v6", "fdd1:8161:2d2c::/64", }, expIPAMConfig: []network.IPAMConfig{ - {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")}, - {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64")}, + {Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")}, + {Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")}, }, }, { @@ -428,7 +421,7 @@ func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIP expIPAMConfig[i].Gateway = llAddr } } - assert.Check(t, is.DeepEqual(res.Network.IPAM.Config, expIPAMConfig, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}))) + assert.Check(t, is.DeepEqual(res.Network.IPAM.Config, expIPAMConfig, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})), "unexpected IPAM config '%s'", tc.name) }) }) } diff --git a/integration/internal/network/network.go b/integration/internal/network/network.go index d8c9c0fc1a..8120178704 100644 --- a/integration/internal/network/network.go +++ b/integration/internal/network/network.go @@ -33,6 +33,21 @@ func CreateNoError(ctx context.Context, t *testing.T, apiClient client.APIClient return name } +// Inspect inspects a network with the specified options +func Inspect(ctx context.Context, apiClient client.APIClient, name string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { + return apiClient.NetworkInspect(ctx, name, options) +} + +// InspectNoError inspects a network with the specified options and verifies there were no errors +func InspectNoError(ctx context.Context, t *testing.T, apiClient client.APIClient, name string, options client.NetworkInspectOptions) client.NetworkInspectResult { + t.Helper() + + c, err := apiClient.NetworkInspect(ctx, name, options) + assert.NilError(t, err) + + return c +} + func RemoveNoError(ctx context.Context, t *testing.T, apiClient client.APIClient, name string) { t.Helper() diff --git a/integration/network/bridge/bridge_linux_test.go b/integration/network/bridge/bridge_linux_test.go index 9ac4400482..9e9d3bc598 100644 --- a/integration/network/bridge/bridge_linux_test.go +++ b/integration/network/bridge/bridge_linux_test.go @@ -1287,3 +1287,95 @@ func TestJoinError(t *testing.T) { res = ctr.ExecT(ctx, t, c, cid, []string{"ip", "link", "show", "eth1"}) assert.Check(t, is.Equal(res.ExitCode, 0), "container should have an eth1") } + +func TestPreferredSubnetRestore(t *testing.T) { + skip.If(t, testEnv.IsRootless(), "fails before and after restart") + + ctx := setupTest(t) + d := daemon.New(t) + d.StartWithBusybox(ctx, t) + defer d.Stop(t) + c := d.NewClientT(t) + + const v4netName = "testnetv4restore" + network.CreateNoError(ctx, t, c, v4netName, + network.WithIPv4(true), + network.WithIPAMConfig(networktypes.IPAMConfig{ + Subnet: netip.MustParsePrefix("0.0.0.0/24"), + }), + ) + + defer func() { network.RemoveNoError(ctx, t, c, v4netName) }() + + const v6netName = "testnetv6restore" + network.CreateNoError(ctx, t, c, v6netName, + network.WithIPv4(false), + network.WithIPv6(), + network.WithIPAMConfig(networktypes.IPAMConfig{ + Subnet: netip.MustParsePrefix("::/120"), + }), + ) + + defer func() { network.RemoveNoError(ctx, t, c, v6netName) }() + + const dualStackNetName = "testnetdualrestore" + network.CreateNoError(ctx, t, c, dualStackNetName, + network.WithIPv4(true), + network.WithIPv6(), + network.WithIPAMConfig(networktypes.IPAMConfig{ + Subnet: netip.MustParsePrefix("0.0.0.0/24"), + }, networktypes.IPAMConfig{ + Subnet: netip.MustParsePrefix("::/120"), + }), + ) + + defer func() { network.RemoveNoError(ctx, t, c, dualStackNetName) }() + + inspOpts := client.NetworkInspectOptions{} + + v4Insp := network.InspectNoError(ctx, t, c, v4netName, inspOpts) + assert.Check(t, is.Len(v4Insp.Network.IPAM.Config, 1)) + v4allocCidr := v4Insp.Network.IPAM.Config[0].Subnet + assert.Check(t, is.Equal(v4allocCidr.Addr().IsUnspecified(), false), "expected specific subnet") + + v6Insp := network.InspectNoError(ctx, t, c, v6netName, inspOpts) + assert.Check(t, is.Len(v6Insp.Network.IPAM.Config, 1)) + v6allocCidr := v6Insp.Network.IPAM.Config[0].Subnet + assert.Check(t, is.Equal(v6allocCidr.Addr().IsUnspecified(), false), "expected specific subnet") + + dualStackInsp := network.InspectNoError(ctx, t, c, dualStackNetName, inspOpts) + assert.Check(t, is.Len(dualStackInsp.Network.IPAM.Config, 2)) + var dualv4, dualv6 netip.Prefix + if dualStackInsp.Network.IPAM.Config[0].Subnet.Addr().Is4() { + dualv4 = dualStackInsp.Network.IPAM.Config[0].Subnet + dualv6 = dualStackInsp.Network.IPAM.Config[1].Subnet + } else { + dualv4 = dualStackInsp.Network.IPAM.Config[1].Subnet + dualv6 = dualStackInsp.Network.IPAM.Config[0].Subnet + } + assert.Check(t, is.Equal(dualv4.Addr().IsUnspecified(), false), "expected specific v4 subnet") + assert.Check(t, is.Equal(dualv6.Addr().IsUnspecified(), false), "expected specific v6 subnet") + + d.Restart(t) + + v4Insp = network.InspectNoError(ctx, t, c, v4netName, inspOpts) + assert.Check(t, is.Len(v4Insp.Network.IPAM.Config, 1)) + assert.Check(t, is.Equal(v4Insp.Network.IPAM.Config[0].Subnet, v4allocCidr)) + + v6Insp = network.InspectNoError(ctx, t, c, v6netName, inspOpts) + assert.Check(t, is.Len(v6Insp.Network.IPAM.Config, 1)) + assert.Check(t, is.Equal(v6Insp.Network.IPAM.Config[0].Subnet, v6allocCidr)) + + dualStackInsp = network.InspectNoError(ctx, t, c, dualStackNetName, inspOpts) + assert.Check(t, is.Len(dualStackInsp.Network.IPAM.Config, 2)) + var dualv4after, dualv6after netip.Prefix + if dualStackInsp.Network.IPAM.Config[0].Subnet.Addr().Is4() { + dualv4after = dualStackInsp.Network.IPAM.Config[0].Subnet + dualv6after = dualStackInsp.Network.IPAM.Config[1].Subnet + } else { + dualv4after = dualStackInsp.Network.IPAM.Config[1].Subnet + dualv6after = dualStackInsp.Network.IPAM.Config[0].Subnet + } + assert.Check(t, is.Equal(dualv4after, dualv4), "expected same v4 subnet after restart") + assert.Check(t, is.Equal(dualv6after, dualv6), "expected same v6 subnet after restart") +}