Skip to content

Conversation

@qmonnet
Copy link
Member

@qmonnet qmonnet commented Jan 19, 2026

Based on #1205

Make sure that we do not expose overlapping prefixes to a given VPC. The only supported exception is when one of the expose is a default expose destination.

WIP: Ensure we have at most one default expose object exposed to any given VPC.

@qmonnet qmonnet self-assigned this Jan 19, 2026
@qmonnet qmonnet added this to the GW R2 milestone Jan 19, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds validation to prevent overlapping prefixes in VPC peerings, building on PR #1205 which introduced support for "default" destination in flow-filter stage. The validation ensures that prefixes exposed to a VPC do not overlap, with the exception that overlaps are allowed when one of the exposes is a "default" expose destination.

Changes:

  • Added check_overlap() method to validate non-overlapping prefixes across VPC peerings
  • Updated import from Prefix to IpRangeWithPorts trait to use the overlaps() method
  • Added comprehensive test coverage for overlapping prefix detection and default expose behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
config/src/external/overlay/vpc.rs Implements overlap validation logic in new check_overlap() method and integrates it into the VPC validation flow
config/src/external/overlay/tests.rs Adds tests for overlapping prefix detection and validation of default expose allowing overlaps

@qmonnet qmonnet force-pushed the pr/qmonnet/expose_refined_more branch from bf1f81b to 82efafa Compare January 19, 2026 20:19
@qmonnet qmonnet requested a review from Copilot January 19, 2026 20:38
@qmonnet qmonnet marked this pull request as ready for review January 19, 2026 20:38
@qmonnet qmonnet requested a review from a team as a code owner January 19, 2026 20:38
@qmonnet qmonnet requested a review from mvachhar January 19, 2026 20:38
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment on lines 162 to 220
// FIXME: Find a less expensive approach to find overlapping prefixes
for (i, current_peering) in self.peerings.iter().enumerate() {
for other_peering in &self.peerings[i + 1..] {
for current_expose in &current_peering.remote.exposes {
for other_expose in &other_peering.remote.exposes {
match (current_expose.default, other_expose.default) {
(true, true) => {
// We support at most one default destination exposed to any VPC
error!(
"Multiple 'default' destinations exposed to VPC {}",
self.name
);
return Err(ConfigError::Forbidden(
"Multiple 'default' destinations exposed to VPC",
));
}
(true, false) | (false, true) => {
// Overlap is allowed between a prefix and a default expose
continue;
}
(false, false) => { /* keep processing */ }
}
for current_prefix in current_expose.public_ips() {
for other_prefix in other_expose.public_ips() {
if current_prefix.overlaps(other_prefix) {
error!(
"Prefixes exposed to VPC {} overlap: {} and {}",
self.name, current_prefix, other_prefix
);
return Err(ConfigError::OverlappingPrefixes(
*current_prefix,
*other_prefix,
));
}
}
}
}
}
}
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nested loop structure has O(n²×m²) complexity where n is the number of peerings and m is the number of exposes per peering. While the FIXME comment acknowledges this, consider whether this validation should be performed at a different stage or if there's a more efficient approach using a data structure like a prefix tree or interval tree to detect overlaps in O(n×m×log(n×m)) time.

Suggested change
// FIXME: Find a less expensive approach to find overlapping prefixes
for (i, current_peering) in self.peerings.iter().enumerate() {
for other_peering in &self.peerings[i + 1..] {
for current_expose in &current_peering.remote.exposes {
for other_expose in &other_peering.remote.exposes {
match (current_expose.default, other_expose.default) {
(true, true) => {
// We support at most one default destination exposed to any VPC
error!(
"Multiple 'default' destinations exposed to VPC {}",
self.name
);
return Err(ConfigError::Forbidden(
"Multiple 'default' destinations exposed to VPC",
));
}
(true, false) | (false, true) => {
// Overlap is allowed between a prefix and a default expose
continue;
}
(false, false) => { /* keep processing */ }
}
for current_prefix in current_expose.public_ips() {
for other_prefix in other_expose.public_ips() {
if current_prefix.overlaps(other_prefix) {
error!(
"Prefixes exposed to VPC {} overlap: {} and {}",
self.name, current_prefix, other_prefix
);
return Err(ConfigError::OverlappingPrefixes(
*current_prefix,
*other_prefix,
));
}
}
}
}
}
}
}
// Enforce "at most one default destination exposed to any VPC" and
// efficiently detect overlapping prefixes between non-default exposes
// across different peerings.
// Track how many *peerings* have at least one default expose.
// The previous implementation only rejected multiple defaults across
// different peerings (it never compared exposes within the same peering),
// so we preserve that behavior here.
let mut peerings_with_default = 0usize;
// Collect all prefixes from non-default exposes, annotated with the
// peering index. Overlap is allowed between a prefix and a default
// expose, so defaults are excluded from overlap detection.
let mut prefixes: Vec<(IpRangeWithPorts, usize)> = Vec::new();
for (peering_idx, peering) in self.peerings.iter().enumerate() {
let mut has_default_in_peering = false;
for expose in &peering.remote.exposes {
if expose.default {
has_default_in_peering = true;
// Skip prefixes for default exposes: overlap with defaults is allowed.
continue;
}
for prefix in expose.public_ips() {
prefixes.push((*prefix, peering_idx));
}
}
if has_default_in_peering {
peerings_with_default += 1;
if peerings_with_default > 1 {
// We support at most one default destination exposed to any VPC
error!(
"Multiple 'default' destinations exposed to VPC {}",
self.name
);
return Err(ConfigError::Forbidden(
"Multiple 'default' destinations exposed to VPC",
));
}
}
}
// Sort prefixes by their natural ordering so that any overlapping ranges
// from different peerings will be adjacent or near-adjacent.
prefixes.sort_by(|(a, _), (b, _)| a.cmp(b));
// Scan adjacent prefixes to detect overlaps between different peerings.
for window in prefixes.windows(2) {
let ((prefix_a, peering_a), (prefix_b, peering_b)) = (&window[0], &window[1]);
// Only enforce no-overlap across *different* peerings, matching the
// previous implementation's semantics.
if peering_a != peering_b && prefix_a.overlaps(prefix_b) {
error!(
"Prefixes exposed to VPC {} overlap: {} and {}",
self.name, prefix_a, prefix_b
);
return Err(ConfigError::OverlappingPrefixes(*prefix_a, *prefix_b));
}
}

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shhhhh don't tell Manish, he'll make nightmares about it!

Base automatically changed from pr/qmonnet/handle-default to pr/fredi/expose_refined January 19, 2026 21:01
@qmonnet qmonnet force-pushed the pr/qmonnet/expose_refined_more branch from a976833 to 6832a8c Compare January 19, 2026 21:10
Make sure that we do not expose overlapping prefixes to a given VPC. The
only supported exception is when one of the expose is a `default` expose
destination.

Note: The implementation is currently rather inefficient as we have
seven nested loops (counting the loop on VPCs for validation). We should
look into some structure where to insert prefixes as we encounter them,
like for check_peering_count(), but it's not trivial to identify
overlapping prefixes (accounting for both IP prefixes and port ranges)
in a structure without looping on them anyway, so we stick to the naive
implementation for now.

Signed-off-by: Quentin Monnet <qmo@qmon.net>
@qmonnet qmonnet force-pushed the pr/qmonnet/expose_refined_more branch from 6832a8c to 3f933d2 Compare January 19, 2026 21:11
We decided to reject, for now at least, configurations where multiple
"default" VpcExpose objects are exposed to a single VPC. This commit
adjusts validation accordingly, and add the related unit tests.

Also reject the configuration if a peering (not a VPC) contains multiple
"default" expose blocks.

Signed-off-by: Quentin Monnet <qmo@qmon.net>
@qmonnet qmonnet force-pushed the pr/qmonnet/expose_refined_more branch from 3f933d2 to 8275ebc Compare January 19, 2026 21:12
@qmonnet qmonnet merged commit d1222c7 into pr/fredi/expose_refined Jan 19, 2026
22 checks passed
@qmonnet qmonnet deleted the pr/qmonnet/expose_refined_more branch January 19, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants