Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions cloud/nodeipam/ipam/cloud_allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ type cloudAllocator struct {
disableIPv6NodeCIDRAllocation bool
}

const providerIDPrefix = "linode://"
const (
providerIDPrefix = "linode://"
ipv6BitLen = 128
ipv6PodCIDRMaskSize = 112
)

var _ CIDRAllocator = &cloudAllocator{}

Expand Down Expand Up @@ -347,12 +351,49 @@ func getIPv6RangeFromLinodeInterface(iface linodego.LinodeInterface) string {
return ""
}

// getIPv6PodCIDR attempts to compute a stable IPv6 /112 PodCIDR
// within the provided base IPv6 range using the mnemonic subprefix :0:c::/112.
//
// The mnemonic subprefix :0:c::/112 is constructed by setting hextets 5..7 to 0, c, 0
// (i.e., bytes 8-13 set to 00 00 00 0c 00 00) within the /64 base, as implemented below.
//
// Rules:
// - For a /64 base, return {base64}:0:c::/112
// - Only applies when desiredMask is /112 and the result is fully contained
// in the base range. Otherwise, returns (nil, false) to signal fallback.
func getIPv6PodCIDR(ip net.IP, desiredMask int) (*net.IPNet, bool) {
// Some validation checks
if ip == nil || desiredMask != ipv6PodCIDRMaskSize {
return nil, false
}

// We need to make a copy so we don't mutate caller's backing array (net.IP is a slice)
ipCopy := make(net.IP, len(ip))
copy(ipCopy, ip)

// Keep first 64 bits (bytes 0..7) and set hextets 5..7 to 0, c, 0 respectively
// Hextet index to byte mapping: h5->[8,9], h6->[10,11], h7->[12,13]
ipCopy[8], ipCopy[9] = 0x00, 0x00 // :0
ipCopy[10], ipCopy[11] = 0x00, 0x0c // :c
ipCopy[12], ipCopy[13] = 0x00, 0x00 // :0
// last hextet (bytes 14..15) will be zeroed by mask below

podMask := net.CIDRMask(desiredMask, ipv6BitLen)
// Ensure the address is the network address for the desired mask
ipCopy = ipCopy.Mask(podMask)
podCIDR := &net.IPNet{IP: ipCopy, Mask: podMask}

return podCIDR, true
}

// allocateIPv6CIDR allocates an IPv6 CIDR for the given node.
// It retrieves the instance configuration for the node and extracts the IPv6 range.
// It then creates a new net.IPNet with the IPv6 address and mask size defined
// by nodeCIDRMaskSizeIPv6. The function returns an error if it fails to retrieve
// the instance configuration or parse the IPv6 range.
func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*net.IPNet, error) {
logger := klog.FromContext(ctx)

if node.Spec.ProviderID == "" {
return nil, fmt.Errorf("node %s has no ProviderID set, cannot calculate ipv6 range for it", node.Name)
}
Expand Down Expand Up @@ -410,18 +451,26 @@ func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*
}
}

ip, _, err := net.ParseCIDR(ipv6Range)
ip, base, err := net.ParseCIDR(ipv6Range)
if err != nil {
return nil, fmt.Errorf("failed parsing ipv6 range %s: %w", ipv6Range, err)
}

mask := net.CIDRMask(c.nodeCIDRMaskSizeIPv6, 128)
ipv6Embedded := &net.IPNet{
IP: ip.Mask(mask),
Mask: mask,
// get pod cidr using stable mnemonic subprefix :0:c::/112
if podCIDR, ok := getIPv6PodCIDR(ip, c.nodeCIDRMaskSizeIPv6); ok {
logger.V(4).Info("Using stable IPv6 PodCIDR subprefix :0:c::/112", "ip", ip, "podCIDR", podCIDR)
// Verify the /112 PodCIDR is fully contained within the base /64 range
if !base.Contains(podCIDR.IP) {
return nil, fmt.Errorf("stable IPv6 PodCIDR %s is not contained in base range %s", podCIDR, base)
}
return podCIDR, nil
}

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

// WARNING: If you're adding any return calls or defer any more work from this
Expand Down
62 changes: 54 additions & 8 deletions cloud/nodeipam/ipam/cloud_allocator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,52 @@ type testCase struct {
instance *linodego.Instance
}

func TestComputeStableIPv6PodCIDR(t *testing.T) {
for _, tc := range []struct {
name string
baseCIDR string
desiredMask int
wantCIDR string
wantOK bool
}{
{name: "nil base", baseCIDR: "", desiredMask: 112, wantOK: false},
{name: "non-/112 desired", baseCIDR: "2300:5800:2:1::/64", desiredMask: 120, wantOK: false},
{name: "success /64 -> mnemonic /112", baseCIDR: "2300:5800:2:1::/64", desiredMask: 112, wantCIDR: "2300:5800:2:1:0:c::/112", wantOK: true},
} {
t.Run(tc.name, func(t *testing.T) {
var baseIP net.IP
if tc.baseCIDR != "" {
ip, _, err := net.ParseCIDR(tc.baseCIDR)
if err != nil {
t.Fatalf("parse base cidr: %v", err)
}
baseIP = ip
}
got, ok := getIPv6PodCIDR(baseIP, tc.desiredMask)
if ok != tc.wantOK {
t.Fatalf("ok mismatch: got %v want %v (gotCIDR=%v)", ok, tc.wantOK, func() string {
if got != nil {
return got.String()
}
return ""
}())
}
if !tc.wantOK {
if got != nil {
t.Fatalf("expected nil cidr on failure, got %v", got.String())
}
return
}
if got == nil {
t.Fatalf("expected non-nil cidr")
}
if got.String() != tc.wantCIDR {
t.Fatalf("cidr mismatch: got %s want %s", got.String(), tc.wantCIDR)
}
})
}
}

func TestGetIPv6RangeFromLinodeInterface(t *testing.T) {
for _, tc := range []struct {
iface linodego.LinodeInterface
Expand Down Expand Up @@ -249,7 +295,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.0/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -292,7 +338,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
// it should return first /30 CIDR after service range
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.64/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -337,7 +383,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.76/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -410,7 +456,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "10.10.1.0/24",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -449,7 +495,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.0/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -492,7 +538,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
// it should return first /30 CIDR after service range
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.64/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -537,7 +583,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "127.123.234.76/30",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
{
Expand Down Expand Up @@ -610,7 +656,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) {
},
expectedAllocatedCIDR: map[int]string{
0: "10.10.1.0/24",
1: "2300:5800:2:1::/112",
1: "2300:5800:2:1:0:c::/112",
},
},
}
Expand Down
4 changes: 2 additions & 2 deletions docs/configuration/nodeipam.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Note:
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.

## Allocated subnet size
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.
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.

```yaml
spec:
Expand All @@ -33,7 +33,7 @@ spec:
- --allocate-node-cidrs=true
- --cluster-cidr=10.192.0.0/10
- --node-cidr-mask-size-ipv4=25
- --node-cidr-mask-size-ipv6=64
- --node-cidr-mask-size-ipv6=112
```

## Disabling ipv6 ipam allocation
Expand Down
Loading