Browse code

Allow requesting networks with a custom prefix size from the default pools

Signed-off-by: David Negstad <David.Negstad@microsoft.com>

David Negstad authored on 2025/05/30 10:51:11
Showing 10 changed files
... ...
@@ -949,7 +949,10 @@ func (na *cnmNetworkAllocator) allocatePools(n *api.Network) (map[netip.Prefix]s
949 949
 			}
950 950
 		}
951 951
 
952
-		if ic.Subnet == "" {
952
+		// The IPAM config contain an unspecified subnet if a network with a specific prefix size
953
+		// was requested from the default pools. Therefore it's important to update the value in the
954
+		// config with the actual allocated subnet if available.
955
+		if alloc.Pool.String() != "" {
953 956
 			ic.Subnet = alloc.Pool.String()
954 957
 		}
955 958
 
... ...
@@ -133,7 +133,7 @@ func (aSpace *addrSpace) allocatePool(nw netip.Prefix) error {
133 133
 // with existing allocations and 'reserved' prefixes.
134 134
 //
135 135
 // This method is safe for concurrent use.
136
-func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.Prefix, error) {
136
+func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix, prefixSize int) (netip.Prefix, error) {
137 137
 	aSpace.mu.Lock()
138 138
 	defer aSpace.mu.Unlock()
139 139
 
... ...
@@ -150,6 +150,29 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
150 150
 		return subnet
151 151
 	}
152 152
 
153
+	// Filter the pools to only those that match the requested subnet size (if one is specified).
154
+	var predefined []*ipamutils.NetworkToSplit
155
+	if prefixSize == 0 {
156
+		predefined = aSpace.predefined
157
+	} else {
158
+		for _, pdf := range aSpace.predefined {
159
+			if pdf.Base.Bits() > prefixSize || prefixSize > pdf.Base.Addr().BitLen() {
160
+				// The subnet size isn't valid for the pool
161
+				continue
162
+			}
163
+
164
+			predefined = append(predefined, &ipamutils.NetworkToSplit{
165
+				Base: pdf.Base,
166
+				Size: prefixSize,
167
+			})
168
+		}
169
+	}
170
+
171
+	if len(predefined) == 0 {
172
+		// If we don't have any valid predefined networks
173
+		return netip.Prefix{}, ipamapi.ErrInvalidPool
174
+	}
175
+
153 176
 	for {
154 177
 		allocated := it.Get()
155 178
 		if allocated == (netip.Prefix{}) {
... ...
@@ -157,10 +180,11 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
157 157
 			break
158 158
 		}
159 159
 
160
-		if pdfID >= len(aSpace.predefined) {
160
+		if pdfID >= len(predefined) {
161
+			// We ran out of predefined networks.
161 162
 			return netip.Prefix{}, ipamapi.ErrNoMoreSubnets
162 163
 		}
163
-		pdf := aSpace.predefined[pdfID]
164
+		pdf := predefined[pdfID]
164 165
 
165 166
 		if allocated.Overlaps(pdf.Base) {
166 167
 			if allocated.Bits() <= pdf.Base.Bits() {
... ...
@@ -240,7 +264,7 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
240 240
 		prevAlloc = allocated
241 241
 	}
242 242
 
243
-	if pdfID >= len(aSpace.predefined) {
243
+	if pdfID >= len(predefined) {
244 244
 		return netip.Prefix{}, ipamapi.ErrNoMoreSubnets
245 245
 	}
246 246
 
... ...
@@ -248,7 +272,7 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
248 248
 	// networks. Let's try two more times (once on the current 'pdf', and once
249 249
 	// on the next network if any).
250 250
 	if partialOverlap {
251
-		pdf := aSpace.predefined[pdfID]
251
+		pdf := predefined[pdfID]
252 252
 
253 253
 		if next := netiputil.PrefixAfter(prevAlloc, pdf.Size); pdf.Overlaps(next) {
254 254
 			return makeAlloc(next), nil
... ...
@@ -268,8 +292,8 @@ func (aSpace *addrSpace) allocatePredefinedPool(reserved []netip.Prefix) (netip.
268 268
 	//   overlapped at all.
269 269
 	//
270 270
 	// Hence, we're sure 'pdfID' has never been subnetted yet.
271
-	if pdfID < len(aSpace.predefined) {
272
-		pdf := aSpace.predefined[pdfID]
271
+	if pdfID < len(predefined) {
272
+		pdf := predefined[pdfID]
273 273
 
274 274
 		next := pdf.FirstPrefix()
275 275
 		return makeAlloc(next), nil
... ...
@@ -32,6 +32,7 @@ func TestDynamicPoolAllocation(t *testing.T) {
32 32
 	testcases := []struct {
33 33
 		name       string
34 34
 		predefined []*ipamutils.NetworkToSplit
35
+		subnetSize int
35 36
 		allocated  []netip.Prefix
36 37
 		reserved   []netip.Prefix
37 38
 		expPrefix  netip.Prefix
... ...
@@ -343,6 +344,92 @@ func TestDynamicPoolAllocation(t *testing.T) {
343 343
 			},
344 344
 			expPrefix: netip.MustParsePrefix("192.168.0.0/24"),
345 345
 		},
346
+		{
347
+			name: "Smaller requested network subnet size than predefined",
348
+			predefined: []*ipamutils.NetworkToSplit{
349
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16},
350
+			},
351
+			subnetSize: 24,
352
+			expPrefix:  netip.MustParsePrefix("172.17.0.0/24"),
353
+		},
354
+		{
355
+			name: "Larger requested network subnet size than predefined",
356
+			predefined: []*ipamutils.NetworkToSplit{
357
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 20},
358
+			},
359
+			subnetSize: 16,
360
+			expPrefix:  netip.MustParsePrefix("172.17.0.0/16"),
361
+		},
362
+		{
363
+			name: "Invalid specified network subnet size",
364
+			predefined: []*ipamutils.NetworkToSplit{
365
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16},
366
+			},
367
+			subnetSize: 150,
368
+			expErr:     ipamapi.ErrInvalidPool,
369
+		},
370
+		{
371
+			name: "Partially allocated predefined pool with specified size",
372
+			predefined: []*ipamutils.NetworkToSplit{
373
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16},
374
+			},
375
+			allocated: []netip.Prefix{
376
+				netip.MustParsePrefix("172.17.0.0/20"),
377
+			},
378
+			subnetSize: 24,
379
+			expPrefix:  netip.MustParsePrefix("172.17.16.0/24"),
380
+		},
381
+		{
382
+			name: "Partially allocated predefined with gap and specified size",
383
+			predefined: []*ipamutils.NetworkToSplit{
384
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16},
385
+			},
386
+			allocated: []netip.Prefix{
387
+				netip.MustParsePrefix("172.17.0.0/20"),
388
+				netip.MustParsePrefix("172.18.0.0/20"),
389
+			},
390
+			subnetSize: 24,
391
+			expPrefix:  netip.MustParsePrefix("172.17.16.0/24"),
392
+		},
393
+		{
394
+			name: "Partially allocated avoids overlapping",
395
+			predefined: []*ipamutils.NetworkToSplit{
396
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 16},
397
+			},
398
+			allocated: []netip.Prefix{
399
+				netip.MustParsePrefix("172.17.0.0/24"),
400
+			},
401
+			expPrefix: netip.MustParsePrefix("172.18.0.0/16"),
402
+		},
403
+		{
404
+			name: "Multiple predefined pools last one satisfies specified size",
405
+			predefined: []*ipamutils.NetworkToSplit{
406
+				{Base: netip.MustParsePrefix("172.17.0.0/12"), Size: 24},
407
+				{Base: netip.MustParsePrefix("10.0.0.0/12"), Size: 20},
408
+			},
409
+			subnetSize: 20,
410
+			expPrefix:  netip.MustParsePrefix("10.0.0.0/20"),
411
+		},
412
+		{
413
+			name: "Multiple predefined pools none matching specified size",
414
+			predefined: []*ipamutils.NetworkToSplit{
415
+				{Base: netip.MustParsePrefix("172.17.0.0/24"), Size: 24},
416
+				{Base: netip.MustParsePrefix("172.18.0.0/20"), Size: 20},
417
+				{Base: netip.MustParsePrefix("172.19.0.0/24"), Size: 24},
418
+			},
419
+			subnetSize: 16,
420
+			expErr:     ipamapi.ErrInvalidPool,
421
+		},
422
+		{
423
+			name: "Multiple predefined pools none valid for specified size",
424
+			predefined: []*ipamutils.NetworkToSplit{
425
+				{Base: netip.MustParsePrefix("172.17.0.0/24"), Size: 24},
426
+				{Base: netip.MustParsePrefix("172.18.0.0/20"), Size: 20},
427
+				{Base: netip.MustParsePrefix("172.19.0.0/24"), Size: 23},
428
+			},
429
+			subnetSize: 33,
430
+			expErr:     ipamapi.ErrInvalidPool,
431
+		},
346 432
 	}
347 433
 
348 434
 	for _, tc := range testcases {
... ...
@@ -351,7 +438,7 @@ func TestDynamicPoolAllocation(t *testing.T) {
351 351
 			assert.NilError(t, err)
352 352
 			as.allocated = tc.allocated
353 353
 
354
-			p, err := as.allocatePredefinedPool(tc.reserved)
354
+			p, err := as.allocatePredefinedPool(tc.reserved, tc.subnetSize)
355 355
 
356 356
 			assert.Check(t, is.ErrorIs(err, tc.expErr))
357 357
 			assert.Check(t, is.Equal(p, tc.expPrefix))
... ...
@@ -465,7 +552,7 @@ func TestPoolAllocateAndRelease(t *testing.T) {
465 465
 				// Allocate a pool for netname, check that a subnet is returned that
466 466
 				// isn't already allocated, and doesn't overlap with a reserved range.
467 467
 				alloc: func(netname string) {
468
-					subnet, err := as.allocatePredefinedPool(tc.reserved)
468
+					subnet, err := as.allocatePredefinedPool(tc.reserved, 0)
469 469
 					assert.NilError(t, err)
470 470
 
471 471
 					otherNetname, exists := subnetToNetname[subnet]
... ...
@@ -134,22 +134,42 @@ func (a *Allocator) RequestPool(req ipamapi.PoolRequest) (ipamapi.AllocatedPool,
134 134
 	if err != nil {
135 135
 		return ipamapi.AllocatedPool{}, err
136 136
 	}
137
+
138
+	k := PoolID{AddressSpace: req.AddressSpace}
139
+
140
+	prefixLength := 0
141
+
142
+	if req.Pool != "" {
143
+		prefix, err := netip.ParsePrefix(req.Pool)
144
+		if err != nil {
145
+			return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidPool)
146
+		}
147
+
148
+		if prefix.Addr().IsUnspecified() {
149
+			// If the prefix is unspecified, we're only interested in the prefix size.
150
+			// We'll attempt to use the specified size to allocate a subnet from the
151
+			// predefined pools.
152
+			req.Pool = ""
153
+
154
+			if prefix.Bits() > 0 {
155
+				prefixLength = prefix.Bits()
156
+			}
157
+		} else {
158
+			k.Subnet = prefix
159
+		}
160
+	}
161
+
137 162
 	if req.Pool == "" && req.SubPool != "" {
138 163
 		return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidSubPool)
139 164
 	}
140 165
 
141
-	k := PoolID{AddressSpace: req.AddressSpace}
142 166
 	if req.Pool == "" {
143
-		if k.Subnet, err = aSpace.allocatePredefinedPool(req.Exclude); err != nil {
167
+		if k.Subnet, err = aSpace.allocatePredefinedPool(req.Exclude, prefixLength); err != nil {
144 168
 			return ipamapi.AllocatedPool{}, err
145 169
 		}
146 170
 		return ipamapi.AllocatedPool{PoolID: k.String(), Pool: k.Subnet}, nil
147 171
 	}
148 172
 
149
-	if k.Subnet, err = netip.ParsePrefix(req.Pool); err != nil {
150
-		return ipamapi.AllocatedPool{}, parseErr(ipamapi.ErrInvalidPool)
151
-	}
152
-
153 173
 	if req.SubPool != "" {
154 174
 		if k.ChildSubnet, err = netip.ParsePrefix(req.SubPool); err != nil {
155 175
 			return ipamapi.AllocatedPool{}, types.InternalErrorf("invalid pool request: %v", ipamapi.ErrInvalidSubPool)
... ...
@@ -285,6 +285,61 @@ func TestPredefinedPool(t *testing.T) {
285 285
 	}
286 286
 }
287 287
 
288
+func TestPredefinedPoolWithPreferredSubnetSize(t *testing.T) {
289
+	a, err := NewAllocator(ipamutils.GetLocalScopeDefaultNetworks(), ipamutils.GetGlobalScopeDefaultNetworks())
290
+	assert.NilError(t, err)
291
+
292
+	alloc1, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/24"})
293
+	if err != nil {
294
+		t.Fatal(err)
295
+	}
296
+
297
+	alloc2, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace})
298
+	if err != nil {
299
+		t.Fatal(err)
300
+	}
301
+
302
+	if alloc1.Pool == alloc2.Pool {
303
+		t.Fatalf("Unexpected default network returned: %s = %s", alloc2.Pool, alloc1.Pool)
304
+	}
305
+
306
+	if alloc1.Pool.Bits() != 24 {
307
+		t.Fatalf("Unexpected default network size: %s != 24", alloc1.Pool)
308
+	}
309
+
310
+	if alloc2.Pool.Bits() == 24 {
311
+		t.Fatalf("Unexpected default network size: %s == 24", alloc2.Pool)
312
+	}
313
+
314
+	// Release the second pool first
315
+	if err := a.ReleasePool(alloc2.PoolID); err != nil {
316
+		t.Fatal(err)
317
+	}
318
+
319
+	_, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "/24"})
320
+	if err == nil {
321
+		t.Fatal(err, "Expected failure requesting pool with unspecified address family")
322
+	}
323
+
324
+	alloc4, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/25"})
325
+	if err != nil {
326
+		t.Fatal(err)
327
+	}
328
+
329
+	if alloc4.Pool.Bits() != 25 {
330
+		t.Fatalf("Unexpected default network size: %s != 25", alloc4.Pool)
331
+	}
332
+
333
+	if err := a.ReleasePool(alloc4.PoolID); err != nil {
334
+		t.Fatal(err)
335
+	}
336
+
337
+	// Check invalid subnet size requests
338
+	if _, err := a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: "0.0.0.0/AB"}); err == nil {
339
+		t.Fatalf("Expected failure requesting pool with invalid subnet size")
340
+	}
341
+}
342
+
288 343
 func TestRemoveSubnet(t *testing.T) {
289 344
 	a, err := NewAllocator(ipamutils.GetLocalScopeDefaultNetworks(), ipamutils.GetGlobalScopeDefaultNetworks())
290 345
 	assert.NilError(t, err)
... ...
@@ -755,14 +810,15 @@ func TestOverlappingRequests(t *testing.T) {
755 755
 		{[]string{"10.0.0.0/8"}, "11.0.0.0/8", true},
756 756
 		{[]string{"74.0.0.0/7"}, "9.111.99.72/30", true},
757 757
 		{[]string{"110.192.0.0/10"}, "16.0.0.0/10", true},
758
+		{[]string{"0.0.0.0/16"}, "0.0.0.0/16", true}, // two default allocations should succeed
758 759
 
759 760
 		// Previously allocated network entirely contains request
760 761
 		{[]string{"10.0.0.0/8"}, "10.0.0.0/8", false}, // exact overlap
761
-		{[]string{"0.0.0.0/1"}, "16.182.0.0/15", false},
762
+		{[]string{"16.182.0.0/15"}, "16.182.0.0/16", false},
762 763
 		{[]string{"16.0.0.0/4"}, "17.11.66.0/23", false},
763 764
 
764 765
 		// Previously allocated network overlaps beginning of request
765
-		{[]string{"0.0.0.0/1"}, "0.0.0.0/0", false},
766
+		{[]string{"16.182.0.0/16"}, "16.182.0.0/15", false},
766 767
 		{[]string{"64.0.0.0/6"}, "64.0.0.0/3", false},
767 768
 		{[]string{"112.0.0.0/6"}, "112.0.0.0/4", false},
768 769
 
... ...
@@ -774,18 +830,21 @@ func TestOverlappingRequests(t *testing.T) {
774 774
 		// Previously allocated network entirely contained within request
775 775
 		{[]string{"10.0.0.0/8"}, "10.0.0.0/6", false}, // non-canonical
776 776
 		{[]string{"10.0.0.0/8"}, "8.0.0.0/6", false},  // canonical
777
-		{[]string{"25.173.144.0/20"}, "0.0.0.0/0", false},
777
+		{[]string{"25.173.144.0/20"}, "25.173.143.0/16", false},
778 778
 
779 779
 		// IPv6
780
+		{[]string{"::/0"}, "::/0", true},     // two default allocations should succeed
781
+		{[]string{"f000::/4"}, "::/0", true}, // default allocation shouldn't overlap explicit allocation
782
+
780 783
 		// Previously allocated network entirely contains request
781
-		{[]string{"::/0"}, "f656:3484:c878:a05:e540:a6ed:4d70:3740/123", false},
784
+		{[]string{"f656::/0"}, "f656:3484:c878:a05:e540:a6ed:4d70:3740/123", false},
782 785
 		{[]string{"8000::/1"}, "8fe8:e7c4:5779::/49", false},
783 786
 		{[]string{"f000::/4"}, "ffc7:6000::/19", false},
784 787
 
785 788
 		// Previously allocated network overlaps beginning of request
786
-		{[]string{"::/2"}, "::/0", false},
787
-		{[]string{"::/3"}, "::/1", false},
788
-		{[]string{"::/6"}, "::/5", false},
789
+		{[]string{"f656::/20"}, "f656::/16", false},
790
+		{[]string{"8000::/32"}, "8000::/31", false},
791
+		{[]string{"f000::/60"}, "f000::/20", false},
789 792
 
790 793
 		// Previously allocated network overlaps end of request
791 794
 		{[]string{"c000::/2"}, "8000::/1", false},
... ...
@@ -793,7 +852,7 @@ func TestOverlappingRequests(t *testing.T) {
793 793
 		{[]string{"cf80::/9"}, "c000::/4", false},
794 794
 
795 795
 		// Previously allocated network entirely contained within request
796
-		{[]string{"ff77:93f8::/29"}, "::/0", false},
796
+		{[]string{"ff77:93f8::/29"}, "ff77:93f7::/28", false},
797 797
 		{[]string{"9287:2e20:5134:fab6:9061:a0c6:bfe3:9400/119"}, "8000::/1", false},
798 798
 		{[]string{"3ea1:bfa9:8691:d1c6:8c46:519b:db6d:e700/120"}, "3000::/4", false},
799 799
 	}
... ...
@@ -805,15 +864,15 @@ func TestOverlappingRequests(t *testing.T) {
805 805
 		// Set up some existing allocations.  This should always succeed.
806 806
 		for _, env := range tc.environment {
807 807
 			_, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: env})
808
-			assert.NilError(t, err)
808
+			assert.NilError(t, err, "error requesting pool %v, %v", localAddressSpace, env)
809 809
 		}
810 810
 
811 811
 		// Make the test allocation.
812 812
 		_, err = a.RequestPool(ipamapi.PoolRequest{AddressSpace: localAddressSpace, Pool: tc.subnet})
813 813
 		if tc.ok {
814
-			assert.NilError(t, err)
814
+			assert.NilError(t, err, "error requesting pool %v, %v", localAddressSpace, tc.subnet)
815 815
 		} else {
816
-			assert.Check(t, is.ErrorContains(err, ""))
816
+			assert.Check(t, is.ErrorContains(err, ""), "expected error requesting overlapping pool %v, %v", localAddressSpace, tc.subnet)
817 817
 		}
818 818
 	}
819 819
 }
... ...
@@ -1533,10 +1533,21 @@ func (n *Network) ipamAllocateVersion(ipam ipamapi.Ipam, v6 bool, ipamConf []*Ip
1533 1533
 			reserved = netutils.InferReservedNetworks(v6)
1534 1534
 		}
1535 1535
 
1536
+		// Determine if the preferred pool is unspecified (blank, or a 0.0.0.0 or :: address)
1537
+		prefPool := cfg.PreferredPool
1538
+		isDefaultPool := prefPool == ""
1539
+		if !isDefaultPool {
1540
+			if prefix, err := netip.ParsePrefix(prefPool); err != nil {
1541
+				// This should never happen
1542
+				return nil, types.InvalidParameterErrorf("invalid preferred address %q: %v", prefPool, err)
1543
+			} else if prefix.Addr().IsUnspecified() {
1544
+				isDefaultPool = true
1545
+			}
1546
+		}
1547
+
1536 1548
 		// During network restore, if no subnet was specified in the original network-create
1537 1549
 		// request, use the previously allocated subnet.
1538
-		prefPool := cfg.PreferredPool
1539
-		if prefPool == "" && len(ipamInfo) > i {
1550
+		if isDefaultPool && len(ipamInfo) > i {
1540 1551
 			prefPool = ipamInfo[i].Pool.String()
1541 1552
 		}
1542 1553
 
... ...
@@ -812,27 +812,49 @@ func buildIPAMResources(nw *libnetwork.Network) networktypes.IPAM {
812 812
 	var ipamConfig []networktypes.IPAMConfig
813 813
 
814 814
 	ipamDriver, ipamOptions, ipv4Conf, ipv6Conf := nw.IpamConfig()
815
+	ipv4Info, ipv6Info := nw.IpamInfo()
815 816
 
816 817
 	hasIPv4Config := false
817
-	for _, cfg := range ipv4Conf {
818
-		if cfg.PreferredPool == "" {
819
-			continue
818
+	if len(ipv4Info) > 0 {
819
+		// Only check ipv4 networks if there were any allocated
820
+		for i, cfg := range ipv4Conf {
821
+			if cfg.PreferredPool == "" {
822
+				continue
823
+			}
824
+			hasIPv4Config = true
825
+			subnet := ipv4Info[i].IPAMData.Pool
826
+			if subnet != nil {
827
+				cfg.PreferredPool = subnet.String()
828
+			}
829
+			if ipv4Info[i].IPAMData.Gateway != nil && cfg.Gateway == "" {
830
+				cfg.Gateway = ipv4Info[i].IPAMData.Gateway.IP.String()
831
+			}
832
+
833
+			ipamConfig = append(ipamConfig, cfg.IPAMConfig())
820 834
 		}
821
-		hasIPv4Config = true
822
-		ipamConfig = append(ipamConfig, cfg.IPAMConfig())
823 835
 	}
824 836
 
825 837
 	hasIPv6Config := false
826
-	for _, cfg := range ipv6Conf {
827
-		if cfg.PreferredPool == "" {
828
-			continue
838
+	if len(ipv6Info) > 0 {
839
+		// Only check ipv6 networks if there were any allocated
840
+		for i, cfg := range ipv6Conf {
841
+			if cfg.PreferredPool == "" {
842
+				continue
843
+			}
844
+			hasIPv6Config = true
845
+			subnet := ipv6Info[i].IPAMData.Pool
846
+			if subnet != nil {
847
+				cfg.PreferredPool = subnet.String()
848
+			}
849
+
850
+			if ipv6Info[i].IPAMData.Gateway != nil && cfg.Gateway == "" {
851
+				cfg.Gateway = ipv6Info[i].IPAMData.Gateway.IP.String()
852
+			}
853
+			ipamConfig = append(ipamConfig, cfg.IPAMConfig())
829 854
 		}
830
-		hasIPv6Config = true
831
-		ipamConfig = append(ipamConfig, cfg.IPAMConfig())
832 855
 	}
833 856
 
834 857
 	if !hasIPv4Config || !hasIPv6Config {
835
-		ipv4Info, ipv6Info := nw.IpamInfo()
836 858
 		if !hasIPv4Config {
837 859
 			for _, info := range ipv4Info {
838 860
 				ipamConfig = append(ipamConfig, info.IPAMData.IPAMConfig())
... ...
@@ -70,8 +70,8 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
70 70
 				"--fixed-cidr-v6", "fdd1:8161:2d2c::/64",
71 71
 			},
72 72
 			expIPAMConfig: []network.IPAMConfig{
73
-				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")},
74
-				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64")},
73
+				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
74
+				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")},
75 75
 			},
76 76
 		},
77 77
 		{
... ...
@@ -123,7 +123,7 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
123 123
 				"--fixed-cidr-v6", "fe80::/64",
124 124
 			},
125 125
 			expIPAMConfig: []network.IPAMConfig{
126
-				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")},
126
+				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
127 127
 				{Subnet: netip.MustParsePrefix("fe80::/64"), IPRange: netip.MustParsePrefix("fe80::/64"), Gateway: llGwPlaceholder},
128 128
 			},
129 129
 		},
... ...
@@ -173,15 +173,8 @@ func TestDaemonDefaultBridgeIPAM_Docker0(t *testing.T) {
173 173
 			// The bridge's address/subnet should be ignored, this is a change
174 174
 			// of fixed-cidr.
175 175
 			expIPAMConfig: []network.IPAMConfig{
176
-				{Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24")},
177
-				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64")},
178
-				// No Gateway is configured, because the address could not be learnt from the
179
-				// bridge. An address will have been allocated but, because there's config (the
180
-				// fixed-cidr), inspect shows just the config. (Surprisingly, when there's no
181
-				// config at all, the inspect output still says its showing config but actually
182
-				// shows the running state.) When the daemon is restarted, after a gateway
183
-				// address has been assigned to the bridge, that address will become config - so
184
-				// a Gateway address will show up in the inspect output.
176
+				{Subnet: netip.MustParsePrefix("192.168.177.0/24"), IPRange: netip.MustParsePrefix("192.168.177.0/24"), Gateway: netip.MustParseAddr("192.168.177.1")},
177
+				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c:1::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c:1::1")},
185 178
 			},
186 179
 		},
187 180
 		{
... ...
@@ -225,8 +218,8 @@ func TestDaemonDefaultBridgeIPAM_UserBr(t *testing.T) {
225 225
 				"--fixed-cidr-v6", "fdd1:8161:2d2c::/64",
226 226
 			},
227 227
 			expIPAMConfig: []network.IPAMConfig{
228
-				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24")},
229
-				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64")},
228
+				{Subnet: netip.MustParsePrefix("192.168.176.0/24"), IPRange: netip.MustParsePrefix("192.168.176.0/24"), Gateway: netip.MustParseAddr("192.168.176.1")},
229
+				{Subnet: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), IPRange: netip.MustParsePrefix("fdd1:8161:2d2c::/64"), Gateway: netip.MustParseAddr("fdd1:8161:2d2c::1")},
230 230
 			},
231 231
 		},
232 232
 		{
... ...
@@ -428,7 +421,7 @@ func testDefaultBridgeIPAM(ctx context.Context, t *testing.T, tc defaultBridgeIP
428 428
 					expIPAMConfig[i].Gateway = llAddr
429 429
 				}
430 430
 			}
431
-			assert.Check(t, is.DeepEqual(res.Network.IPAM.Config, expIPAMConfig, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})))
431
+			assert.Check(t, is.DeepEqual(res.Network.IPAM.Config, expIPAMConfig, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})), "unexpected IPAM config '%s'", tc.name)
432 432
 		})
433 433
 	})
434 434
 }
... ...
@@ -33,6 +33,21 @@ func CreateNoError(ctx context.Context, t *testing.T, apiClient client.APIClient
33 33
 	return name
34 34
 }
35 35
 
36
+// Inspect inspects a network with the specified options
37
+func Inspect(ctx context.Context, apiClient client.APIClient, name string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) {
38
+	return apiClient.NetworkInspect(ctx, name, options)
39
+}
40
+
41
+// InspectNoError inspects a network with the specified options and verifies there were no errors
42
+func InspectNoError(ctx context.Context, t *testing.T, apiClient client.APIClient, name string, options client.NetworkInspectOptions) client.NetworkInspectResult {
43
+	t.Helper()
44
+
45
+	c, err := apiClient.NetworkInspect(ctx, name, options)
46
+	assert.NilError(t, err)
47
+
48
+	return c
49
+}
50
+
36 51
 func RemoveNoError(ctx context.Context, t *testing.T, apiClient client.APIClient, name string) {
37 52
 	t.Helper()
38 53
 
... ...
@@ -1287,3 +1287,95 @@ func TestJoinError(t *testing.T) {
1287 1287
 	res = ctr.ExecT(ctx, t, c, cid, []string{"ip", "link", "show", "eth1"})
1288 1288
 	assert.Check(t, is.Equal(res.ExitCode, 0), "container should have an eth1")
1289 1289
 }
1290
+
1291
+func TestPreferredSubnetRestore(t *testing.T) {
1292
+	skip.If(t, testEnv.IsRootless(), "fails before and after restart")
1293
+
1294
+	ctx := setupTest(t)
1295
+	d := daemon.New(t)
1296
+	d.StartWithBusybox(ctx, t)
1297
+	defer d.Stop(t)
1298
+	c := d.NewClientT(t)
1299
+
1300
+	const v4netName = "testnetv4restore"
1301
+	network.CreateNoError(ctx, t, c, v4netName,
1302
+		network.WithIPv4(true),
1303
+		network.WithIPAMConfig(networktypes.IPAMConfig{
1304
+			Subnet: netip.MustParsePrefix("0.0.0.0/24"),
1305
+		}),
1306
+	)
1307
+
1308
+	defer func() { network.RemoveNoError(ctx, t, c, v4netName) }()
1309
+
1310
+	const v6netName = "testnetv6restore"
1311
+	network.CreateNoError(ctx, t, c, v6netName,
1312
+		network.WithIPv4(false),
1313
+		network.WithIPv6(),
1314
+		network.WithIPAMConfig(networktypes.IPAMConfig{
1315
+			Subnet: netip.MustParsePrefix("::/120"),
1316
+		}),
1317
+	)
1318
+
1319
+	defer func() { network.RemoveNoError(ctx, t, c, v6netName) }()
1320
+
1321
+	const dualStackNetName = "testnetdualrestore"
1322
+	network.CreateNoError(ctx, t, c, dualStackNetName,
1323
+		network.WithIPv4(true),
1324
+		network.WithIPv6(),
1325
+		network.WithIPAMConfig(networktypes.IPAMConfig{
1326
+			Subnet: netip.MustParsePrefix("0.0.0.0/24"),
1327
+		}, networktypes.IPAMConfig{
1328
+			Subnet: netip.MustParsePrefix("::/120"),
1329
+		}),
1330
+	)
1331
+
1332
+	defer func() { network.RemoveNoError(ctx, t, c, dualStackNetName) }()
1333
+
1334
+	inspOpts := client.NetworkInspectOptions{}
1335
+
1336
+	v4Insp := network.InspectNoError(ctx, t, c, v4netName, inspOpts)
1337
+	assert.Check(t, is.Len(v4Insp.Network.IPAM.Config, 1))
1338
+	v4allocCidr := v4Insp.Network.IPAM.Config[0].Subnet
1339
+	assert.Check(t, is.Equal(v4allocCidr.Addr().IsUnspecified(), false), "expected specific subnet")
1340
+
1341
+	v6Insp := network.InspectNoError(ctx, t, c, v6netName, inspOpts)
1342
+	assert.Check(t, is.Len(v6Insp.Network.IPAM.Config, 1))
1343
+	v6allocCidr := v6Insp.Network.IPAM.Config[0].Subnet
1344
+	assert.Check(t, is.Equal(v6allocCidr.Addr().IsUnspecified(), false), "expected specific subnet")
1345
+
1346
+	dualStackInsp := network.InspectNoError(ctx, t, c, dualStackNetName, inspOpts)
1347
+	assert.Check(t, is.Len(dualStackInsp.Network.IPAM.Config, 2))
1348
+	var dualv4, dualv6 netip.Prefix
1349
+	if dualStackInsp.Network.IPAM.Config[0].Subnet.Addr().Is4() {
1350
+		dualv4 = dualStackInsp.Network.IPAM.Config[0].Subnet
1351
+		dualv6 = dualStackInsp.Network.IPAM.Config[1].Subnet
1352
+	} else {
1353
+		dualv4 = dualStackInsp.Network.IPAM.Config[1].Subnet
1354
+		dualv6 = dualStackInsp.Network.IPAM.Config[0].Subnet
1355
+	}
1356
+	assert.Check(t, is.Equal(dualv4.Addr().IsUnspecified(), false), "expected specific v4 subnet")
1357
+	assert.Check(t, is.Equal(dualv6.Addr().IsUnspecified(), false), "expected specific v6 subnet")
1358
+
1359
+	d.Restart(t)
1360
+
1361
+	v4Insp = network.InspectNoError(ctx, t, c, v4netName, inspOpts)
1362
+	assert.Check(t, is.Len(v4Insp.Network.IPAM.Config, 1))
1363
+	assert.Check(t, is.Equal(v4Insp.Network.IPAM.Config[0].Subnet, v4allocCidr))
1364
+
1365
+	v6Insp = network.InspectNoError(ctx, t, c, v6netName, inspOpts)
1366
+	assert.Check(t, is.Len(v6Insp.Network.IPAM.Config, 1))
1367
+	assert.Check(t, is.Equal(v6Insp.Network.IPAM.Config[0].Subnet, v6allocCidr))
1368
+
1369
+	dualStackInsp = network.InspectNoError(ctx, t, c, dualStackNetName, inspOpts)
1370
+	assert.Check(t, is.Len(dualStackInsp.Network.IPAM.Config, 2))
1371
+	var dualv4after, dualv6after netip.Prefix
1372
+	if dualStackInsp.Network.IPAM.Config[0].Subnet.Addr().Is4() {
1373
+		dualv4after = dualStackInsp.Network.IPAM.Config[0].Subnet
1374
+		dualv6after = dualStackInsp.Network.IPAM.Config[1].Subnet
1375
+	} else {
1376
+		dualv4after = dualStackInsp.Network.IPAM.Config[1].Subnet
1377
+		dualv6after = dualStackInsp.Network.IPAM.Config[0].Subnet
1378
+	}
1379
+	assert.Check(t, is.Equal(dualv4after, dualv4), "expected same v4 subnet after restart")
1380
+	assert.Check(t, is.Equal(dualv6after, dualv6), "expected same v6 subnet after restart")
1381
+}