Skip to content

Commit fa6f7d2

Browse files
committed
Allow for allocating all valid host IPs from ReservedSubnets
Allow allocation of first/last IPs in reserved subnets when they are valid host addresses. Ensure that IPv4 network (.0) and broadcast addresses are automatically excluded from reserved subnet allocators to prevent invalid IP assignments. Signed-off-by: Patryk Diak <[email protected]>
1 parent 10338ca commit fa6f7d2

File tree

3 files changed

+95
-19
lines changed

3 files changed

+95
-19
lines changed

go-controller/pkg/allocator/ip/allocator.go

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,30 @@ type Range struct {
9090
}
9191

9292
// NewAllocatorCIDRRange creates a Range over a net.IPNet, calling allocatorFactory to construct the backing store.
93+
// It excludes the network address (.0) and broadcast address (IPv4 only) from allocation.
9394
func NewAllocatorCIDRRange(cidr *net.IPNet, allocatorFactory allocator.AllocatorFactory) (*Range, error) {
95+
r, err := NewAllocatorFullCIDRRange(cidr, allocatorFactory)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
if utilnet.IsIPv4CIDR(cidr) {
101+
// Don't use the IPv4 network's broadcast address.
102+
r.max--
103+
}
104+
// Don't use the network's ".0" address.
105+
r.base.Add(r.base, big.NewInt(1))
106+
r.max--
107+
108+
r.max = maximum(0, r.max)
109+
// Reconfigure the allocator to use the new max value
110+
r.alloc, err = allocatorFactory(r.max, r.net.String())
111+
return r, err
112+
}
113+
114+
// NewAllocatorFullCIDRRange creates a Range over a net.IPNet without excluding any IPs,
115+
// calling allocatorFactory to construct the backing store.
116+
func NewAllocatorFullCIDRRange(cidr *net.IPNet, allocatorFactory allocator.AllocatorFactory) (*Range, error) {
94117
max := utilnet.RangeSize(cidr)
95118
base := utilnet.BigForIP(cidr.IP)
96119
rangeSpec := cidr.String()
@@ -100,19 +123,11 @@ func NewAllocatorCIDRRange(cidr *net.IPNet, allocatorFactory allocator.Allocator
100123
if max > 65536 {
101124
max = 65536
102125
}
103-
} else {
104-
// Don't use the IPv4 network's broadcast address.
105-
max--
106126
}
107-
108-
// Don't use the network's ".0" address.
109-
base.Add(base, big.NewInt(1))
110-
max--
111-
112127
r := Range{
113128
net: cidr,
114129
base: base,
115-
max: maximum(0, int(max)),
130+
max: int(max),
116131
}
117132
var err error
118133
r.alloc, err = allocatorFactory(r.max, rangeSpec)
@@ -213,14 +228,37 @@ func (r *Range) Has(ip net.IP) bool {
213228
}
214229

215230
// Reserved returns true if the provided IP can't be allocated. This is *only*
216-
// true for the network and broadcast addresses.
231+
// true for the network and broadcast addresses of the original CIDR.
217232
func (r *Range) Reserved(ip net.IP) bool {
218233
if !r.net.Contains(ip) {
219234
return false
220235
}
221236

222-
offset := calculateIPOffset(r.base, ip)
223-
return offset == -1 || offset == r.max
237+
// For IPv4, reserve network (.0) and broadcast addresses
238+
if utilnet.IsIPv4CIDR(r.net) {
239+
// Network address is the base IP of the original CIDR
240+
networkAddr := r.net.IP
241+
if ip.Equal(networkAddr) {
242+
return true
243+
}
244+
245+
// Broadcast address is the last IP in the original CIDR
246+
rangeSize := utilnet.RangeSize(r.net)
247+
broadcastAddr, _ := utilnet.GetIndexedIP(r.net, int(rangeSize)-1)
248+
if ip.Equal(broadcastAddr) {
249+
return true
250+
}
251+
}
252+
253+
// For IPv6, only reserve the network address (no broadcast concept)
254+
if utilnet.IsIPv6CIDR(r.net) {
255+
networkAddr := r.net.IP
256+
if ip.Equal(networkAddr) {
257+
return true
258+
}
259+
}
260+
261+
return false
224262
}
225263

226264
// contains returns true and the offset if the ip is in the range, and false

go-controller/pkg/allocator/ip/subnet/allocator.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
iputils "github.com/containernetworking/plugins/pkg/ip"
1111

1212
"k8s.io/klog/v2"
13+
utilnet "k8s.io/utils/net"
1314

1415
bitmapallocator "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/allocator/bitmap"
1516
ipallocator "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/allocator/ip"
@@ -84,9 +85,9 @@ func newIPAMAllocator(cidr *net.IPNet) (ipallocator.ContinuousAllocator, error)
8485
}
8586

8687
// newReservedIPAMAllocator provides an ipam interface which can be used for IPAM
87-
// allocations for a given cidr using static IP allocations only.
88+
// allocations for a given cidr using static IP allocations only. All IPs are available.
8889
func newReservedIPAMAllocator(cidr *net.IPNet) (ipallocator.StaticAllocator, error) {
89-
return ipallocator.NewAllocatorCIDRRange(cidr, func(max int, rangeSpec string) (bitmapallocator.Interface, error) {
90+
return ipallocator.NewAllocatorFullCIDRRange(cidr, func(max int, rangeSpec string) (bitmapallocator.Interface, error) {
9091
return bitmapallocator.NewRoundRobinAllocationMap(max, rangeSpec), nil
9192
})
9293
}
@@ -109,12 +110,20 @@ func (allocator *allocator) AddOrUpdateSubnet(config SubnetConfig) error {
109110
klog.Warningf("Replacing subnets %v with %v for %s", util.StringSlice(subnetInfo.subnets), util.StringSlice(config.Subnets), config.Name)
110111
}
111112
var ipams []ipallocator.ContinuousAllocator
113+
114+
// subnetBoundaryIPs holds network and broadcast addresses for IPv4 subnets.
115+
// These are automatically excluded from reserved subnet allocators to prevent allocation.
116+
var subnetBoundaryIPs []net.IP
112117
for _, subnet := range config.Subnets {
113118
ipam, err := allocator.ipamFunc(subnet)
114119
if err != nil {
115120
return fmt.Errorf("failed to initialize IPAM of subnet %s for %s: %w", subnet, config.Name, err)
116121
}
117122
ipams = append(ipams, ipam)
123+
124+
if utilnet.IsIPv4CIDR(subnet) {
125+
subnetBoundaryIPs = append(subnetBoundaryIPs, subnet.IP, util.SubnetBroadcastIP(*subnet))
126+
}
118127
}
119128

120129
// reservedSubnets is a subset of subnets, and it should not be used by automatic IPAM
@@ -141,6 +150,15 @@ func (allocator *allocator) AddOrUpdateSubnet(config SubnetConfig) error {
141150
return fmt.Errorf("failed to initialize IPAM of reserved subnet %s for %s: %w", reservedSubnet, config.Name, err)
142151
}
143152
staticIPAMs = append(staticIPAMs, ipam)
153+
154+
// Exclude network and broadcast addresses from reserved subnet allocators
155+
for _, excludedSubnetIP := range subnetBoundaryIPs {
156+
if reservedSubnet.Contains(excludedSubnetIP) {
157+
if err := ipam.Allocate(excludedSubnetIP); err != nil {
158+
return fmt.Errorf("failed to exclude %s from reserved subnet allocator %s: %w", excludedSubnetIP, ipam.CIDR(), err)
159+
}
160+
}
161+
}
144162
}
145163
allocator.cache[config.Name] = subnetInfo{
146164
subnets: config.Subnets,
@@ -243,6 +261,7 @@ func (allocator *allocator) AllocateIPPerSubnet(name string, ips []*net.IPNet) e
243261
err = fmt.Errorf("failed to allocate IP %s for %s: attempted to reserve multiple IPs in the same static IPAM instance", ipnet.IP, name)
244262
return err
245263
}
264+
246265
if err = staticIPAM.Allocate(ipnet.IP); err != nil {
247266
return err
248267
}

go-controller/pkg/allocator/ip/subnet/allocator_test.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,9 +250,9 @@ var _ = ginkgo.Describe("Subnet IP allocator operations", func() {
250250
"10.1.1.0/24",
251251
}
252252
reservedSubnets := []string{
253-
"10.1.1.0/28", // Reserve 10.1.1.0-15
253+
"10.1.1.16/28", // Reserve 10.1.1.16-31
254254
}
255-
expectedIP := "10.1.1.16/24"
255+
expectedIP := "10.1.1.1/24"
256256

257257
err := allocator.AddOrUpdateSubnet(SubnetConfig{
258258
Name: subnetName,
@@ -266,8 +266,14 @@ var _ = ginkgo.Describe("Subnet IP allocator operations", func() {
266266
gomega.Expect(ips).To(gomega.HaveLen(1))
267267
gomega.Expect(ips[0].String()).To(gomega.Equal(expectedIP))
268268

269-
// Should be able to allocate specific IPs from reserved range
270-
reservedIPs, err := util.ParseIPNets([]string{"10.1.1.5/24"})
269+
// Should be able to allocate the first IP from the reserved range
270+
reservedIPs, err := util.ParseIPNets([]string{"10.1.1.16/24"})
271+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
272+
err = allocator.AllocateIPPerSubnet(subnetName, reservedIPs)
273+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
274+
275+
// Should be able to allocate the last IP from the reserved range
276+
reservedIPs, err = util.ParseIPNets([]string{"10.1.1.31/24"})
271277
gomega.Expect(err).NotTo(gomega.HaveOccurred())
272278
err = allocator.AllocateIPPerSubnet(subnetName, reservedIPs)
273279
gomega.Expect(err).NotTo(gomega.HaveOccurred())
@@ -282,7 +288,8 @@ var _ = ginkgo.Describe("Subnet IP allocator operations", func() {
282288
"10.1.1.0/24",
283289
}
284290
reservedSubnets := []string{
285-
"10.1.1.0/28", // Reserve 10.1.1.0-15
291+
"10.1.1.0/28", // Reserve 10.1.1.0-15
292+
"10.1.1.240/28", // Reserve 10.1.1.240-255
286293
}
287294
excludeSubnets := []string{
288295
"10.1.1.200/29", // Exclude 10.1.1.200-207
@@ -313,6 +320,18 @@ var _ = ginkgo.Describe("Subnet IP allocator operations", func() {
313320
gomega.Expect(err).NotTo(gomega.HaveOccurred())
314321
err = allocator.AllocateIPPerSubnet(subnetName, excludedIPs)
315322
gomega.Expect(err).To(gomega.MatchError(ipam.ErrAllocated))
323+
324+
// Should not be able to allocate the network IP
325+
reservedIPs, err = util.ParseIPNets([]string{"10.1.1.0/24"})
326+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
327+
err = allocator.AllocateIPPerSubnet(subnetName, reservedIPs)
328+
gomega.Expect(err).To(gomega.HaveOccurred())
329+
330+
// Should not be able to allocate the broadcast IP
331+
reservedIPs, err = util.ParseIPNets([]string{"10.1.1.255/24"})
332+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
333+
err = allocator.AllocateIPPerSubnet(subnetName, reservedIPs)
334+
gomega.Expect(err).To(gomega.HaveOccurred())
316335
})
317336

318337
})

0 commit comments

Comments
 (0)