Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for /32 (/128) networks and perNodeBlockSize 1 #57

Merged
merged 3 commits into from
Oct 15, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ spec:
> __Notes:__
>
> * pool name is composed of alphanumeric letters separated by dots(`.`) underscores(`_`) or hyphens(`-`).
> * `perNodeBlockSize` minimum size is 2.
> * `perNodeBlockSize` minimum size is 1.
adrianchiris marked this conversation as resolved.
Show resolved Hide resolved
> * `subnet` must be large enough to accommodate at least one `perNodeBlockSize` block of IPs.


Expand Down
8 changes: 4 additions & 4 deletions api/v1alpha1/cidrpool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ var _ = Describe("CIDRPool", func() {
},
Entry("empty", "", int32(30), false),
Entry("invalid value", "aaaa", int32(30), false),
Entry("/32", "192.168.1.1/32", int32(32), false),
Entry("/128", "2001:db8:3333:4444::0/128", int32(128), false),
Entry("/32", "192.168.1.1/32", int32(32), true),
Entry("/128", "2001:db8:3333:4444::0/128", int32(128), true),
Entry("valid ipv4", "192.168.1.0/24", int32(30), true),
Entry("valid ipv6", "2001:db8:3333:4444::0/64", int32(120), true),
)
Expand All @@ -203,8 +203,8 @@ var _ = Describe("CIDRPool", func() {
Entry("not set", "192.168.0.0/16", int32(0), false),
Entry("negative", "192.168.0.0/16", int32(-10), false),
Entry("larger than CIDR", "192.168.0.0/16", int32(8), false),
Entry("smaller than 31 for IPv4 pool", "192.168.0.0/16", int32(32), false),
Entry("smaller than 127 for IPv6 pool", "2001:db8:3333:4444::0/64", int32(128), false),
Entry("32 for IPv4 pool", "192.168.0.0/16", int32(32), true),
Entry("128 for IPv6 pool", "2001:db8:3333:4444::0/64", int32(128), true),
Entry("match CIDR prefix size - ipv4", "192.168.0.0/16", int32(16), true),
Entry("match CIDR prefix size - ipv6", "2001:db8:3333:4444::0/64", int32(64), true),
)
Expand Down
10 changes: 2 additions & 8 deletions api/v1alpha1/cidrpool_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,6 @@ func (r *CIDRPool) validateCIDR() field.ErrorList {
return field.ErrorList{field.Invalid(field.NewPath("spec", "cidr"), r.Spec.CIDR, "network prefix has host bits set")}
}

setBits, bitsTotal := network.Mask.Size()
if setBits == bitsTotal {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "cidr"), r.Spec.CIDR, "single IP prefixes are not supported")}
}

if r.Spec.GatewayIndex != nil && *r.Spec.GatewayIndex < 0 {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "gatewayIndex"), r.Spec.GatewayIndex, "must not be negative")}
Expand All @@ -75,9 +69,9 @@ func (r *CIDRPool) validateCIDR() field.ErrorList {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "perNodeNetworkPrefix"), r.Spec.PerNodeNetworkPrefix, "must not be negative")}
}

setBits, bitsTotal := network.Mask.Size()
if r.Spec.PerNodeNetworkPrefix == 0 ||
r.Spec.PerNodeNetworkPrefix >= int32(bitsTotal) ||
r.Spec.PerNodeNetworkPrefix > int32(bitsTotal) ||
r.Spec.PerNodeNetworkPrefix < int32(setBits) {
return field.ErrorList{field.Invalid(
field.NewPath("spec", "perNodeNetworkPrefix"),
Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha1/ippool_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ func (r *IPPool) Validate() field.ErrorList {
field.NewPath("spec", "subnet"), r.Spec.Subnet, "is invalid subnet"))
}

if r.Spec.PerNodeBlockSize < 2 {
if r.Spec.PerNodeBlockSize < 1 {
errList = append(errList, field.Invalid(
field.NewPath("spec", "perNodeBlockSize"),
r.Spec.PerNodeBlockSize, "must be at least 2"))
r.Spec.PerNodeBlockSize, "must be at least 1"))
}

if network != nil && r.Spec.PerNodeBlockSize >= 2 {
if network != nil && r.Spec.PerNodeBlockSize >= 1 {
if GetPossibleIPCount(network).Cmp(big.NewInt(int64(r.Spec.PerNodeBlockSize))) < 0 {
// config is not valid even if only one node exist in the cluster
errList = append(errList, field.Invalid(
Expand Down
12 changes: 12 additions & 0 deletions pkg/ip/cidr.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ func IsBroadcast(ip net.IP, network *net.IPNet) bool {
if network.IP.To4() == nil {
return false
}
if IsPointToPointSubnet(network) || IsSingleIPSubnet(network) {
vasrem marked this conversation as resolved.
Show resolved Hide resolved
return false
}
if !network.Contains(ip) {
return false
}
Expand All @@ -153,8 +156,17 @@ func IsPointToPointSubnet(network *net.IPNet) bool {
return ones == maskLen-1
}

// IsSingleIPSubnet returns true if the network is a single IP subnet (/32 or /128)
func IsSingleIPSubnet(network *net.IPNet) bool {
ones, maskLen := network.Mask.Size()
return ones == maskLen
}

// LastIP returns the last IP of a subnet, excluding the broadcast if IPv4 (if not /31 net)
func LastIP(network *net.IPNet) net.IP {
if IsSingleIPSubnet(network) {
return network.IP
}
var end net.IP
for i := 0; i < len(network.IP); i++ {
end = append(end, network.IP[i]|^network.Mask[i])
Expand Down
66 changes: 66 additions & 0 deletions pkg/ip/cidr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ var _ = Describe("CIDR functions", func() {
testNet,
true,
},
{
net.ParseIP("192.168.0.10"),
func() *net.IPNet {
_, testNet, _ := net.ParseCIDR("192.168.0.10/32")
return testNet
}(),
false,
},
{
net.ParseIP("192.168.0.1"),
func() *net.IPNet {
_, testNet, _ := net.ParseCIDR("192.168.0.0/31")
return testNet
}(),
false,
},
}

for _, test := range testCases {
Expand Down Expand Up @@ -373,6 +389,30 @@ var _ = Describe("CIDR functions", func() {
Expect(gen().String()).To(Equal("::2/127"))
Expect(gen().String()).To(Equal("::4/127"))
})
It("valid - single IP IPv4 subnet", func() {
_, net, _ := net.ParseCIDR("192.168.0.0/16")
gen := GetSubnetGen(net, 32)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("192.168.0.0/32"))
Expect(gen().String()).To(Equal("192.168.0.1/32"))
Expect(gen().String()).To(Equal("192.168.0.2/32"))
})
It("valid - single IP IPv6 subnet", func() {
_, net, _ := net.ParseCIDR("2002:0:0:1234::/64")
gen := GetSubnetGen(net, 128)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("2002:0:0:1234::/128"))
Expect(gen().String()).To(Equal("2002:0:0:1234::1/128"))
Expect(gen().String()).To(Equal("2002:0:0:1234::2/128"))
})
It("valid - single IP IPv4 subnet, point to point network", func() {
_, net, _ := net.ParseCIDR("192.168.0.0/31")
gen := GetSubnetGen(net, 32)
Expect(gen).NotTo(BeNil())
Expect(gen().String()).To(Equal("192.168.0.0/32"))
Expect(gen().String()).To(Equal("192.168.0.1/32"))
Expect(gen()).To(BeNil())
})
})
Context("IsPointToPointSubnet", func() {
It("/31", func() {
Expand All @@ -388,6 +428,24 @@ var _ = Describe("CIDR functions", func() {
Expect(IsPointToPointSubnet(network)).To(BeFalse())
})
})
Context("IsSingleIPSubnet", func() {
It("/32", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/32")
Expect(IsSingleIPSubnet(network)).To(BeTrue())
})
It("/128", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::1/128")
Expect(IsSingleIPSubnet(network)).To(BeTrue())
})
It("/24", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/24")
Expect(IsSingleIPSubnet(network)).To(BeFalse())
})
It("/31", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/31")
Expect(IsSingleIPSubnet(network)).To(BeFalse())
})
})
Context("LastIP", func() {
It("/31", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/31")
Expand All @@ -397,6 +455,14 @@ var _ = Describe("CIDR functions", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::0/127")
Expect(LastIP(network).String()).To(Equal("2002:0:0:1234::1"))
})
It("/32", func() {
_, network, _ := net.ParseCIDR("192.168.1.10/32")
Expect(LastIP(network).String()).To(Equal("192.168.1.10"))
})
It("/128", func() {
_, network, _ := net.ParseCIDR("2002:0:0:1234::10/128")
Expect(LastIP(network).String()).To(Equal("2002:0:0:1234::10"))
})
It("/24", func() {
_, network, _ := net.ParseCIDR("192.168.1.0/24")
Expect(LastIP(network).String()).To(Equal("192.168.1.254"))
Expand Down
55 changes: 40 additions & 15 deletions pkg/ipam-controller/allocator/allocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,16 @@ func (pa *PoolAllocator) AllocateFromPool(ctx context.Context, node string) (*Al
return &existingAlloc, nil
}
allocations := pa.getAllocationsAsSlice()
// determine the first possible range for the subnet
var startIP net.IP
if len(allocations) == 0 || ip.Distance(pa.cfg.Subnet.IP, allocations[0].StartIP) > 2 {
// start allocations from the network address if there are no allocations or if the "hole" exist before
// the firs allocation
startIP = ip.NextIP(pa.cfg.Subnet.IP)
if pa.canUseNetworkAddress() {
startIP = pa.cfg.Subnet.IP
} else {
startIP = ip.NextIP(pa.cfg.Subnet.IP)
}
// check if the first possible range is already allocated, if so, search for "holes" or use the next subnet
if len(allocations) != 0 && allocations[0].StartIP.Equal(startIP) {
startIP = nil
for i := 0; i < len(allocations); i++ {
nextI := i + 1
// if last allocation in the list
Expand Down Expand Up @@ -122,6 +126,12 @@ func (pa *PoolAllocator) Deallocate(ctx context.Context, node string) {
}
}

// canUseNetworkAddress returns true if it is allowed to use network address in the node range
// it is allowed to use network address if the subnet is point to point of a single IP subnet
func (pa *PoolAllocator) canUseNetworkAddress() bool {
return ip.IsPointToPointSubnet(pa.cfg.Subnet) || ip.IsSingleIPSubnet(pa.cfg.Subnet)
}

// load loads range to the pool allocator with validation for conflicts
func (pa *PoolAllocator) load(ctx context.Context, nodeName string, allocRange AllocatedRange) error {
log := pa.getLog(ctx, pa.cfg).WithValues("node", nodeName)
Expand All @@ -147,29 +157,44 @@ func (pa *PoolAllocator) checkAllocation(allocRange AllocatedRange) error {
if !pa.cfg.Subnet.Contains(allocRange.StartIP) || !pa.cfg.Subnet.Contains(allocRange.EndIP) {
return fmt.Errorf("invalid allocation allocators: start or end IP is out of the subnet")
}

if ip.Cmp(allocRange.EndIP, allocRange.StartIP) <= 0 {
return fmt.Errorf("invalid allocation allocators: start IP must be less then end IP")
if ip.Cmp(allocRange.EndIP, allocRange.StartIP) < 0 {
return fmt.Errorf("invalid allocation allocators: start IP must be less or equal to end IP")
}

// check that StartIP of the range has valid offset.
// all ranges have same size, so we can simply check that (StartIP offset - 1) % pa.cfg.PerNodeBlockSize == 0
// -1 required because we skip network addressee (e.g. in 192.168.0.0/24, first allocation will be 192.168.0.1)
distanceFromNetworkStart := ip.Distance(pa.cfg.Subnet.IP, allocRange.StartIP)
if distanceFromNetworkStart < 1 ||
math.Mod(float64(distanceFromNetworkStart)-1, float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
// check that StartIP of the range has valid offset.
// all ranges have same size, so we can simply check that (StartIP offset) % pa.cfg.PerNodeBlockSize == 0
if pa.canUseNetworkAddress() {
if math.Mod(float64(distanceFromNetworkStart), float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
}
} else {
if distanceFromNetworkStart < 1 ||
// -1 required because we skip network address (e.g. in 192.168.0.0/24, first allocation will be 192.168.0.1)
math.Mod(float64(distanceFromNetworkStart)-1, float64(pa.cfg.PerNodeBlockSize)) != 0 {
return fmt.Errorf("invalid start IP offset")
}
}
if ip.Distance(allocRange.StartIP, allocRange.EndIP) != int64(pa.cfg.PerNodeBlockSize)-1 {
return fmt.Errorf("ip count mismatch")
}
// for single IP ranges we need to discard allocation if it matches the gateway
if pa.cfg.PerNodeBlockSize == 1 && pa.cfg.Gateway != nil && allocRange.StartIP.Equal(pa.cfg.Gateway) {
return fmt.Errorf("gw can't be allocated when perNodeBlockSize is 1")
}
return nil
}

// return slice with allocated ranges.
// ranges are not overlap and are sorted, but there can be "holes" between ranges
func (pa *PoolAllocator) getAllocationsAsSlice() []AllocatedRange {
allocatedRanges := make([]AllocatedRange, 0, len(pa.allocations))
allocatedRanges := make([]AllocatedRange, 0, len(pa.allocations)+1)

if pa.cfg.PerNodeBlockSize == 1 && pa.cfg.Gateway != nil {
// in case if perNodeBlockSize is 1 we should not allocate the gateway,
// add a "virtual" allocation for the gateway if we detect that only 1 IP is requested per node,
// this allocation should never be exposed to the CR's status
allocatedRanges = append(allocatedRanges, AllocatedRange{StartIP: pa.cfg.Gateway, EndIP: pa.cfg.Gateway})
}
for _, a := range pa.allocations {
allocatedRanges = append(allocatedRanges, a)
}
Expand Down
Loading
Loading