From cd0a49ec3f6841ebfe307c23adbb9e990349d833 Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Wed, 14 Jan 2026 10:49:25 +0100 Subject: [PATCH 1/7] feat(k8s-intf): bump to gateway v0.15.0 Signed-off-by: Fredi Raspall --- k8s-intf/src/bolero/expose.rs | 1 + k8s-intf/src/generated/gateway_agent_crd.rs | 4 +++- scripts/k8s-crd.env | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/k8s-intf/src/bolero/expose.rs b/k8s-intf/src/bolero/expose.rs index 9199f509c..d363a86cb 100644 --- a/k8s-intf/src/bolero/expose.rs +++ b/k8s-intf/src/bolero/expose.rs @@ -126,6 +126,7 @@ impl ValueGenerator for LegalValueExposeGenerator<'_> { Some(GatewayAgentPeeringsPeeringExpose { r#as: Some(final_as).filter(|f| !f.is_empty()), ips: Some(final_ips).filter(|f| !f.is_empty()), + default: None, nat: if has_as { Some( d.produce::>()? diff --git a/k8s-intf/src/generated/gateway_agent_crd.rs b/k8s-intf/src/generated/gateway_agent_crd.rs index 9a1fa6366..06514a17c 100644 --- a/k8s-intf/src/generated/gateway_agent_crd.rs +++ b/k8s-intf/src/generated/gateway_agent_crd.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -pub const GW_API_VERSION: Option<&str> = Some("v0.34.0"); +pub const GW_API_VERSION: Option<&str> = Some("v0.35.0"); // WARNING: generated by kopium - manual changes will be overwritten // kopium command: kopium -D PartialEq -Af - @@ -170,6 +170,8 @@ pub struct GatewayAgentPeeringsPeeringExpose { #[serde(default, skip_serializing_if = "Option::is_none", rename = "as")] pub r#as: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + pub default: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub ips: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub nat: Option, diff --git a/scripts/k8s-crd.env b/scripts/k8s-crd.env index e85015a9c..54a9195b2 100644 --- a/scripts/k8s-crd.env +++ b/scripts/k8s-crd.env @@ -1,4 +1,4 @@ -K8S_GATEWAY_AGENT_REF=v0.34.0 +K8S_GATEWAY_AGENT_REF=v0.35.0 K8S_GATEWAY_AGENT_CRD_URL="https://raw.githubusercontent.com/githedgehog/gateway/${K8S_GATEWAY_AGENT_REF}/config/crd/bases/gwint.githedgehog.com_gatewayagents.yaml" # path to local CRD definitions From 04f6a42c9f0a7d58587fb591f8995b6944c7dfab Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Wed, 14 Jan 2026 13:31:25 +0100 Subject: [PATCH 2/7] feat(config): extend converter for default expose Extends the conversion from CRD to internal type to allow the support of default exposes. A default expose cannot contain any ip/nots or nat configuration. Signed-off-by: Fredi Raspall --- config/src/converters/k8s/config/expose.rs | 16 ++++++++++++++++ config/src/external/overlay/vpcpeering.rs | 1 + 2 files changed, 17 insertions(+) diff --git a/config/src/converters/k8s/config/expose.rs b/config/src/converters/k8s/config/expose.rs index 6a6dce8fb..9742a42a3 100644 --- a/config/src/converters/k8s/config/expose.rs +++ b/config/src/converters/k8s/config/expose.rs @@ -149,6 +149,22 @@ impl TryFrom<(&SubnetMap, &GatewayAgentPeeringsPeeringExpose)> for VpcExpose { ) -> Result { let mut vpc_expose = VpcExpose::empty(); + // check if it is a default expose + vpc_expose.default = expose.default.unwrap_or(false); + if vpc_expose.default { + if expose.ips.as_ref().is_some_and(|ips| !ips.is_empty()) { + return Err(FromK8sConversionError::Invalid( + "A Default expose can't contain prefixes".to_string(), + )); + } + if expose.r#as.as_ref().is_some_and(|r#as| !r#as.is_empty()) { + return Err(FromK8sConversionError::Invalid( + "A Default expose can't contain 'as' prefixes".to_string(), + )); + } + return Ok(vpc_expose); + } + // Process PeeringIP rules if let Some(ips) = expose.ips.as_ref() { if ips.is_empty() { diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index 31dfc3276..359ba93c8 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -66,6 +66,7 @@ fn empty_btreeset() -> &'static BTreeSet { use crate::{ConfigError, ConfigResult}; #[derive(Clone, Debug, Default, PartialEq)] pub struct VpcExpose { + pub default: bool, pub ips: BTreeSet, pub nots: BTreeSet, pub nat: Option, From ae9a24ce06e58c1d45255d7ce821b5eecf6eb1ce Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Wed, 14 Jan 2026 15:54:20 +0100 Subject: [PATCH 3/7] feat(config): cleanup / simplify overlay validation Since we keep at most one config, there's no need to clear intermediate collections. Also, reorganize the code so that adding validations is clearer. Signed-off-by: Fredi Raspall --- config/src/display.rs | 11 ++++++ config/src/external/overlay/mod.rs | 45 ++++++++++++----------- config/src/external/overlay/vpc.rs | 45 ++++++++++++++--------- config/src/external/overlay/vpcpeering.rs | 6 +-- 4 files changed, 63 insertions(+), 44 deletions(-) diff --git a/config/src/display.rs b/config/src/display.rs index 1d2750dfd..b6d08ee8f 100644 --- a/config/src/display.rs +++ b/config/src/display.rs @@ -8,6 +8,7 @@ use crate::external::overlay::vpc::Vpc; use std::fmt::Display; +use crate::external::overlay::Overlay; use crate::external::overlay::vpc::{Peering, VpcId, VpcTable}; use crate::external::overlay::vpcpeering::VpcManifest; use crate::external::overlay::vpcpeering::{VpcExpose, VpcPeering, VpcPeeringTable}; @@ -28,6 +29,9 @@ const SEP: &str = " "; impl Display for VpcExpose { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut carriage = false; + if self.default { + write!(f, "{SEP} prefixes: default")?; + } if !self.ips.is_empty() { write!(f, "{SEP} prefixes:")?; self.ips.iter().for_each(|x| { @@ -234,3 +238,10 @@ impl Display for VpcPeeringTable { Ok(()) } } + +impl Display for Overlay { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.vpc_table.fmt(f)?; + self.peering_table.fmt(f) + } +} diff --git a/config/src/external/overlay/mod.rs b/config/src/external/overlay/mod.rs index 59c2e6415..27c6aaf39 100644 --- a/config/src/external/overlay/mod.rs +++ b/config/src/external/overlay/mod.rs @@ -28,48 +28,51 @@ impl Overlay { peering_table, } } + /// Check if a `Vpc` referred in a peering exists fn check_peering_vpc(&self, peering: &str, manifest: &VpcManifest) -> ConfigResult { - if self.vpc_table.get_vpc(&manifest.name).is_none() { + self.vpc_table.get_vpc(&manifest.name).ok_or_else(|| { error!("peering '{}': unknown VPC '{}'", peering, manifest.name); - return Err(ConfigError::NoSuchVpc(manifest.name.clone())); - } + ConfigError::NoSuchVpc(manifest.name.clone()) + })?; Ok(()) } - pub fn validate(&mut self) -> ConfigResult { - debug!("Validating overlay configuration..."); - /* validate peerings and check if referred VPCs exist */ + /// Validate all peerings, checking if the VPCs they refer to exist in vpc table + pub fn validate_peerings(&self) -> ConfigResult { + debug!("Validating VPC peerings..."); for peering in self.peering_table.values() { - peering.validate()?; self.check_peering_vpc(&peering.name, &peering.left)?; self.check_peering_vpc(&peering.name, &peering.right)?; + peering.validate()?; } + Ok(()) + } - /* temporary map of vpc names and ids */ + /// Build a `VpcIdMap`. We have already checked that all VPC Ids are distinct + #[must_use] + pub fn vpcid_map(&self) -> VpcIdMap { let id_map: VpcIdMap = self .vpc_table .values() .map(|vpc| (vpc.name.clone(), vpc.id.clone())) .collect(); + id_map + } + + /// Top most validation function for `Overlay` configuration + pub fn validate(&mut self) -> ConfigResult { + debug!("Validating overlay configuration..."); - /* collect peerings of every VPC */ + self.validate_peerings()?; + let id_map = self.vpcid_map(); + + // collect peerings for every vpc. self.vpc_table .collect_peerings(&self.peering_table, &id_map); self.vpc_table.validate()?; - debug!( - "Overlay configuration is VALID and looks as:\n{}\n{}", - self.vpc_table, self.peering_table - ); - - /* empty peering table: we no longer need it since we have collected - all of the peerings and added them to the corresponding VPCs */ - self.peering_table.clear(); - - /* empty collections used for validation */ - self.vpc_table.clear_vnis(); - + debug!("Overlay configuration is VALID:\n{self}"); Ok(()) } } diff --git a/config/src/external/overlay/vpc.rs b/config/src/external/overlay/vpc.rs index 306c8a556..a31e6ac7f 100644 --- a/config/src/external/overlay/vpc.rs +++ b/config/src/external/overlay/vpc.rs @@ -10,7 +10,7 @@ use lpm::prefix::Prefix; use net::vxlan::Vni; use std::collections::BTreeMap; use std::collections::BTreeSet; -use tracing::{debug, warn}; +use tracing::{debug, error, warn}; use crate::external::overlay::VpcManifest; use crate::external::overlay::VpcPeeringTable; @@ -89,7 +89,7 @@ impl Vpc { } /// Collect all peerings from the [`VpcPeeringTable`] table this vpc participates in - pub fn collect_peerings(&mut self, peering_table: &VpcPeeringTable, idmap: &VpcIdMap) { + pub fn set_peerings(&mut self, peering_table: &VpcPeeringTable, idmap: &VpcIdMap) { debug!("Collecting peerings for vpc '{}'...", self.name); self.peerings = peering_table .peerings_vpc(&self.name) @@ -114,6 +114,28 @@ impl Vpc { } } + /// Check that a [`Vpc`] does not peer more than once with another. + pub fn check_peering_count(&self) -> ConfigResult { + // We use the VPC Ids to identify peer VPCs. + let mut peers = BTreeSet::new(); + for peering in &self.peerings { + if (!peers.insert(peering.remote_id.clone())) { + error!( + "VPC {} peers more than once with peer {}", + self.name, peering.remote.name + ); + return Err(ConfigError::DuplicateVpcPeerings(peering.name.clone())); + } + } + Ok(()) + } + + /// Validate a [`Vpc`] + pub fn validate(&self) -> ConfigResult { + self.check_peering_count()?; + Ok(()) + } + /// Tell how many peerings this VPC has #[must_use] pub fn num_peerings(&self) -> usize { @@ -170,6 +192,7 @@ impl VpcTable { self.vpcs.insert(vpc.name.clone(), vpc); Ok(()) } + /// Get a [`Vpc`] from the vpc table by name #[must_use] pub fn get_vpc(&self, vpc_name: &str) -> Option<&Vpc> { @@ -205,27 +228,13 @@ impl VpcTable { pub fn collect_peerings(&mut self, peering_table: &VpcPeeringTable, idmap: &VpcIdMap) { debug!("Collecting peerings for all VPCs.."); self.values_mut() - .for_each(|vpc| vpc.collect_peerings(peering_table, idmap)); - } - /// Clear set of vnis - pub fn clear_vnis(&mut self) { - self.vnis.clear(); + .for_each(|vpc| vpc.set_peerings(peering_table, idmap)); } /// Validate the [`VpcTable`] pub fn validate(&self) -> ConfigResult { for vpc in self.values() { - let mut peers = BTreeSet::new(); - // For each VPC, loop over all peerings - for peering in &vpc.peerings { - // Check whether we have duplicate remote VPCs between peerings. - // If we fail to insert, this means the remote VPC ID is already in our set, - // and we have a duplicate peering: this is a configuration error. - if (!peers.insert(peering.remote_id.clone())) { - return Err(ConfigError::DuplicateVpcPeerings(peering.name.clone())); - } - } - peers.clear(); + vpc.validate()?; } Ok(()) } diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index 359ba93c8..8f09183be 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -562,11 +562,7 @@ impl VpcPeeringTable { pub fn is_empty(&self) -> bool { self.0.is_empty() } - /// Empty a [`VpcPeeringTable`] - pub fn clear(&mut self) { - debug!("Emptying peering table..."); - self.0.clear(); - } + /// Add a [`VpcPeering`] to a [`VpcPeeringTable`] pub fn add(&mut self, peering: VpcPeering) -> ConfigResult { if peering.name.is_empty() { From 85b9fd6dab01eb0e86fc5d3157d308784f198c71 Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Wed, 14 Jan 2026 19:05:23 +0100 Subject: [PATCH 4/7] feat(config): deny configs with default - Forbid prefixes 0/0 or ::/0 in ip/nat/nots/as-not's in exposes - Do not allow default exposes to have ip/nat/nots/not-as Signed-off-by: Fredi Raspall --- config/src/external/overlay/vpcpeering.rs | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index 8f09183be..f28ca16b0 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -279,6 +279,40 @@ impl VpcExpose { self.nat.as_ref().is_some_and(VpcExposeNat::is_stateless) } + fn validate_default_expose(&self) -> ConfigResult { + if self.default { + if !self.ips.is_empty() || !self.nots.is_empty() || self.nat.is_some() { + return Err(ConfigError::Invalid( + "Default expose cannot have ips/nots or nat configuration".to_string(), + )); + } + } else { + if self.ips.iter().any(|p| p.prefix().is_root()) { + return Err(ConfigError::Forbidden( + "Expose: root prefix as 'ip' forbidden", + )); + } + if self.nots.iter().any(|p| p.prefix().is_root()) { + return Err(ConfigError::Forbidden( + "Expose: root prefix as 'not' is forbidden", + )); + } + if let Some(nat) = &self.nat { + if nat.as_range.iter().any(|p| p.prefix().is_root()) { + return Err(ConfigError::Forbidden( + "Expose: root prefix as NAT 'as' is forbidden", + )); + } + if nat.not_as.iter().any(|p| p.prefix().is_root()) { + return Err(ConfigError::Forbidden( + "Expose: root prefix as NAT 'as-not' is forbidden", + )); + } + } + } + Ok(()) + } + /// Validate the [`VpcExpose`]: /// /// 1. Make sure that all prefixes and exclusion prefixes for this [`VpcExpose`] are of the same @@ -293,6 +327,9 @@ impl VpcExpose { /// 5. Make sure we have the same number of addresses available on each side (public/private), /// taking exclusion prefixes into account. pub fn validate(&self) -> ConfigResult { + // 0. Check default exposes and prefixes + self.validate_default_expose()?; + // 1. Static NAT: Check that all prefixes in a list are of the same IP version, as we don't // support NAT46 or NAT64 at the moment. // @@ -584,6 +621,7 @@ impl VpcPeeringTable { Ok(()) } } + /// Iterate over all [`VpcPeering`]s in a [`VpcPeeringTable`] pub fn values(&self) -> impl Iterator { self.0.values() From ac1306dcc1a60fc05ff0fad99547fd890c23b58a Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Wed, 14 Jan 2026 19:10:45 +0100 Subject: [PATCH 5/7] feat(config): reorg validation of peerings Reorganize code so that we validate the `Peering` objects collected in Vpcs instead of the undirected `VpcPeering` objects learnt from the CRD. Signed-off-by: Fredi Raspall --- config/src/external/overlay/mod.rs | 1 - config/src/external/overlay/vpc.rs | 30 ++++++++++++++++++++++- config/src/external/overlay/vpcpeering.rs | 4 +-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/config/src/external/overlay/mod.rs b/config/src/external/overlay/mod.rs index 27c6aaf39..0ca4dea09 100644 --- a/config/src/external/overlay/mod.rs +++ b/config/src/external/overlay/mod.rs @@ -43,7 +43,6 @@ impl Overlay { for peering in self.peering_table.values() { self.check_peering_vpc(&peering.name, &peering.left)?; self.check_peering_vpc(&peering.name, &peering.right)?; - peering.validate()?; } Ok(()) } diff --git a/config/src/external/overlay/vpc.rs b/config/src/external/overlay/vpc.rs index a31e6ac7f..314398749 100644 --- a/config/src/external/overlay/vpc.rs +++ b/config/src/external/overlay/vpc.rs @@ -12,6 +12,7 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use tracing::{debug, error, warn}; +use crate::converters::k8s::config::peering; use crate::external::overlay::VpcManifest; use crate::external::overlay::VpcPeeringTable; use crate::internal::interfaces::interface::{InterfaceConfig, InterfaceConfigTable}; @@ -33,6 +34,21 @@ pub struct Peering { pub adv_communities: Vec, /* communities with which to advertise prefixes in this peering */ } +impl Peering { + fn validate(&self) -> ConfigResult { + debug!( + "Validating manifest of VPC {} in peering {}", + self.local.name, self.name + ); + self.local.validate()?; + if false { + // not needed will be validated when validating the remote vpc + self.remote.validate()?; + } + Ok(()) + } +} + #[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq)] /// Type for a fixed-sized VPC unique id pub struct VpcId(pub(crate) [char; 5]); @@ -115,7 +131,8 @@ impl Vpc { } /// Check that a [`Vpc`] does not peer more than once with another. - pub fn check_peering_count(&self) -> ConfigResult { + fn check_peering_count(&self) -> ConfigResult { + debug!("Checking peering duplicates for for VPC {}...", self.name); // We use the VPC Ids to identify peer VPCs. let mut peers = BTreeSet::new(); for peering in &self.peerings { @@ -130,9 +147,20 @@ impl Vpc { Ok(()) } + /// Check the peerings that a VPC participates in + fn check_peerings(&self) -> ConfigResult { + debug!("Checking peerings of VPC {}...", self.name); + for peering in &self.peerings { + peering.validate()?; + } + Ok(()) + } + /// Validate a [`Vpc`] pub fn validate(&self) -> ConfigResult { + debug!("Validating config for VPC {}...", self.name); self.check_peering_count()?; + self.check_peerings()?; Ok(()) } diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index f28ca16b0..2f0975a73 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -7,7 +7,6 @@ use lpm::prefix::{IpRangeWithPorts, Prefix, PrefixWithOptionalPorts, PrefixWithP use std::collections::{BTreeMap, BTreeSet}; use std::ops::Bound::{Excluded, Unbounded}; use std::time::Duration; -use tracing::debug; #[derive(Clone, Debug, Default, PartialEq)] pub struct VpcExposeStatelessNat; @@ -564,8 +563,9 @@ impl VpcPeering { gw_group, } } + #[cfg(test)] + /// Validate A VpcPeering. Only used in tests. Dataplane validates `Peerings` pub fn validate(&self) -> ConfigResult { - debug!("Validating VPC peering '{}'...", &self.name); self.left.validate()?; self.right.validate()?; Ok(()) From 61529899e9d3bb9e857ab694db7409015c9b0256 Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Thu, 15 Jan 2026 15:24:13 +0100 Subject: [PATCH 6/7] feat(config): method to adv prefixes of expose Adds a method to return the set of prefixes that should be advertised for an expose. Signed-off-by: Fredi Raspall --- config/src/external/overlay/vpcpeering.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/src/external/overlay/vpcpeering.rs b/config/src/external/overlay/vpcpeering.rs index 2f0975a73..efdc57b92 100644 --- a/config/src/external/overlay/vpcpeering.rs +++ b/config/src/external/overlay/vpcpeering.rs @@ -193,6 +193,20 @@ impl VpcExpose { pub fn has_host_prefixes(&self) -> bool { self.ips.iter().filter(|p| p.prefix().is_host()).count() > 0 } + + /// The prefixes of an expose to be advertised to a remote peer + #[must_use] + pub fn adv_prefixes(&self) -> Vec { + if self.default { + // only V4 atm + vec![Prefix::root_v4()] + } else if let Some(nat) = self.nat.as_ref() { + nat.as_range.iter().map(|p| p.prefix()).collect::>() + } else { + self.ips.iter().map(|p| p.prefix()).collect::>() + } + } + // If the as_range list is empty, then there's no NAT required for the expose, meaning that the // public IPs are those from the "ips" list. This method returns the current list of public IPs // for the VpcExpose. @@ -207,6 +221,7 @@ impl VpcExpose { &nat.as_range } } + // Same as public_ips, but returns the list of excluded prefixes #[must_use] pub fn public_excludes(&self) -> &BTreeSet { From cfe789682db05c5c9c63457cae33fa065ed7f790 Mon Sep 17 00:00:00 2001 From: Fredi Raspall Date: Thu, 15 Jan 2026 15:25:46 +0100 Subject: [PATCH 7/7] feat(mgmt): augment routing config for default exposes Adapt the logic to determine prefixes to be advertised for a given peering expose. Signed-off-by: Fredi Raspall --- mgmt/src/processor/confbuild/internal.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index e6c7b46f4..008e1b57c 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -170,11 +170,8 @@ impl VpcRoutingConfigIpv4 { } /* advertise */ - let nets = rmanifest.exposes.iter().flat_map(|e| { - e.public_ips() - .iter() - .map(|prefix_with_ports| prefix_with_ports.prefix()) - }); + let nets = rmanifest.exposes.iter().flat_map(|e| e.adv_prefixes()); + self.adv_nets.extend(nets); /* build adv prefix list and route-map */ @@ -184,13 +181,9 @@ impl VpcRoutingConfigIpv4 { Some(vpc.adv_plist_desc(&rmanifest.name)), ); for expose in rmanifest.exposes.iter() { - let prefixes = expose.public_ips().iter(); - let plists = prefixes.map(|prefix_with_ports| { - PrefixListEntry::new( - PrefixListAction::Permit, - PrefixListPrefix::Prefix(prefix_with_ports.prefix()), - None, - ) + let prefixes = expose.adv_prefixes().into_iter(); + let plists = prefixes.map(|p| { + PrefixListEntry::new(PrefixListAction::Permit, PrefixListPrefix::Prefix(p), None) }); adv_plist.add_entries(plists)?; }