Skip to content

Commit 0bb337b

Browse files
committed
feat(vpcpeering): Detect overlapping prefixes when adding a VpcExpose
When validating a VpcExpose object, we ensure that the prefixes in each category (ips, as_range, nots, not_as) do not overlap. However, within a VpcManifest, we must also ensure that prefixes from different VpcExpose objects do not collide. In particular, we want to make sure that the available addresses in the "ips" lists do not collide with "ips" lists from other VpcExpose (or if the prefixes collide, they should also be covered by exclusion prefixes). Same applies to the "as" lists. Introduce a function to check overlap between two sets of addresses (one set = allowed prefixes + exclusion prefixes). See comments in the code for details on the implementation. Call this function as part of the validation process for a VpcManifest, to ensure there is no collision. Signed-off-by: Quentin Monnet <[email protected]>
1 parent b0b97e7 commit 0bb337b

File tree

1 file changed

+141
-0
lines changed

1 file changed

+141
-0
lines changed

mgmt/src/models/external/overlay/vpcpeering.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,27 @@ impl VpcManifest {
143143
..Default::default()
144144
}
145145
}
146+
fn validate_expose_collisions(&self) -> ConfigResult {
147+
// Check that prefixes in each expose don't overlap with prefixes in other exposes
148+
for (index, expose_left) in self.exposes.iter().enumerate() {
149+
// Loop over the remaining exposes in the list
150+
for expose_right in self.exposes.iter().skip(index + 1) {
151+
validate_overlapping(
152+
&expose_left.ips,
153+
&expose_left.nots,
154+
&expose_right.ips,
155+
&expose_right.nots,
156+
)?;
157+
validate_overlapping(
158+
&expose_left.as_range,
159+
&expose_left.not_as,
160+
&expose_right.as_range,
161+
&expose_right.not_as,
162+
)?;
163+
}
164+
}
165+
Ok(())
166+
}
146167
pub fn add_expose(&mut self, expose: VpcExpose) -> ConfigResult {
147168
self.exposes.push(expose);
148169
Ok(())
@@ -157,6 +178,7 @@ impl VpcManifest {
157178
for expose in &self.exposes {
158179
expose.validate()?;
159180
}
181+
self.validate_expose_collisions()?;
160182
Ok(())
161183
}
162184
}
@@ -230,6 +252,125 @@ impl VpcPeeringTable {
230252
}
231253
}
232254

255+
// Validate that two sets of prefixes, with their exclusion prefixes applied, don't overlap
256+
fn validate_overlapping(
257+
prefixes_left: &BTreeSet<Prefix>,
258+
excludes_left: &BTreeSet<Prefix>,
259+
prefixes_right: &BTreeSet<Prefix>,
260+
excludes_right: &BTreeSet<Prefix>,
261+
) -> Result<(), ConfigError> {
262+
// Find colliding prefixes
263+
let mut colliding = Vec::new();
264+
for prefix_left in prefixes_left.iter() {
265+
for prefix_right in prefixes_right.iter() {
266+
if prefix_left.covers(prefix_right) || prefix_right.covers(prefix_left) {
267+
colliding.push((prefix_left.clone(), prefix_right.clone()));
268+
}
269+
}
270+
}
271+
// If not prefixes collide, we're good - exit.
272+
if colliding.is_empty() {
273+
return Ok(());
274+
}
275+
276+
// How do we determine whether there is a collision between the set of available addresses on
277+
// the left side, and the set of available addresses on the right side? A collision means:
278+
//
279+
// - Prefixes collide, in other words, they have a non-empty intersection (we've checked that
280+
// earlier)
281+
//
282+
// - This intersection is not fully covered by exclusion prefixes
283+
//
284+
// The idea in the loop below is that for each pair of colliding prefixes:
285+
//
286+
// - We retrieve the size of the intersection of the colliding prefixes. This is easy, because
287+
// they're "prefixes", so if they collide we have necessarily one that is contained within the
288+
// other, and the size of the intersection is the size of the smallest one.
289+
//
290+
// - We retrieve the size of the union of all the exclusion prefixes (from left and right sides)
291+
// covering part of this intersection (which we know is the smallest of the two colliding
292+
// prefixes). The union of the exclusion prefixes is the set of non-overlapping exclusion
293+
// prefixes that cover the intersection of allowed prefixes, such that if exclusion prefixes
294+
// collide, we always keep the largest prefix.
295+
//
296+
// - If the size of the intersection of colliding allowed prefixes is bigger than the size of
297+
// the union of the exclusion prefixes applying to them, then it needs that some addresses are
298+
// effectively allowed in both the left-side and the right-side set of available addresses,
299+
// and this is an error. If the sizes are identical, then all addresses in the intersection of
300+
// the prefixes are excluded on at least one side, so it's all good.
301+
for (prefix_left, prefix_right) in colliding {
302+
// If prefixes collide, there's necessarily one prefix that is contained inside of the
303+
// other. Find the intersection of the two colliding prefixes, which is the smallest of the
304+
// two prefixes.
305+
let intersection_prefix = if prefix_left.covers(&prefix_right) {
306+
&prefix_right
307+
} else {
308+
&prefix_left
309+
};
310+
311+
// Retrieve the union of all exclusion prefixes covering the intersection of the colliding
312+
// prefixes
313+
let mut union_excludes = BTreeSet::new();
314+
315+
// Consider exclusion prefixes from excludes_left
316+
'outer: for exclude_left in excludes_left.iter().filter(|exclude| {
317+
exclude.covers(intersection_prefix) || intersection_prefix.covers(exclude)
318+
}) {
319+
for exclude_right in excludes_right.iter().filter(|exclude| {
320+
exclude.covers(intersection_prefix) || intersection_prefix.covers(exclude)
321+
}) {
322+
if exclude_left.covers(exclude_right) {
323+
// exclude_left contains exclude_right, and given that exclusion prefixes in
324+
// list excludes_right don't overlap there's no exclusion prefix containing
325+
// exclude_left. We want to keep exclude_left as part of the union.
326+
union_excludes.insert(exclude_left.clone());
327+
continue 'outer;
328+
} else if exclude_right.covers(exclude_left) {
329+
// exclude_left is contained within exclude_right, don't keep it as part of the
330+
// union. Process next exclusion prefix from list excludes_left.
331+
continue 'outer;
332+
}
333+
}
334+
// No collision for this exclude_left, add it to the union
335+
union_excludes.insert(exclude_left.clone());
336+
}
337+
// Consider exclusion prefixes from excludes_right
338+
'outer: for exclude_right in excludes_right.iter().filter(|exclude| {
339+
exclude.covers(intersection_prefix) || intersection_prefix.covers(exclude)
340+
}) {
341+
for exclude_left in excludes_left.iter().filter(|exclude| {
342+
exclude.covers(intersection_prefix) || intersection_prefix.covers(exclude)
343+
}) {
344+
if exclude_right.covers(exclude_left) {
345+
// exclude_right contains exclude_left, and given that exclusions prefixes in
346+
// list excludes_left don't overlap there's no exclusion prefix containing
347+
// exclude_right. We want to keep exclude_right as part of the union.
348+
union_excludes.insert(exclude_right.clone());
349+
continue 'outer;
350+
} else if exclude_left.covers(exclude_right) {
351+
// exclude_right is contained within exclude_left, don't keep it as part of the
352+
// union. Process next exclusion prefix from list excludes_right.
353+
continue 'outer;
354+
}
355+
}
356+
// No collision for this exclude_right, add it to the union
357+
union_excludes.insert(exclude_right.clone());
358+
}
359+
360+
let union_size = union_excludes
361+
.iter()
362+
.map(|exclude| exclude.size())
363+
.sum::<u128>();
364+
if union_size < intersection_prefix.size() {
365+
// Some addresses at the intersection of both prefixes are not covered by the union of
366+
// all exclusion prefixes, in other words, they are available from both prefixes. This
367+
// is an error.
368+
return Err(ConfigError::OverlappingPrefixes(prefix_left, prefix_right));
369+
}
370+
}
371+
Ok(())
372+
}
373+
233374
#[cfg(test)]
234375
mod tests {
235376
use super::*;

0 commit comments

Comments
 (0)