Skip to content

Commit 3d9de1d

Browse files
authored
Merge pull request #2657 from cloudfoundry/fix_ip_allocation_from_reserved_range_in_dynamic_networks
Fix IP Allocation Bug: Reserved Range Not Detected
2 parents 3f4620e + c6bfa1f commit 3d9de1d

5 files changed

Lines changed: 328 additions & 6 deletions

File tree

src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_repo.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def try_to_allocate_dynamic_ip(reservation, subnet)
106106
ip_address_cidr = find_next_available_ip(addresses_we_cant_allocate, first_range_address, subnet.prefix)
107107

108108
if !(subnet.range == ip_address_cidr || subnet.range.include?(ip_address_cidr))
109-
raise NoMoreIPsAvailableAndStopRetrying
109+
raise NoMoreIPsAvailableAndStopRetrying
110110
end
111111

112112
save_ip(ip_address_cidr, reservation, false)
@@ -123,12 +123,12 @@ def find_next_available_ip(addresses_we_cant_allocate, first_range_address, pref
123123

124124
while found == false
125125
current_prefix = to_ipaddr("#{current_ip.base_addr}/#{prefix}")
126+
filtered_ips.reject! { |ip| ip.to_range.last.to_i < current_prefix.to_i }
126127

127-
if filtered_ips.any? { |ip| current_prefix.include?(ip) }
128-
filtered_ips.reject! { |ip| ip.to_i < current_prefix.to_i }
129-
actual_ip_prefix = filtered_ips.first.count
130-
if actual_ip_prefix > current_prefix.count
131-
current_ip = to_ipaddr(current_ip.to_i + actual_ip_prefix)
128+
blocking_ip = filtered_ips.first
129+
if blocking_ip&.overlaps?(current_prefix)
130+
if blocking_ip.count > current_prefix.count
131+
current_ip = to_ipaddr(blocking_ip.to_i + blocking_ip.count)
132132
else
133133
current_ip = to_ipaddr(current_ip.to_i + current_prefix.count)
134134
end

src/bosh-director/lib/bosh/director/ip_addr_or_cidr.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ def first
8484
Bosh::Director::IpAddrOrCidr.new(@ipaddr.to_range.first.to_i)
8585
end
8686

87+
def overlaps?(other)
88+
my_first = @ipaddr.to_range.first.to_i
89+
my_last = @ipaddr.to_range.last.to_i
90+
other_first = other.to_range.first.to_i
91+
other_last = other.to_range.last.to_i
92+
my_first <= other_last && other_first <= my_last
93+
end
94+
8795
private
8896

8997
def max_addresses

src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_repo_spec.rb

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,201 @@ def fail_saving_ips(ips, fail_error)
492492
it_behaves_like :retries_on_race_condition
493493
end
494494
end
495+
496+
context 'when handling CIDR blocks and overlapping ranges' do
497+
def save_ip_string(ip_string)
498+
ip_addr = to_ipaddr(ip_string)
499+
Bosh::Director::Models::IpAddress.new(
500+
address_str: ip_addr.to_s,
501+
network_name: 'my-manual-network',
502+
instance: instance_model,
503+
task_id: Bosh::Director::Config.current_job.task_id
504+
).save
505+
end
506+
507+
context 'when database has individual IPs that are contained in a reserved CIDR block' do
508+
it 'deduplicates and skips the entire CIDR block' do
509+
network_spec['subnets'].first['range'] = '10.0.11.32/27'
510+
network_spec['subnets'].first['gateway'] = '10.0.11.33'
511+
network_spec['subnets'].first['reserved'] = ['10.0.11.32 - 10.0.11.35', '10.0.11.63']
512+
network_spec['subnets'].first['static'] = ['10.0.11.36', '10.0.11.37', '10.0.11.38', '10.0.11.39', '10.0.11.40']
513+
514+
save_ip_string('10.0.11.32/32')
515+
save_ip_string('10.0.11.33/32')
516+
517+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
518+
expect(ip_address).to eq(cidr_ip('10.0.11.41'))
519+
end
520+
end
521+
522+
context 'when multiple overlapping CIDR blocks exist' do
523+
it 'deduplicates to largest block only' do
524+
network_spec['subnets'].first['range'] = '192.168.1.0/24'
525+
network_spec['subnets'].first['gateway'] = '192.168.1.1'
526+
network_spec['subnets'].first['reserved'] = [
527+
'192.168.1.0 - 192.168.1.15',
528+
'192.168.1.0 - 192.168.1.3',
529+
'192.168.1.4 - 192.168.1.7',
530+
'192.168.1.8',
531+
]
532+
533+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
534+
expect(ip_address).to eq(cidr_ip('192.168.1.16'))
535+
end
536+
end
537+
538+
context 'when nested CIDR blocks exist' do
539+
it 'deduplicates to outermost block' do
540+
network_spec['subnets'].first['range'] = '192.168.1.0/24'
541+
network_spec['subnets'].first['gateway'] = '192.168.1.1'
542+
network_spec['subnets'].first['reserved'] = [
543+
'192.168.1.0/24',
544+
'192.168.1.0/26',
545+
'192.168.1.0/28',
546+
]
547+
548+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
549+
expect(ip_address).to be_nil
550+
end
551+
end
552+
553+
context 'when adjacent non-overlapping CIDR blocks exist' do
554+
it 'preserves all blocks and skips each correctly' do
555+
network_spec['subnets'].first['range'] = '10.0.0.0/24'
556+
network_spec['subnets'].first['gateway'] = '10.0.0.1'
557+
network_spec['subnets'].first['reserved'] = [
558+
'10.0.0.0 - 10.0.0.3',
559+
'10.0.0.4 - 10.0.0.7',
560+
'10.0.0.8 - 10.0.0.11',
561+
]
562+
563+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
564+
expect(ip_address).to eq(cidr_ip('10.0.0.12'))
565+
end
566+
end
567+
568+
context 'when large CIDR block contains scattered individual IPs' do
569+
it 'deduplicates scattered IPs within the block' do
570+
network_spec['subnets'].first['range'] = '10.1.1.0/24'
571+
network_spec['subnets'].first['gateway'] = '10.1.1.1'
572+
network_spec['subnets'].first['reserved'] = ['10.1.1.0/24']
573+
network_spec['subnets'].first['static'] = []
574+
575+
save_ip_string('10.1.1.5/32')
576+
save_ip_string('10.1.1.50/32')
577+
save_ip_string('10.1.1.100/32')
578+
save_ip_string('10.1.1.200/32')
579+
580+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
581+
expect(ip_address).to be_nil
582+
end
583+
end
584+
585+
context 'when handling AWS reserved IP ranges' do
586+
it 'correctly skips reserved ranges with database IPs' do
587+
network_spec['subnets'].first['range'] = '10.0.11.32/27'
588+
network_spec['subnets'].first['gateway'] = '10.0.11.33'
589+
network_spec['subnets'].first['reserved'] = ['10.0.11.32 - 10.0.11.35', '10.0.11.63']
590+
network_spec['subnets'].first['static'] = []
591+
592+
save_ip_string('10.0.11.32/32')
593+
save_ip_string('10.0.11.33/32')
594+
save_ip_string('10.0.11.34/32')
595+
596+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
597+
expect(ip_address).to eq(cidr_ip('10.0.11.36'))
598+
end
599+
end
600+
601+
context 'when candidate block lands inside a reserved CIDR that started before it' do
602+
it 'does not allocate an address inside the reserved range' do
603+
# Reserved: 10.0.0.130/23 covers 10.0.130.0 - 10.0.131.255 (512 IPs).
604+
# Walking forward from 10.0.129.0/24: that first candidate overlaps
605+
# the /23, so we advance by 512, landing at 10.0.131.0. But .131.0
606+
# is still inside the /23. The pruning step must NOT discard the /23
607+
# just because its base (.130.0) < new candidate base (.131.0).
608+
# The first address outside the reserved range is 10.0.132.0.
609+
network_spec['subnets'].first['range'] = '10.0.128.0/21'
610+
network_spec['subnets'].first['gateway'] = '10.0.128.1'
611+
network_spec['subnets'].first['reserved'] = [
612+
'10.0.128.0 - 10.0.129.255',
613+
'10.0.130.0 - 10.0.131.255',
614+
'10.0.132.0 - 10.0.133.0',
615+
]
616+
network_spec['subnets'].first['static'] = []
617+
618+
ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet)
619+
expect(ip_address).to eq(cidr_ip('10.0.133.1'))
620+
end
621+
end
622+
end
623+
624+
context 'when allocating dynamic IPs from an IPv6 subnet' do
625+
let(:ipv6_network_spec) do
626+
{
627+
'name' => 'my-manual-network',
628+
'subnets' => [
629+
{
630+
'range' => '2001:db8::/120',
631+
'gateway' => '2001:db8::1',
632+
'dns' => [],
633+
'static' => [],
634+
'reserved' => [],
635+
'cloud_properties' => {},
636+
'az' => 'az-1',
637+
},
638+
],
639+
}
640+
end
641+
642+
let(:ipv6_network) do
643+
ManualNetwork.parse(
644+
ipv6_network_spec,
645+
availability_zones,
646+
per_spec_logger,
647+
)
648+
end
649+
650+
let(:ipv6_subnet) do
651+
ManualNetworkSubnet.parse(
652+
ipv6_network.name,
653+
ipv6_network_spec['subnets'].first,
654+
availability_zones,
655+
)
656+
end
657+
658+
let(:ipv6_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, ipv6_network) }
659+
660+
it 'returns the first available IPv6 address' do
661+
ip_address = ip_repo.allocate_dynamic_ip(ipv6_reservation, ipv6_subnet)
662+
expect(ip_address).to eq(cidr_ip('2001:db8::2'))
663+
end
664+
665+
it 'allocates sequential IPv6 addresses' do
666+
first = ip_repo.allocate_dynamic_ip(ipv6_reservation, ipv6_subnet)
667+
second = ip_repo.allocate_dynamic_ip(ipv6_reservation, ipv6_subnet)
668+
expect(first).to eq(cidr_ip('2001:db8::2'))
669+
expect(second).to eq(cidr_ip('2001:db8::3'))
670+
end
671+
672+
context 'when there are reserved IPv6 ranges' do
673+
it 'skips reserved addresses' do
674+
ipv6_network_spec['subnets'].first['reserved'] = ['2001:db8::2 - 2001:db8::4']
675+
676+
ip_address = ip_repo.allocate_dynamic_ip(ipv6_reservation, ipv6_subnet)
677+
expect(ip_address).to eq(cidr_ip('2001:db8::5'))
678+
end
679+
end
680+
681+
context 'when there are reserved IPv6 CIDR blocks' do
682+
it 'skips the entire CIDR block' do
683+
ipv6_network_spec['subnets'].first['reserved'] = ['2001:db8::/124']
684+
685+
ip_address = ip_repo.allocate_dynamic_ip(ipv6_reservation, ipv6_subnet)
686+
expect(ip_address).to eq(cidr_ip('2001:db8::10'))
687+
end
688+
end
689+
end
495690
end
496691

497692
describe :allocate_vip_ip do

src/bosh-director/spec/unit/bosh/director/ip_addr_or_cidr_spec.rb

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,102 @@ module Bosh::Director
131131
end
132132
end
133133
end
134+
135+
describe '#overlaps?' do
136+
context 'when two /32 IPs are the same' do
137+
it 'returns true' do
138+
a = IpAddrOrCidr.new('10.0.0.1')
139+
b = IpAddrOrCidr.new('10.0.0.1')
140+
expect(a.overlaps?(b)).to be true
141+
end
142+
end
143+
144+
context 'when two /32 IPs are different' do
145+
it 'returns false' do
146+
a = IpAddrOrCidr.new('10.0.0.1')
147+
b = IpAddrOrCidr.new('10.0.0.2')
148+
expect(a.overlaps?(b)).to be false
149+
end
150+
end
151+
152+
context 'when a /32 is inside a CIDR block' do
153+
it 'returns true (IP inside block)' do
154+
ip = IpAddrOrCidr.new('192.168.1.5')
155+
block = IpAddrOrCidr.new('192.168.1.0/25')
156+
expect(ip.overlaps?(block)).to be true
157+
expect(block.overlaps?(ip)).to be true
158+
end
159+
end
160+
161+
context 'when a /32 is outside a CIDR block' do
162+
it 'returns false' do
163+
ip = IpAddrOrCidr.new('192.168.2.1')
164+
block = IpAddrOrCidr.new('192.168.1.0/25')
165+
expect(ip.overlaps?(block)).to be false
166+
expect(block.overlaps?(ip)).to be false
167+
end
168+
end
169+
170+
context 'when two CIDR blocks overlap partially' do
171+
it 'returns true' do
172+
a = IpAddrOrCidr.new('192.168.1.0/30')
173+
b = IpAddrOrCidr.new('192.168.1.2/30')
174+
expect(a.overlaps?(b)).to be true
175+
end
176+
end
177+
178+
context 'when two CIDR blocks are adjacent but non-overlapping' do
179+
it 'returns false' do
180+
a = IpAddrOrCidr.new('192.168.1.0/30')
181+
b = IpAddrOrCidr.new('192.168.1.4/30')
182+
expect(a.overlaps?(b)).to be false
183+
expect(b.overlaps?(a)).to be false
184+
end
185+
end
186+
187+
context 'when a smaller block is nested inside a larger block' do
188+
it 'returns true' do
189+
outer = IpAddrOrCidr.new('10.0.0.0/24')
190+
inner = IpAddrOrCidr.new('10.0.0.128/25')
191+
expect(outer.overlaps?(inner)).to be true
192+
expect(inner.overlaps?(outer)).to be true
193+
end
194+
end
195+
196+
context 'when CIDR blocks are completely disjoint' do
197+
it 'returns false' do
198+
a = IpAddrOrCidr.new('10.0.0.0/24')
199+
b = IpAddrOrCidr.new('10.0.1.0/24')
200+
expect(a.overlaps?(b)).to be false
201+
end
202+
end
203+
204+
context 'with the exact scenario that triggers the IPAddr coercion bug' do
205+
it 'correctly detects overlap between /32 and /25' do
206+
ip32 = IpAddrOrCidr.new('192.168.1.0')
207+
block25 = IpAddrOrCidr.new('192.168.1.0/25')
208+
expect(ip32.overlaps?(block25)).to be true
209+
210+
# A /32 NOT in the /25 range
211+
ip_outside = IpAddrOrCidr.new('192.168.1.200')
212+
expect(ip_outside.overlaps?(block25)).to be false
213+
end
214+
end
215+
216+
context 'with IPv6 addresses' do
217+
it 'detects overlapping IPv6 ranges' do
218+
a = IpAddrOrCidr.new('fd00::/64')
219+
b = IpAddrOrCidr.new('fd00::1')
220+
expect(a.overlaps?(b)).to be true
221+
expect(b.overlaps?(a)).to be true
222+
end
223+
224+
it 'returns false for non-overlapping IPv6 ranges' do
225+
a = IpAddrOrCidr.new('fd00::/64')
226+
b = IpAddrOrCidr.new('fd01::1')
227+
expect(a.overlaps?(b)).to be false
228+
end
229+
end
230+
end
134231
end
135232
end

src/spec/integration/global_networking/ip_reservations/allocating_dynamic_ips_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,5 +249,27 @@ def deploy_with_static_ip(deployment_name, ip, range)
249249
expect(second_deploy_instances.first.ips).to eq(first_deploy_instances.first.ips)
250250
expect(second_deploy_instances.first.vm_cid).to eq(first_deploy_instances.first.vm_cid)
251251
end
252+
253+
it 'does not allocate IPs within a CIDR reserved range' do
254+
cloud_config_hash = SharedSupport::DeploymentManifestHelper.simple_cloud_config
255+
cloud_config_hash['networks'].first['subnets'] = [
256+
{
257+
'range' => '192.168.1.0/24',
258+
'gateway' => '192.168.1.1',
259+
'dns' => [],
260+
'static' => [],
261+
'reserved' => ['192.168.1.0/28'],
262+
'cloud_properties' => {},
263+
}
264+
]
265+
266+
manifest_hash = SharedSupport::DeploymentManifestHelper.deployment_manifest(name: 'my-deploy', instances: 2)
267+
268+
upload_cloud_config(cloud_config_hash: cloud_config_hash)
269+
deploy_simple_manifest(manifest_hash: manifest_hash)
270+
271+
deployed_ips = director.instances(deployment_name: 'my-deploy').map(&:ips).flatten
272+
expect(deployed_ips).to match_array(['192.168.1.16', '192.168.1.17'])
273+
end
252274
end
253275
end

0 commit comments

Comments
 (0)