From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 797781FF13A for ; Wed, 15 Apr 2026 13:11:59 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 26837F288; Wed, 15 Apr 2026 13:11:54 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs v2 1/7] sdn: fabric: add BGP protocol support Date: Wed, 15 Apr 2026 13:11:28 +0200 Message-ID: <20260415111134.124720-2-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260415111134.124720-1-h.laimer@proxmox.com> References: <20260415111134.124720-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1776251421225 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.084 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 6SJ2R3FLJY5E2VOD3BQS4EI3EBHU2IMU X-Message-ID-Hash: 6SJ2R3FLJY5E2VOD3BQS4EI3EBHU2IMU X-MailFrom: h.laimer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: From: Stefan Hanreich Add BGP as a fabric protocol for eBGP unnumbered underlays. Each node has a mandatory, globally unique ASN for interface-based eBGP peering. Unlike OSPF and OpenFabric, BGP does not have its own FRR daemon - the fabric config needs to coexist with EVPN in a single 'router bgp' block. To handle this, the fabric merges into an existing router rather than replacing it, using local-as to present the per-node ASN to underlay peers when the router already runs under the EVPN ASN. For IPv6-only nodes, the BGP router-id is derived from the IPv6 address using FNV-1a, since router-id must be a 32-bit value. Co-authored-by: Hannes Laimer Signed-off-by: Stefan Hanreich Signed-off-by: Hannes Laimer --- proxmox-frr/src/ser/bgp.rs | 85 +++++- proxmox-ve-config/src/sdn/fabric/frr.rs | 254 +++++++++++++++- proxmox-ve-config/src/sdn/fabric/mod.rs | 111 +++++++ .../src/sdn/fabric/section_config/fabric.rs | 22 ++ .../src/sdn/fabric/section_config/mod.rs | 19 ++ .../src/sdn/fabric/section_config/node.rs | 21 ++ .../sdn/fabric/section_config/protocol/bgp.rs | 286 ++++++++++++++++++ .../sdn/fabric/section_config/protocol/mod.rs | 1 + .../tests/fabric/cfg/bgp_default/fabrics.cfg | 17 ++ .../fabric/cfg/bgp_ipv6_only/fabrics.cfg | 17 ++ proxmox-ve-config/tests/fabric/main.rs | 119 +++++++- .../snapshots/fabric__bgp_default_pve.snap | 28 ++ .../snapshots/fabric__bgp_default_pve1.snap | 27 ++ .../snapshots/fabric__bgp_ipv6_only_pve.snap | 29 ++ .../snapshots/fabric__bgp_ipv6_only_pve1.snap | 28 ++ .../fabric__bgp_merge_with_evpn_pve.snap | 34 +++ 16 files changed, 1089 insertions(+), 9 deletions(-) create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs index 0bf4a1d..5d454fa 100644 --- a/proxmox-frr/src/ser/bgp.rs +++ b/proxmox-frr/src/ser/bgp.rs @@ -43,9 +43,9 @@ pub enum LocalAsFlags { #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub struct LocalAsSettings { - asn: u32, + pub asn: u32, #[serde(default)] - mode: Option, + pub mode: Option, } #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] @@ -159,9 +159,84 @@ pub struct CommonAddressFamilyOptions { #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)] pub struct AddressFamilies { - ipv4_unicast: Option, - ipv6_unicast: Option, - l2vpn_evpn: Option, + pub ipv4_unicast: Option, + pub ipv6_unicast: Option, + pub l2vpn_evpn: Option, +} + +impl AddressFamilies { + /// Merge another [`AddressFamilies`] into this one. + /// + /// For each address family: if `self` already has it, extend its neighbors, networks, and + /// redistribute lists. If `self` doesn't have it, take it from `other`. + pub fn merge(&mut self, other: AddressFamilies) { + match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) { + (Some(existing), Some(incoming)) => { + existing + .common_options + .neighbors + .extend(incoming.common_options.neighbors); + existing + .common_options + .import_vrf + .extend(incoming.common_options.import_vrf); + existing + .common_options + .custom_frr_config + .extend(incoming.common_options.custom_frr_config); + existing.networks.extend(incoming.networks); + existing.redistribute.extend(incoming.redistribute); + } + (None, Some(incoming)) => { + self.ipv4_unicast = Some(incoming); + } + _ => {} + } + + match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) { + (Some(existing), Some(incoming)) => { + existing + .common_options + .neighbors + .extend(incoming.common_options.neighbors); + existing + .common_options + .import_vrf + .extend(incoming.common_options.import_vrf); + existing + .common_options + .custom_frr_config + .extend(incoming.common_options.custom_frr_config); + existing.networks.extend(incoming.networks); + existing.redistribute.extend(incoming.redistribute); + } + (None, Some(incoming)) => { + self.ipv6_unicast = Some(incoming); + } + _ => {} + } + + // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this) + if self.l2vpn_evpn.is_none() { + self.l2vpn_evpn = other.l2vpn_evpn; + } + } +} + +impl BgpRouter { + /// Merge a fabric-generated [`BgpRouter`] into an existing one. + /// + /// Appends the fabric's neighbor groups and merges address families. Keeps the existing + /// router's ASN, router-id, and other top-level settings. The caller is responsible for + /// setting `local_as` on the fabric's neighbor group if the ASNs differ. + pub fn merge_fabric(&mut self, other: BgpRouter) { + self.neighbor_groups.extend(other.neighbor_groups); + self.address_families.merge(other.address_families); + + if self.default_ipv4_unicast.is_none() { + self.default_ipv4_unicast = other.default_ipv4_unicast; + } + } } #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs index b816ef6..373df80 100644 --- a/proxmox-ve-config/src/sdn/fabric/frr.rs +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs @@ -2,19 +2,28 @@ use std::net::{IpAddr, Ipv4Addr}; use tracing; +use proxmox_frr::ser::bgp::{ + AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF, + Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs, + RedistributeProtocol, Redistribution, +}; use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName}; use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter}; use proxmox_frr::ser::route_map::{ - AccessAction, AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet, + AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet, }; +use proxmox_frr::ser::AccessAction; use proxmox_frr::ser::{ - self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, + self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, VrfName, }; -use proxmox_network_types::ip_address::Cidr; +use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr}; use proxmox_sdn_types::net::Net; use crate::common::valid::Valid; + +use crate::sdn::fabric::section_config::protocol::bgp::{router_id_from_ipv6, BgpNode}; use crate::sdn::fabric::section_config::protocol::{ + bgp::BgpRedistributionSource, openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, ospf::OspfInterfaceProperties, }; @@ -277,6 +286,214 @@ pub fn build_fabric( ); } } + FabricEntry::Bgp(bgp_entry) => { + let Ok(node) = bgp_entry.node_section(¤t_node) else { + continue; + }; + + let BgpNode::Internal(properties) = node.properties() else { + continue; + }; + + let fabric = bgp_entry.fabric_section(); + + let local_asn = properties.asn().as_u32(); + + let mut bgp_interfaces = Vec::new(); + for interface in properties.interfaces() { + bgp_interfaces.push(interface.name.as_str().try_into()?) + } + + let neighbor_group = NeighborGroup { + name: FrrWord::new(fabric.id().to_string())?, + bfd: fabric.properties().bfd(), + remote_as: NeighborRemoteAs::External, + local_as: Default::default(), + interfaces: bgp_interfaces, + ips: Default::default(), + ebgp_multihop: Default::default(), + update_source: Default::default(), + }; + + let redistribute: Vec = fabric + .properties() + .redistribute + .iter() + .map(|redistribution| Redistribution { + protocol: match redistribution.source { + BgpRedistributionSource::Ospf => RedistributeProtocol::Ospf, + BgpRedistributionSource::Connected => RedistributeProtocol::Connected, + BgpRedistributionSource::Isis => RedistributeProtocol::Isis, + BgpRedistributionSource::Kernel => RedistributeProtocol::Kernel, + BgpRedistributionSource::Openfabric => RedistributeProtocol::Openfabric, + BgpRedistributionSource::Ospf6 => RedistributeProtocol::Ospf6, + BgpRedistributionSource::Static => RedistributeProtocol::Static, + }, + metric: redistribution.metric, + route_map: redistribution.route_map.clone().map(RouteMapName::new), + }) + .collect(); + + let af_neighbors = vec![AddressFamilyNeighbor { + name: fabric.id().to_string(), + route_map_in: None, + route_map_out: None, + soft_reconfiguration_inbound: Some(true), + }]; + + let ipv4_family = node.ip().map(|ipv4| Ipv4UnicastAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Default::default(), + neighbors: af_neighbors.clone(), + custom_frr_config: Default::default(), + }, + redistribute: redistribute.clone(), + networks: vec![Ipv4Cidr::from(ipv4)], + }); + + let ipv6_family = node.ip6().map(|ipv6| Ipv6UnicastAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Default::default(), + neighbors: af_neighbors, + custom_frr_config: Default::default(), + }, + networks: vec![Ipv6Cidr::from(ipv6)], + redistribute, + }); + + let address_families = AddressFamilies { + ipv4_unicast: ipv4_family, + ipv6_unicast: ipv6_family, + ..Default::default() + }; + + let router_id = match (node.ip(), node.ip6()) { + (Some(ipv4), _) => ipv4, + (None, Some(ipv6)) => router_id_from_ipv6(&ipv6), + (None, None) => { + anyhow::bail!("BGP node must have an IPv4 or IPv6 address") + } + }; + + let mut router = BgpRouter { + asn: local_asn, + router_id, + neighbor_groups: vec![neighbor_group], + address_families, + coalesce_time: Default::default(), + default_ipv4_unicast: Some(false), + hard_administrative_reset: Default::default(), + graceful_restart_notification: Default::default(), + disable_ebgp_connected_route_check: Default::default(), + bestpath_as_path_multipath_relax: Default::default(), + custom_frr_config: Default::default(), + }; + + if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) { + // If the existing router uses a different ASN (e.g. the + // EVPN ASN), set local-as on the fabric neighbor group so + // the underlay peers see the correct per-node ASN. + if existing.asn != local_asn { + if let Some(ng) = router.neighbor_groups.first_mut() { + ng.local_as = Some(LocalAsSettings { + asn: local_asn, + mode: Some(LocalAsFlags::ReplaceAs), + }); + } + } + existing.merge_fabric(router); + } else { + frr_config.bgp.vrf_router.insert(VrfName::Default, router); + } + + // Create access-list and route-map for source address selection, + // so the kernel uses the loopback IP as source for fabric-learned routes. + if let Some(ipv4_prefix) = fabric.ip_prefix() { + let access_list_name = + AccessListName::new(format!("pve_bgp_{}_ips", fabric_id)); + + let rule = ser::route_map::AccessListRule { + action: AccessAction::Permit, + network: Cidr::from(ipv4_prefix), + is_ipv6: false, + seq: None, + }; + + frr_config.access_lists.insert(access_list_name, vec![rule]); + + let (routemap_name, routemap_rule) = build_bgp_routemap( + fabric_id, + IpAddr::from(node.ip().expect("node must have an ipv4 address")), + routemap_seq, + ); + + routemap_seq += 10; + + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(routemap_rule) + } else { + frr_config + .routemaps + .insert(routemap_name.clone(), vec![routemap_rule]); + } + + if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp) + { + routemap.v4 = Some(routemap_name); + } else { + frr_config.protocol_routemaps.insert( + FrrProtocol::Bgp, + IpProtocolRouteMap { + v4: Some(routemap_name), + v6: None, + }, + ); + } + } + + if let Some(ipv6_prefix) = fabric.ip6_prefix() { + let access_list_name = + AccessListName::new(format!("pve_bgp_{}_ip6s", fabric_id)); + + let rule = ser::route_map::AccessListRule { + action: AccessAction::Permit, + network: Cidr::from(ipv6_prefix), + is_ipv6: true, + seq: None, + }; + + frr_config.access_lists.insert(access_list_name, vec![rule]); + + let (routemap_name, routemap_rule) = build_bgp_routemap( + fabric_id, + IpAddr::from(node.ip6().expect("node must have an ipv6 address")), + routemap_seq, + ); + + routemap_seq += 10; + + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(routemap_rule) + } else { + frr_config + .routemaps + .insert(routemap_name.clone(), vec![routemap_rule]); + } + + if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp) + { + routemap.v6 = Some(routemap_name); + } else { + frr_config.protocol_routemaps.insert( + FrrProtocol::Bgp, + IpProtocolRouteMap { + v4: None, + v6: Some(routemap_name), + }, + ); + } + } + } } } @@ -416,6 +633,37 @@ fn build_openfabric_routemap( ) } +/// Helper that builds a RouteMap for the BGP protocol. +fn build_bgp_routemap( + fabric_id: &FabricId, + router_ip: IpAddr, + seq: u16, +) -> (RouteMapName, RouteMapEntry) { + let routemap_name = match router_ip { + IpAddr::V4(_) => RouteMapName::new("pve_bgp".to_owned()), + IpAddr::V6(_) => RouteMapName::new("pve_bgp6".to_owned()), + }; + ( + routemap_name, + RouteMapEntry { + seq, + action: AccessAction::Permit, + matches: vec![match router_ip { + IpAddr::V4(_) => RouteMapMatch::IpAddressAccessList(AccessListName::new(format!( + "pve_bgp_{fabric_id}_ips" + ))), + IpAddr::V6(_) => RouteMapMatch::Ip6AddressAccessList(AccessListName::new(format!( + "pve_bgp_{fabric_id}_ip6s" + ))), + }], + sets: vec![RouteMapSet::Src(router_ip)], + custom_frr_config: Vec::new(), + call: None, + exit_action: None, + }, + ) +} + /// Helper that builds a RouteMap for the OSPF protocol. fn build_ospf_dummy_routemap( fabric_id: &FabricId, diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs index 677a309..f4c134b 100644 --- a/proxmox-ve-config/src/sdn/fabric/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -20,6 +20,10 @@ use crate::sdn::fabric::section_config::node::{ api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater}, Node, NodeId, NodeSection, }; +use crate::sdn::fabric::section_config::protocol::bgp::{ + router_id_from_ipv6, BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater, + BgpProperties, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater, @@ -64,6 +68,10 @@ pub enum FabricConfigError { // this is technically possible, but we don't allow it #[error("duplicate OSPF area")] DuplicateOspfArea, + #[error("ASN {0} is already used by another BGP fabric node")] + DuplicateBgpAsn(u32), + #[error("BGP router-id collision: nodes have different IPv6 addresses but the same derived router-id {0}")] + DuplicateBgpRouterId(std::net::Ipv4Addr), #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")] OverlappingIp4Prefix(String, String, String, String), #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")] @@ -162,6 +170,33 @@ where } } +impl Entry { + /// Get the BGP fabric config. + /// + /// This method is implemented for [`Entry`], + /// so it is guaranteed that a [`FabricSection`] is returned. + pub fn fabric_section(&self) -> &FabricSection { + if let Fabric::Bgp(section) = &self.fabric { + return section; + } + + unreachable!(); + } + + /// Get the BGP node config for the given node_id. + /// + /// This method is implemented for [`Entry`], + /// so it is guaranteed that a [`NodeSection`] is returned. + /// An error is returned if the node is not found. + pub fn node_section(&self, id: &NodeId) -> Result<&NodeSection, FabricConfigError> { + if let Node::Bgp(section) = self.get_node(id)? { + return Ok(section); + } + + unreachable!(); + } +} + impl Entry { /// Get the OpenFabric fabric config. /// @@ -230,6 +265,7 @@ impl Entry { pub enum FabricEntry { Openfabric(Entry), Ospf(Entry), + Bgp(Entry), } impl FabricEntry { @@ -241,6 +277,7 @@ impl FabricEntry { entry.add_node(node_section) } (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section), + (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section), _ => Err(FabricConfigError::ProtocolMismatch), } } @@ -251,6 +288,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => entry.get_node(id), FabricEntry::Ospf(entry) => entry.get_node(id), + FabricEntry::Bgp(entry) => entry.get_node(id), } } @@ -260,6 +298,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => entry.get_node_mut(id), FabricEntry::Ospf(entry) => entry.get_node_mut(id), + FabricEntry::Bgp(entry) => entry.get_node_mut(id), } } @@ -339,6 +378,46 @@ impl FabricEntry { Ok(()) } + (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => { + let BgpNode::Internal(ref mut props) = node_section.properties else { + return Err(FabricConfigError::ProtocolMismatch); + }; + + let NodeDataUpdater:: { + ip, + ip6, + properties: BgpNodePropertiesUpdater { asn, interfaces }, + delete, + } = updater; + + if let Some(ip) = ip { + node_section.ip = Some(ip); + } + + if let Some(ip) = ip6 { + node_section.ip6 = Some(ip); + } + + if let Some(asn) = asn { + props.asn = asn; + } + + if let Some(interfaces) = interfaces { + props.interfaces = interfaces; + } + + for property in delete { + match property { + NodeDeletableProperties::Ip => node_section.ip = None, + NodeDeletableProperties::Ip6 => node_section.ip6 = None, + NodeDeletableProperties::Protocol( + BgpNodeDeletableProperties::Interfaces, + ) => props.interfaces = Vec::new(), + } + } + + Ok(()) + } _ => Err(FabricConfigError::ProtocolMismatch), } } @@ -348,6 +427,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => entry.nodes.iter(), FabricEntry::Ospf(entry) => entry.nodes.iter(), + FabricEntry::Bgp(entry) => entry.nodes.iter(), } } @@ -356,6 +436,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => entry.delete_node(id), FabricEntry::Ospf(entry) => entry.delete_node(id), + FabricEntry::Bgp(entry) => entry.delete_node(id), } } @@ -365,6 +446,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => entry.into_pair(), FabricEntry::Ospf(entry) => entry.into_pair(), + FabricEntry::Bgp(entry) => entry.into_pair(), } } @@ -373,6 +455,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => &entry.fabric, FabricEntry::Ospf(entry) => &entry.fabric, + FabricEntry::Bgp(entry) => &entry.fabric, } } @@ -381,6 +464,7 @@ impl FabricEntry { match self { FabricEntry::Openfabric(entry) => &mut entry.fabric, FabricEntry::Ospf(entry) => &mut entry.fabric, + FabricEntry::Bgp(entry) => &mut entry.fabric, } } } @@ -392,6 +476,7 @@ impl From for FabricEntry { FabricEntry::Openfabric(Entry::new(fabric_section)) } Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)), + Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)), } } } @@ -513,6 +598,8 @@ impl Validatable for FabricConfig { fn validate(&self) -> Result<(), FabricConfigError> { let mut node_interfaces = HashSet::new(); let mut ospf_area = HashSet::new(); + let mut bgp_asns = HashSet::new(); + let mut bgp_router_ids = HashSet::new(); // Check for overlapping IP prefixes across fabrics let fabrics: Vec<_> = self.fabrics.values().map(|f| f.fabric()).collect(); @@ -573,6 +660,30 @@ impl Validatable for FabricConfig { return Err(FabricConfigError::DuplicateInterface); } } + Node::Bgp(node_section) => { + if let BgpNode::Internal(props) = node_section.properties() { + if !props.interfaces().all(|interface| { + node_interfaces.insert((node_id, interface.name().as_str())) + }) { + return Err(FabricConfigError::DuplicateInterface); + } + + let asn = props.asn().as_u32(); + if !bgp_asns.insert(asn) { + return Err(FabricConfigError::DuplicateBgpAsn(asn)); + } + + // IPv6-only nodes derive router-id via hash, check for collisions + if node_section.ip().is_none() { + if let Some(ipv6) = node_section.ip6() { + let rid = router_id_from_ipv6(&ipv6); + if !bgp_router_ids.insert(rid) { + return Err(FabricConfigError::DuplicateBgpRouterId(rid)); + } + } + } + } + } } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs index 38911a6..9f41eae 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs @@ -8,6 +8,9 @@ use proxmox_schema::{ }; use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::protocol::bgp::{ + BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater, }; @@ -139,6 +142,10 @@ impl UpdaterType for FabricSection { type Updater = FabricSectionUpdater; } +impl UpdaterType for FabricSection { + type Updater = FabricSectionUpdater; +} + /// Enum containing all types of fabrics. /// /// It utilizes [`FabricSection`] to define all possible types of fabrics. For parsing the @@ -159,6 +166,7 @@ impl UpdaterType for FabricSection { pub enum Fabric { Openfabric(FabricSection), Ospf(FabricSection), + Bgp(FabricSection), } impl UpdaterType for Fabric { @@ -173,6 +181,7 @@ impl Fabric { match self { Self::Openfabric(fabric_section) => fabric_section.id(), Self::Ospf(fabric_section) => fabric_section.id(), + Self::Bgp(fabric_section) => fabric_section.id(), } } @@ -183,6 +192,7 @@ impl Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(), + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(), } } @@ -193,6 +203,7 @@ impl Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), } } @@ -203,6 +214,7 @@ impl Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(), + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(), } } @@ -213,6 +225,7 @@ impl Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), } } } @@ -225,6 +238,7 @@ impl Validatable for Fabric { match self { Fabric::Openfabric(fabric_section) => fabric_section.validate(), Fabric::Ospf(fabric_section) => fabric_section.validate(), + Fabric::Bgp(fabric_section) => fabric_section.validate(), } } } @@ -241,12 +255,19 @@ impl From> for Fabric { } } +impl From> for Fabric { + fn from(section: FabricSection) -> Self { + Fabric::Bgp(section) + } +} + /// Enum containing all updater types for fabrics #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "protocol")] pub enum FabricUpdater { Openfabric( as UpdaterType>::Updater), Ospf( as UpdaterType>::Updater), + Bgp( as UpdaterType>::Updater), } impl Updater for FabricUpdater { @@ -254,6 +275,7 @@ impl Updater for FabricUpdater { match self { FabricUpdater::Openfabric(updater) => updater.is_empty(), FabricUpdater::Ospf(updater) => updater.is_empty(), + FabricUpdater::Bgp(updater) => updater.is_empty(), } } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs index d02d4ae..698dac9 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs @@ -10,6 +10,7 @@ use crate::sdn::fabric::section_config::{ fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR}, node::{Node, NodeSection, NODE_ID_REGEX_STR}, protocol::{ + bgp::{BgpNode, BgpProperties}, openfabric::{OpenfabricNodeProperties, OpenfabricProperties}, ospf::{OspfNodeProperties, OspfProperties}, }, @@ -31,8 +32,10 @@ impl From
for FabricOrNode { match section { Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()), + Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::OpenfabricNode(node_section) => Self::Node(node_section.into()), Section::OspfNode(node_section) => Self::Node(node_section.into()), + Section::BgpNode(node_section) => Self::Node(node_section.into()), } } } @@ -62,8 +65,10 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION pub enum Section { OpenfabricFabric(FabricSection), OspfFabric(FabricSection), + BgpFabric(FabricSection), OpenfabricNode(NodeSection), OspfNode(NodeSection), + BgpNode(NodeSection), } impl From> for Section { @@ -78,6 +83,12 @@ impl From> for Section { } } +impl From> for Section { + fn from(section: FabricSection) -> Self { + Self::BgpFabric(section) + } +} + impl From> for Section { fn from(section: NodeSection) -> Self { Self::OpenfabricNode(section) @@ -90,11 +101,18 @@ impl From> for Section { } } +impl From> for Section { + fn from(section: NodeSection) -> Self { + Self::BgpNode(section) + } +} + impl From for Section { fn from(fabric: Fabric) -> Self { match fabric { Fabric::Openfabric(fabric_section) => fabric_section.into(), Fabric::Ospf(fabric_section) => fabric_section.into(), + Fabric::Bgp(fabric_section) => fabric_section.into(), } } } @@ -104,6 +122,7 @@ impl From for Section { match node { Node::Openfabric(node_section) => node_section.into(), Node::Ospf(node_section) => node_section.into(), + Node::Bgp(node_section) => node_section.into(), } } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs index 17d2f0b..8f4564c 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs @@ -10,6 +10,7 @@ use proxmox_schema::{ }; use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties}; use crate::sdn::fabric::section_config::{ fabric::{FabricId, FABRIC_ID_REGEX_STR}, protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties}, @@ -186,6 +187,7 @@ impl ApiType for NodeSection { pub enum Node { Openfabric(NodeSection), Ospf(NodeSection), + Bgp(NodeSection), } impl Node { @@ -194,6 +196,7 @@ impl Node { match self { Node::Openfabric(node_section) => node_section.id(), Node::Ospf(node_section) => node_section.id(), + Node::Bgp(node_section) => node_section.id(), } } @@ -202,6 +205,7 @@ impl Node { match self { Node::Openfabric(node_section) => node_section.ip(), Node::Ospf(node_section) => node_section.ip(), + Node::Bgp(node_section) => node_section.ip(), } } @@ -210,6 +214,7 @@ impl Node { match self { Node::Openfabric(node_section) => node_section.ip6(), Node::Ospf(node_section) => node_section.ip6(), + Node::Bgp(node_section) => node_section.ip6(), } } } @@ -221,6 +226,7 @@ impl Validatable for Node { match self { Node::Openfabric(node_section) => node_section.validate(), Node::Ospf(node_section) => node_section.validate(), + Node::Bgp(node_section) => node_section.validate(), } } } @@ -237,6 +243,12 @@ impl From> for Node { } } +impl From> for Node { + fn from(value: NodeSection) -> Self { + Self::Bgp(value) + } +} + /// API types for SDN fabric node configurations. /// /// This module provides specialized types that are used for API interactions when retrieving, @@ -258,6 +270,7 @@ pub mod api { use proxmox_schema::{Updater, UpdaterType}; use crate::sdn::fabric::section_config::protocol::{ + bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater}, openfabric::{ OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, @@ -320,6 +333,7 @@ pub mod api { pub enum Node { Openfabric(NodeData), Ospf(NodeData), + Bgp(NodeData), } impl From for Node { @@ -327,6 +341,7 @@ pub mod api { match value { super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), super::Node::Ospf(node_section) => Self::Ospf(node_section.into()), + super::Node::Bgp(node_section) => Self::Bgp(node_section.into()), } } } @@ -336,6 +351,7 @@ pub mod api { match value { Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), Node::Ospf(node_section) => Self::Ospf(node_section.into()), + Node::Bgp(node_section) => Self::Bgp(node_section.into()), } } } @@ -349,6 +365,10 @@ pub mod api { type Updater = NodeDataUpdater; } + impl UpdaterType for NodeData { + type Updater = NodeDataUpdater; + } + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeDataUpdater { #[serde(skip_serializing_if = "Option::is_none")] @@ -384,6 +404,7 @@ pub mod api { NodeDataUpdater, ), Ospf(NodeDataUpdater), + Bgp(NodeDataUpdater), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs new file mode 100644 index 0000000..9b8e5fc --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs @@ -0,0 +1,286 @@ +use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr}; +use std::ops::{Deref, DerefMut}; + +use proxmox_network_types::ip_address::api_types::Ipv4Addr; +use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType}; +use serde::{Deserialize, Serialize}; + +use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater}; + +use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::fabric::FabricSection; +use crate::sdn::fabric::section_config::interface::InterfaceName; +use crate::sdn::fabric::section_config::node::NodeSection; +use crate::sdn::fabric::FabricConfigError; + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +#[serde(rename_all = "lowercase")] +/// Redistribution Sources for BGP fabric +pub enum BgpRedistributionSource { + /// redistribute connected routes + Connected, + /// redistribute IS-IS routes + Isis, + /// redistribute kernel routes + Kernel, + /// redistribute openfabric routes + Openfabric, + /// redistribute ospfv2 routes + Ospf, + /// redistribute ospfv3 routes + Ospf6, + /// redistribute static routes + Static, +} + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// A BGP redistribution target +pub struct BgpRedistribution { + /// The source used for redistribution + pub(crate) source: BgpRedistributionSource, + /// The metric to apply to redistributed routes + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) metric: Option, + /// Route MAP to use for filtering redistributed routes + #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")] + pub(crate) route_map: Option, +} + +#[api( + type: Integer, + minimum: u32::MIN as i64, + maximum: u32::MAX as i64, +)] +#[derive(Debug, Clone, Serialize, Updater, Hash)] +/// Autonomous system number as defined by RFC 6793 +pub struct ASN(u32); + +impl<'de> Deserialize<'de> for ASN { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::{self, Visitor}; + + struct AsnVisitor; + + impl<'de> Visitor<'de> for AsnVisitor { + type Value = ASN; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a u32 or string containing a u32") + } + + fn visit_i64(self, v: i64) -> Result { + u32::try_from(v) + .map(ASN) + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) + } + + fn visit_u64(self, v: u64) -> Result { + u32::try_from(v) + .map(ASN) + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) + } + + fn visit_str(self, v: &str) -> Result { + v.parse::() + .map(ASN) + .map_err(|_| E::custom(format!("invalid ASN: {v}"))) + } + } + + deserializer.deserialize_any(AsnVisitor) + } +} + +impl UpdaterType for ASN { + type Updater = Option; +} + +impl ASN { + pub fn as_u32(&self) -> u32 { + self.0 + } +} + +#[api( + properties: { + redistribute: { + type: Array, + optional: true, + items: { + type: String, + description: "A BGP redistribution source", + format: &ApiStringFormat::PropertyString(&BgpRedistribution::API_SCHEMA), + } + } + }, +)] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// Properties for an Bgp fabric. +pub struct BgpProperties { + /// enable BFD for this fabric + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub(crate) bfd: bool, + /// redistribution configuration for this fabric + #[serde(default, skip_serializing_if = "Vec::is_empty")] + #[updater(serde(skip_serializing_if = "Option::is_none"))] + pub(crate) redistribute: Vec>, +} + +impl BgpProperties { + pub fn bfd(&self) -> bool { + self.bfd + } +} + +impl Validatable for FabricSection { + type Error = FabricConfigError; + + /// Validate the [`FabricSection`]. + fn validate(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum BgpDeletableProperties {} + +#[api] +/// External Bgp Node +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +pub struct ExternalBgpNode { + peer_ip: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +#[serde(rename_all = "snake_case", tag = "role")] +pub enum BgpNode { + Internal(BgpNodeProperties), + External(ExternalBgpNode), +} + +impl ApiType for BgpNode { + const API_SCHEMA: Schema = OneOfSchema::new( + "BGP node", + &( + "role", + false, + &StringSchema::new("internal or external").schema(), + ), + &[ + ("external", &ExternalBgpNode::API_SCHEMA), + ("internal", &BgpNodeProperties::API_SCHEMA), + ], + ) + .schema(); +} + +impl Validatable for NodeSection { + type Error = FabricConfigError; + + fn validate(&self) -> Result<(), Self::Error> { + if self.ip().is_none() && self.ip6().is_none() { + return Err(FabricConfigError::NodeNoIp(self.id().to_string())); + } + Ok(()) + } +} + +#[api( + properties: { + interfaces: { + type: Array, + optional: true, + items: { + type: String, + description: "Properties for an Bgp interface.", + format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA), + } + }, + } +)] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// Properties for an Bgp node. +pub struct BgpNodeProperties { + /// Autonomous system number for this Node + pub(crate) asn: ASN, + /// Interfaces for this Node. + #[serde(default)] + pub(crate) interfaces: Vec>, +} + +impl BgpNodeProperties { + /// Returns the ASN for this node. + pub fn asn(&self) -> &ASN { + &self.asn + } + + /// Returns an iterator over all the interfaces. + pub fn interfaces(&self) -> impl Iterator { + self.interfaces + .iter() + .map(|property_string| property_string.deref()) + } + + /// Returns an iterator over all the interfaces (mutable). + pub fn interfaces_mut(&mut self) -> impl Iterator { + self.interfaces + .iter_mut() + .map(|property_string| property_string.deref_mut()) + } +} + +impl Validatable for NodeSection { + type Error = FabricConfigError; + + /// Validate the [`NodeSection`]. + fn validate(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum BgpNodeDeletableProperties { + Interfaces, +} + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// Properties for an BGP interface. +pub struct BgpInterfaceProperties { + pub(crate) name: InterfaceName, +} + +impl BgpInterfaceProperties { + /// Get the name of the BGP interface. + pub fn name(&self) -> &InterfaceName { + &self.name + } + + /// Set the name of the interface. + pub fn set_name(&mut self, name: InterfaceName) { + self.name = name + } +} + +/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a. +/// +/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the +/// full 16 octets down to 4 bytes. Typical loopback allocations (sequential +/// within a prefix, sparse across /48s) produce zero collisions up to 100k +/// nodes in testing -- well below the random birthday bound (~1% at 10k) +/// because structured addresses spread well under FNV-1a. +pub fn router_id_from_ipv6(addr: &Ipv6Addr) -> StdIpv4Addr { + let mut hash: u32 = 0x811c9dc5; + for &byte in &addr.octets() { + hash ^= byte as u32; + hash = hash.wrapping_mul(0x01000193); + } + StdIpv4Addr::from(hash) +} diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs index c1ec847..8f918ef 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs @@ -1,2 +1,3 @@ +pub mod bgp; pub mod openfabric; pub mod ospf; diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg new file mode 100644 index 0000000..bd434a7 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg @@ -0,0 +1,17 @@ +bgp_fabric: test + bfd 0 + ip_prefix 10.10.10.0/24 + +bgp_node: test_pve + asn 65001 + interfaces name=ens18 + interfaces name=ens19 + ip 10.10.10.1 + role internal + +bgp_node: test_pve1 + asn 65002 + interfaces name=ens19 + ip 10.10.10.2 + role internal + diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg new file mode 100644 index 0000000..f4581fb --- /dev/null +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg @@ -0,0 +1,17 @@ +bgp_fabric: test + bfd 0 + ip6_prefix fd00:10::/64 + +bgp_node: test_pve + asn 65001 + interfaces name=ens18 + interfaces name=ens19 + ip6 fd00:10::1 + role internal + +bgp_node: test_pve1 + asn 65002 + interfaces name=ens19 + ip6 fd00:10::2 + role internal + diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs index 95b2e62..49c5fcc 100644 --- a/proxmox-ve-config/tests/fabric/main.rs +++ b/proxmox-ve-config/tests/fabric/main.rs @@ -1,7 +1,9 @@ #![cfg(feature = "frr")] +use std::net::Ipv4Addr; use std::str::FromStr; -use proxmox_frr::ser::{serializer::dump, FrrConfig}; +use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF}; +use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName}; use proxmox_ve_config::sdn::fabric::{ frr::build_fabric, section_config::node::NodeId, FabricConfig, }; @@ -162,3 +164,118 @@ fn openfabric_ipv6_only() { insta::assert_snapshot!(helper::reference_name!("pve"), output); } + +#[test] +fn bgp_default() { + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); + let mut frr_config = FrrConfig::default(); + + build_fabric( + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), + config.clone(), + &mut frr_config, + ) + .unwrap(); + + let mut output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); + + frr_config = FrrConfig::default(); + build_fabric( + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve1"), output); +} + +#[test] +fn bgp_ipv6_only() { + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); + let mut frr_config = FrrConfig::default(); + + build_fabric( + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), + config.clone(), + &mut frr_config, + ) + .unwrap(); + + let mut output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); + + frr_config = FrrConfig::default(); + build_fabric( + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve1"), output); +} + +/// Test that build_fabric merges into an existing EVPN router and sets local-as +/// when the ASNs differ. +#[test] +fn bgp_merge_with_evpn() { + let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg") + .expect("cannot find config file"); + let config = FabricConfig::parse_section_config(&raw).unwrap(); + + // Pre-populate with an EVPN-like router using a different ASN + let mut frr_config = FrrConfig::default(); + let evpn_router = BgpRouter { + asn: 65000, + router_id: Ipv4Addr::new(10, 10, 10, 1), + coalesce_time: Some(1000), + default_ipv4_unicast: Some(false), + hard_administrative_reset: None, + graceful_restart_notification: None, + disable_ebgp_connected_route_check: None, + bestpath_as_path_multipath_relax: None, + neighbor_groups: Vec::new(), + address_families: AddressFamilies { + ipv4_unicast: None, + ipv6_unicast: None, + l2vpn_evpn: Some(L2vpnEvpnAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Vec::new(), + neighbors: Vec::new(), + custom_frr_config: Vec::new(), + }, + advertise_all_vni: Some(true), + advertise_default_gw: None, + default_originate: Vec::new(), + advertise_ipv4_unicast: None, + advertise_ipv6_unicast: None, + autort_as: None, + route_targets: None, + }), + }, + custom_frr_config: Vec::new(), + }; + frr_config + .bgp + .vrf_router + .insert(VrfName::Default, evpn_router); + + build_fabric( + NodeId::from_str("pve").expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + let output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); +} diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap new file mode 100644 index 0000000..34aa2cc --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap @@ -0,0 +1,28 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65001 + bgp router-id 10.10.10.1 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +ip protocol bgp route-map pve_bgp diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap new file mode 100644 index 0000000..a3e6b6d --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap @@ -0,0 +1,27 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65002 + bgp router-id 10.10.10.2 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.2/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.2 +exit +! +ip protocol bgp route-map pve_bgp diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap new file mode 100644 index 0000000..f335a4f --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap @@ -0,0 +1,29 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65001 + bgp router-id 5.76.46.251 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv6 unicast + network fd00:10::1/128 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family +exit +! +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 +! +route-map pve_bgp6 permit 100 + match ipv6 address pve_bgp_test_ip6s + set src fd00:10::1 +exit +! +! +ipv6 protocol bgp route-map pve_bgp6 diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap new file mode 100644 index 0000000..67628a9 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap @@ -0,0 +1,28 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65002 + bgp router-id 6.76.48.142 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens19 interface peer-group test + ! + address-family ipv6 unicast + network fd00:10::2/128 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family +exit +! +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 +! +route-map pve_bgp6 permit 100 + match ipv6 address pve_bgp_test_ip6s + set src fd00:10::2 +exit +! +! +ipv6 protocol bgp route-map pve_bgp6 diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap new file mode 100644 index 0000000..ef57cd6 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap @@ -0,0 +1,34 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65000 + bgp router-id 10.10.10.1 + no bgp default ipv4-unicast + coalesce-time 1000 + neighbor test peer-group + neighbor test remote-as external + neighbor test local-as 65001 no-prepend replace-as + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family + ! + address-family l2vpn evpn + advertise-all-vni + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +ip protocol bgp route-map pve_bgp -- 2.47.3