Skip to content

Commit 9351cc7

Browse files
committed
[improvement][feat] Deterministic IPv6 /112 PodCIDR via :0:c::/112; update tests/docs
- Add computeStableIPv6PodCIDR() to derive a stable /112 from a node’s /64 using mnemonic subprefix :0:c::/112. - allocateIPv6CIDR(): prefer stable subprefix when mask=112 and base is /64; otherwise fall back to start-of-range masking. - Update tests in cloud/nodeipam/ipam/cloud_allocator_test.go to expect 2300:5800:2:1:0:c::/112. - Document behavior and correct example flag to --node-cidr-mask-size-ipv6=112 in docs/configuration/nodeipam.md. - Add/adjust debug logs for clarity; no change to IPv4 behavior.
1 parent df34350 commit 9351cc7

File tree

3 files changed

+78
-16
lines changed

3 files changed

+78
-16
lines changed

cloud/nodeipam/ipam/cloud_allocator.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -347,12 +347,70 @@ func getIPv6RangeFromLinodeInterface(iface linodego.LinodeInterface) string {
347347
return ""
348348
}
349349

350+
// computeStableIPv6PodCIDR attempts to compute a stable IPv6 /112 PodCIDR
351+
// within the provided base IPv6 range using the mnemonic subprefix :0:c::/112.
352+
//
353+
// This prefix is degiefined here:
354+
//
355+
// Rules:
356+
// - For a /64 base, return {base64}:0:c::/112
357+
// - Only applies when desiredMask is /112 and the result is fully contained
358+
// in the base range. Otherwise, returns (nil, false) to signal fallback.
359+
func computeStableIPv6PodCIDR(base *net.IPNet, desiredMask int) (*net.IPNet, bool) {
360+
if base == nil || desiredMask != 112 {
361+
return nil, false
362+
}
363+
364+
ones, bits := base.Mask.Size()
365+
if bits != 128 {
366+
return nil, false
367+
}
368+
369+
ip := base.IP.To16()
370+
if ip == nil {
371+
return nil, false
372+
}
373+
374+
// Safety: only handle /64 base ranges
375+
if ones != 64 {
376+
return nil, false
377+
}
378+
379+
// Start from the network address of the base range
380+
baseIP := ip.Mask(base.Mask)
381+
out := make(net.IP, len(baseIP))
382+
copy(out, baseIP)
383+
384+
// Keep first 64 bits (bytes 0..7) and set hextets 5..7 to 0, c, 0 respectively
385+
// Hextet index to byte mapping: h5->[8,9], h6->[10,11], h7->[12,13]
386+
out[8], out[9] = 0x00, 0x00 // :0
387+
out[10], out[11] = 0x00, 0x0c // :c
388+
out[12], out[13] = 0x00, 0x00 // :0
389+
// last hextet (bytes 14..15) will be zeroed by mask below
390+
391+
mask := net.CIDRMask(desiredMask, 128)
392+
// Ensure the address is the network address for the desired mask
393+
out = out.Mask(mask)
394+
pod := &net.IPNet{IP: out, Mask: mask}
395+
396+
// Containment check: both start and end of the /112 must be inside base
397+
end := make(net.IP, len(out))
398+
copy(end, out)
399+
end[14], end[15] = 0xff, 0xff
400+
if !base.Contains(out) || !base.Contains(end) {
401+
return nil, false
402+
}
403+
return pod, true
404+
}
405+
350406
// allocateIPv6CIDR allocates an IPv6 CIDR for the given node.
351407
// It retrieves the instance configuration for the node and extracts the IPv6 range.
352408
// It then creates a new net.IPNet with the IPv6 address and mask size defined
353409
// by nodeCIDRMaskSizeIPv6. The function returns an error if it fails to retrieve
354410
// the instance configuration or parse the IPv6 range.
355411
func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*net.IPNet, error) {
412+
logger := klog.FromContext(ctx)
413+
356414
if node.Spec.ProviderID == "" {
357415
return nil, fmt.Errorf("node %s has no ProviderID set, cannot calculate ipv6 range for it", node.Name)
358416
}
@@ -410,18 +468,22 @@ func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*
410468
}
411469
}
412470

413-
ip, _, err := net.ParseCIDR(ipv6Range)
471+
ip, base, err := net.ParseCIDR(ipv6Range)
414472
if err != nil {
415473
return nil, fmt.Errorf("failed parsing ipv6 range %s: %w", ipv6Range, err)
416474
}
417475

418-
mask := net.CIDRMask(c.nodeCIDRMaskSizeIPv6, 128)
419-
ipv6Embedded := &net.IPNet{
420-
IP: ip.Mask(mask),
421-
Mask: mask,
476+
// Try stable subprefix selection first.
477+
if podCIDR, ok := computeStableIPv6PodCIDR(base, c.nodeCIDRMaskSizeIPv6); ok {
478+
logger.V(4).Info("Using stable IPv6 PodCIDR subprefix :0:c::/112", "base", base, "podCIDR", podCIDR)
479+
return podCIDR, nil
422480
}
423481

424-
return ipv6Embedded, nil
482+
// Fallback to the original behavior: mask base IP directly to desired size
483+
podMask := net.CIDRMask(c.nodeCIDRMaskSizeIPv6, 128)
484+
fallbackPodCIDR := &net.IPNet{IP: ip.Mask(podMask), Mask: podMask}
485+
logger.V(4).Info("Falling back to start-of-range IPv6 PodCIDR", "base", base, "podCIDR", fallbackPodCIDR)
486+
return fallbackPodCIDR, nil
425487
}
426488

427489
// WARNING: If you're adding any return calls or defer any more work from this

cloud/nodeipam/ipam/cloud_allocator_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
249249
},
250250
expectedAllocatedCIDR: map[int]string{
251251
0: "127.123.234.0/30",
252-
1: "2300:5800:2:1::/112",
252+
1: "2300:5800:2:1:0:c::/112",
253253
},
254254
},
255255
{
@@ -292,7 +292,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
292292
// it should return first /30 CIDR after service range
293293
expectedAllocatedCIDR: map[int]string{
294294
0: "127.123.234.64/30",
295-
1: "2300:5800:2:1::/112",
295+
1: "2300:5800:2:1:0:c::/112",
296296
},
297297
},
298298
{
@@ -337,7 +337,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
337337
},
338338
expectedAllocatedCIDR: map[int]string{
339339
0: "127.123.234.76/30",
340-
1: "2300:5800:2:1::/112",
340+
1: "2300:5800:2:1:0:c::/112",
341341
},
342342
},
343343
{
@@ -410,7 +410,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
410410
},
411411
expectedAllocatedCIDR: map[int]string{
412412
0: "10.10.1.0/24",
413-
1: "2300:5800:2:1::/112",
413+
1: "2300:5800:2:1:0:c::/112",
414414
},
415415
},
416416
{
@@ -449,7 +449,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
449449
},
450450
expectedAllocatedCIDR: map[int]string{
451451
0: "127.123.234.0/30",
452-
1: "2300:5800:2:1::/112",
452+
1: "2300:5800:2:1:0:c::/112",
453453
},
454454
},
455455
{
@@ -492,7 +492,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
492492
// it should return first /30 CIDR after service range
493493
expectedAllocatedCIDR: map[int]string{
494494
0: "127.123.234.64/30",
495-
1: "2300:5800:2:1::/112",
495+
1: "2300:5800:2:1:0:c::/112",
496496
},
497497
},
498498
{
@@ -537,7 +537,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
537537
},
538538
expectedAllocatedCIDR: map[int]string{
539539
0: "127.123.234.76/30",
540-
1: "2300:5800:2:1::/112",
540+
1: "2300:5800:2:1:0:c::/112",
541541
},
542542
},
543543
{
@@ -610,7 +610,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
610610
},
611611
expectedAllocatedCIDR: map[int]string{
612612
0: "10.10.1.0/24",
613-
1: "2300:5800:2:1::/112",
613+
1: "2300:5800:2:1:0:c::/112",
614614
},
615615
},
616616
}

docs/configuration/nodeipam.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Note:
2121
Make sure node IPAM allocation is disabled in kube-controller-manager to avoid both controllers competing to assign CIDRs to nodes. To make sure its disabled, check and make sure kube-controller-manager is not started with `--allocate-node-cidrs` flag.
2222

2323
## Allocated subnet size
24-
By default, CCM allocates /24 subnet for IPv4 addresses and /112 for IPv6 addresses to nodes. For IPv6 CIDR allocation using CCM, linodes should have IPv6 ranges configured on their interfaces. If one wants different subnet range, it can be configured by using `--node-cidr-mask-size-ipv4` and `--node-cidr-mask-size-ipv6` flags.
24+
By default, CCM allocates /24 subnet for IPv4 addresses and /112 for IPv6 addresses to nodes. For IPv6, CCM derives /112 PodCIDRs from the node's /64 IPv6 range using the stable mnemonic subprefix `:0:c::/112`. For IPv6 CIDR allocation using CCM, Linodes should have IPv6 ranges configured on their interfaces. If one wants different subnet range, it can be configured by using `--node-cidr-mask-size-ipv4` and `--node-cidr-mask-size-ipv6` flags.
2525

2626
```yaml
2727
spec:
@@ -33,7 +33,7 @@ spec:
3333
- --allocate-node-cidrs=true
3434
- --cluster-cidr=10.192.0.0/10
3535
- --node-cidr-mask-size-ipv4=25
36-
- --node-cidr-mask-size-ipv6=64
36+
- --node-cidr-mask-size-ipv6=112
3737
```
3838

3939
## Disabling ipv6 ipam allocation

0 commit comments

Comments
 (0)