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 6D2E51FF13A for ; Wed, 13 May 2026 14:29:47 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id EC3C1BF4D; Wed, 13 May 2026 14:29:43 +0200 (CEST) Message-ID: Date: Wed, 13 May 2026 14:29:29 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support To: pve-devel@lists.proxmox.com References: <20260512141305.199664-1-h.laimer@proxmox.com> <20260512141305.199664-2-h.laimer@proxmox.com> Content-Language: en-US From: Stefan Hanreich In-Reply-To: <20260512141305.199664-2-h.laimer@proxmox.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.612 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [fabric.rs,mod.rs,bgp.rs,frr.rs,node.rs] Message-ID-Hash: APGII4WJOGP7VOCIV6EJJ4WFDO4G3XFM X-Message-ID-Hash: APGII4WJOGP7VOCIV6EJJ4WFDO4G3XFM X-MailFrom: s.hanreich@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: On 5/12/26 4:12 PM, Hannes Laimer wrote: > +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) { maybe extend is the better name, as it mirrors existing conventions from std::Vec? > + 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 40e346f..d70d5aa 100644 > --- a/proxmox-ve-config/src/sdn/fabric/frr.rs > +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs > @@ -2,15 +2,25 @@ 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, OspfRedistribution, OspfRouter}; > -use proxmox_frr::ser::route_map::{AccessListName, RouteMapEntry, RouteMapMatch, RouteMapSet}; > -use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName}; > -use proxmox_network_types::ip_address::Cidr; > +use proxmox_frr::ser::route_map::{ > + AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet, > +}; > +use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, VrfName}; > +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::{bgp_router_id, BgpNode}; > use crate::sdn::fabric::section_config::protocol::{ > + bgp::BgpRedistributionSource, > openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, > ospf::OspfInterfaceProperties, > }; > @@ -289,6 +299,294 @@ pub fn build_fabric( > protocol_routemap.v4 = Some(routemap_name); > } > FabricEntry::WireGuard(_) => {} // not a frr 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(); makes me wonder if implementing AsRef would simplify handling ASN throughout the code? > + > + 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::from), > + }) > + .collect(); > + > + let mut address_families = AddressFamilies::default(); > + > + if let Some(ip) = node.ip() { > + // Build the prefix matcher (route_filter prefix-list, or > + // an auto access-list from ip_prefix). Reused for both the > + // pve_bgp set-src clause and the per-peer inbound filter > + // below, so they stay in sync. > + let inbound_matchers: Vec = > + if let Some(prefix_list_id) = &fabric.properties().route_filter { > + vec![RouteMapMatch::IpAddressPrefixList( > + prefix_list_id.clone().into(), > + )] > + } else if let Some(cidr) = fabric.ip_prefix() { > + let access_list_name = > + AccessListName::new(format!("pve_bgp_{fabric_id}_ips")); > + > + let rule = ser::route_map::AccessListRule { > + action: ser::route_map::AccessAction::Permit, > + network: Cidr::from(cidr), > + is_ipv6: false, > + seq: None, > + }; > + > + frr_config > + .access_lists > + .insert(access_list_name.clone(), vec![rule]); > + > + vec![RouteMapMatch::IpAddressAccessList(access_list_name)] > + } else { > + Vec::new() > + }; > + > + // Per-peer inbound filter: permit prefixes matching the fabric's filter, > + // implicit deny everything else. Stops a misbehaving fabric peer from leaking > + // prefixes outside its declared range into BGP at all. If the user configured > + // a custom route_map_in, it is chained via FRR's `call` action so it only sees > + // prefixes that already passed the fabric-prefix filter. > + let auto_in_routemap = if !inbound_matchers.is_empty() { > + let name = > + ser::route_map::RouteMapName::new(format!("pve_bgp_{fabric_id}_in")); > + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); > + in_routemap.push(RouteMapEntry { > + seq: 10, > + action: ser::route_map::AccessAction::Permit, > + matches: inbound_matchers.clone(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: fabric > + .properties() > + .route_map_in > + .clone() > + .map(RouteMapName::from), > + exit_action: None, > + }); > + Some(name) > + } else { > + None > + }; > + > + address_families.ipv4_unicast = Some(Ipv4UnicastAF { > + common_options: CommonAddressFamilyOptions { > + import_vrf: Default::default(), > + neighbors: vec![AddressFamilyNeighbor { > + name: fabric.id().to_string(), > + route_map_in: auto_in_routemap, > + route_map_out: fabric > + .properties() > + .route_map_out > + .clone() > + .map(RouteMapName::from), > + soft_reconfiguration_inbound: Some(true), > + }], > + custom_frr_config: Default::default(), > + }, > + redistribute: redistribute.clone(), > + networks: vec![Ipv4Cidr::from(ip)], > + }); > + > + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp".to_owned()); > + let routemap = frr_config > + .routemaps > + .entry(routemap_name.clone()) > + .or_default(); > + > + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); > + routemap_seq += 10; > + routemap_entry.matches = inbound_matchers; > + > + routemap.push(routemap_entry); > + > + let protocol_routemap = frr_config > + .protocol_routemaps > + .entry(FrrProtocol::Bgp) > + .or_default(); > + > + protocol_routemap.v4 = Some(routemap_name); > + } > + > + if let Some(ip) = node.ip6() { > + let inbound_matchers: Vec = > + if let Some(prefix_list_id) = &fabric.properties().route_filter { > + vec![RouteMapMatch::Ip6AddressPrefixList( > + prefix_list_id.clone().into(), > + )] > + } else if let Some(cidr) = fabric.ip6_prefix() { > + let access_list_name = > + AccessListName::new(format!("pve_bgp_{fabric_id}_ip6s")); > + > + let rule = ser::route_map::AccessListRule { > + action: ser::route_map::AccessAction::Permit, > + network: Cidr::from(cidr), > + is_ipv6: true, > + seq: None, > + }; > + > + frr_config > + .access_lists > + .insert(access_list_name.clone(), vec![rule]); > + > + vec![RouteMapMatch::Ip6AddressAccessList(access_list_name)] > + } else { > + Vec::new() > + }; > + > + let auto_in_routemap = if !inbound_matchers.is_empty() { > + let name = > + ser::route_map::RouteMapName::new(format!("pve_bgp6_{fabric_id}_in")); > + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); > + in_routemap.push(RouteMapEntry { > + seq: 10, > + action: ser::route_map::AccessAction::Permit, > + matches: inbound_matchers.clone(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: fabric > + .properties() > + .route_map_in > + .clone() > + .map(RouteMapName::from), > + exit_action: None, > + }); > + Some(name) > + } else { > + None > + }; > + > + address_families.ipv6_unicast = Some(Ipv6UnicastAF { > + common_options: CommonAddressFamilyOptions { > + import_vrf: Default::default(), > + neighbors: vec![AddressFamilyNeighbor { > + name: fabric.id().to_string(), > + route_map_in: auto_in_routemap, > + route_map_out: fabric > + .properties() > + .route_map_out > + .clone() > + .map(RouteMapName::from), > + soft_reconfiguration_inbound: Some(true), > + }], > + custom_frr_config: Default::default(), > + }, > + networks: vec![Ipv6Cidr::from(ip)], > + redistribute, > + }); > + > + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp6".to_owned()); > + let routemap = frr_config > + .routemaps > + .entry(routemap_name.clone()) > + .or_default(); > + > + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); > + routemap_seq += 10; > + routemap_entry.matches = inbound_matchers; > + > + routemap.push(routemap_entry); > + > + let protocol_routemap = frr_config > + .protocol_routemaps > + .entry(FrrProtocol::Bgp) > + .or_default(); > + > + protocol_routemap.v6 = Some(routemap_name); > + }; > + > + let router_id = bgp_router_id(&node) > + .ok_or_else(|| anyhow::anyhow!("BGP node must have ip or ip6 set"))?; > + > + 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); > + } > + } > + } > + } > + > + // Append a trailing permit-all to the BGP route-maps so non-fabric BGP > + // routes (e.g. EVPN-imported VRF routes) reach the kernel unchanged. > + // Without this, the implicit deny at the end of the route-map would drop > + // them. > + for routemap_name in [ > + ser::route_map::RouteMapName::new("pve_bgp".to_owned()), > + ser::route_map::RouteMapName::new("pve_bgp6".to_owned()), > + ] { > + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { > + routemap.push(RouteMapEntry { > + seq: 65535, > + action: ser::route_map::AccessAction::Permit, > + matches: Vec::new(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: None, > + exit_action: None, > + }); > } > } > > diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs > index 5c20ec8..b21d335 100644 > --- a/proxmox-ve-config/src/sdn/fabric/mod.rs > +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs > @@ -2,7 +2,7 @@ > pub mod frr; > pub mod section_config; > > -use std::collections::{BTreeMap, HashSet}; > +use std::collections::{BTreeMap, HashMap, HashSet}; > use std::marker::PhantomData; > use std::ops::Deref; > > @@ -21,6 +21,10 @@ use crate::sdn::fabric::section_config::node::{ > api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater}, > Node, NodeId, NodeSection, > }; > +use crate::sdn::fabric::section_config::protocol::bgp::{ > + bgp_router_id, BgpDeletableProperties, BgpNode, BgpNodeDeletableProperties, > + BgpNodePropertiesUpdater, BgpProperties, BgpPropertiesUpdater, > +}; > use crate::sdn::fabric::section_config::protocol::openfabric::{ > OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, > OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater, > @@ -69,6 +73,8 @@ pub enum FabricConfigError { > // this is technically possible, but we don't allow it > #[error("duplicate OSPF area")] > DuplicateOspfArea, > + #[error("BGP router-id collision: multiple nodes resolve to the same 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}'")] > @@ -201,6 +207,7 @@ macro_rules! impl_entry { > impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties); > impl_entry!(Ospf, OspfProperties, OspfNodeProperties); > impl_entry!(WireGuard, WireGuardProperties, WireGuardNode); > +impl_entry!(Bgp, BgpProperties, BgpNode); > > /// All possible entries in a [`FabricConfig`]. > /// > @@ -211,6 +218,7 @@ pub enum FabricEntry { > Openfabric(Entry), > Ospf(Entry), > WireGuard(Entry), > + Bgp(Entry), > } > > impl FabricEntry { > @@ -225,6 +233,7 @@ impl FabricEntry { > (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => { > entry.add_node(node_section) > } > + (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section), > _ => Err(FabricConfigError::ProtocolMismatch), > } > } > @@ -236,6 +245,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.get_node(id), > FabricEntry::Ospf(entry) => entry.get_node(id), > FabricEntry::WireGuard(entry) => entry.get_node(id), > + FabricEntry::Bgp(entry) => entry.get_node(id), > } > } > > @@ -246,6 +256,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.get_node_mut(id), > FabricEntry::Ospf(entry) => entry.get_node_mut(id), > FabricEntry::WireGuard(entry) => entry.get_node_mut(id), > + FabricEntry::Bgp(entry) => entry.get_node_mut(id), > } > } > > @@ -392,6 +403,8 @@ impl FabricEntry { > _ => continue, > } > } > + > + Ok(()) > } > ( > WireGuardNode::External(external_wire_guard_node), > @@ -422,8 +435,48 @@ impl FabricEntry { > _ => continue, > } > } > + > + Ok(()) > + } > + _ => Err(FabricConfigError::ProtocolMismatch), > + } > + } > + (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(), > } > - _ => return Err(FabricConfigError::ProtocolMismatch), > } > > Ok(()) > @@ -438,6 +491,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.nodes.iter(), > FabricEntry::Ospf(entry) => entry.nodes.iter(), > FabricEntry::WireGuard(entry) => entry.nodes.iter(), > + FabricEntry::Bgp(entry) => entry.nodes.iter(), > } > } > > @@ -447,6 +501,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.delete_node(id), > FabricEntry::Ospf(entry) => entry.delete_node(id), > FabricEntry::WireGuard(entry) => entry.delete_node(id), > + FabricEntry::Bgp(entry) => entry.delete_node(id), > } > } > > @@ -457,6 +512,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.into_pair(), > FabricEntry::Ospf(entry) => entry.into_pair(), > FabricEntry::WireGuard(entry) => entry.into_pair(), > + FabricEntry::Bgp(entry) => entry.into_pair(), > } > } > > @@ -466,6 +522,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => &entry.fabric, > FabricEntry::Ospf(entry) => &entry.fabric, > FabricEntry::WireGuard(entry) => &entry.fabric, > + FabricEntry::Bgp(entry) => &entry.fabric, > } > } > > @@ -475,6 +532,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => &mut entry.fabric, > FabricEntry::Ospf(entry) => &mut entry.fabric, > FabricEntry::WireGuard(entry) => &mut entry.fabric, > + FabricEntry::Bgp(entry) => &mut entry.fabric, > } > } > } > @@ -487,6 +545,7 @@ impl From for FabricEntry { > } > Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)), > Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)), > + Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)), > } > } > } > @@ -500,6 +559,8 @@ impl Validatable for FabricEntry { > /// - Node IP addresses are within their respective fabric IP prefix ranges > /// - IP addresses are unique across all nodes in the fabric > /// - Each node passes its own validation checks > + /// - For BGP fabrics, derived router-ids are unique across nodes (catches > + /// FNV-1a hash collisions for IPv6-only nodes) > fn validate(&self) -> Result<(), FabricConfigError> { > let fabric = self.fabric(); > > @@ -607,6 +668,27 @@ impl Validatable for FabricEntry { > } > } > > + // Per-node IPs are unique by the checks above. Router-ids can still > + // collide when at least one node falls back to FNV-1a on its IPv6 > + // address (the hash is 32 bits wide, so two distinct IPv6 addresses > + // can map to the same router-id). > + if let FabricEntry::Bgp(bgp_entry) = self { > + let mut seen_router_ids: HashMap = HashMap::new(); > + for (node_id, node) in &bgp_entry.nodes { > + let Node::Bgp(node_section) = node else { > + continue; > + }; > + if !matches!(node_section.properties(), BgpNode::Internal(_)) { > + continue; > + } > + if let Some(router_id) = bgp_router_id(node_section) { > + if seen_router_ids.insert(router_id, node_id).is_some() { > + return Err(FabricConfigError::DuplicateBgpRouterId(router_id)); > + } > + } > + } > + } > + > fabric.validate() > } > } > @@ -712,6 +794,15 @@ impl Validatable for FabricConfig { > } > } > } > + 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); > + } > + } > + } > } > } > > @@ -939,6 +1030,80 @@ impl FabricConfig { > > Ok(()) > } > + (Fabric::Bgp(fabric_section), FabricUpdater::Bgp(updater)) => { > + let FabricSectionUpdater:: { > + ip_prefix, > + ip6_prefix, > + properties: > + BgpPropertiesUpdater { > + bfd, > + redistribute, > + route_map_in, > + route_map_out, > + route_filter, > + }, > + delete, > + } = updater; > + > + if let Some(prefix) = ip_prefix { > + fabric_section.ip_prefix = Some(prefix); > + } > + > + if let Some(prefix) = ip6_prefix { > + fabric_section.ip6_prefix = Some(prefix); > + } > + > + if let Some(bfd) = bfd { > + fabric_section.properties.bfd = bfd; > + } > + > + if let Some(redistribute) = redistribute { > + fabric_section.properties.redistribute = redistribute; > + } > + > + if let Some(route_map_in) = route_map_in { > + fabric_section.properties.route_map_in = Some(route_map_in); > + } > + > + if let Some(route_map_out) = route_map_out { > + fabric_section.properties.route_map_out = Some(route_map_out); > + } > + > + if let Some(route_filter) = route_filter { > + fabric_section.properties.route_filter = Some(route_filter); > + } > + > + for property in delete { > + match property { > + FabricDeletableProperties::IpPrefix => { > + fabric_section.ip_prefix = None; > + } > + FabricDeletableProperties::Ip6Prefix => { > + fabric_section.ip6_prefix = None; > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::Redistribute, > + ) => { > + fabric_section.properties.redistribute = Vec::new(); > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::RouteFilter, > + ) => { > + fabric_section.properties.route_filter = None; > + } > + FabricDeletableProperties::Protocol(BgpDeletableProperties::RouteMapIn) => { > + fabric_section.properties.route_map_in = None; > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::RouteMapOut, > + ) => { > + fabric_section.properties.route_map_out = None; > + } > + } > + } > + > + Ok(()) > + } > _ => Err(FabricConfigError::ProtocolMismatch), > } > } > 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 e92074c..efa186a 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, > }; > @@ -147,6 +150,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 > @@ -169,6 +176,7 @@ pub enum Fabric { > Ospf(FabricSection), > #[serde(rename = "wireguard")] > WireGuard(FabricSection), > + Bgp(FabricSection), > } > > impl UpdaterType for Fabric { > @@ -184,6 +192,7 @@ impl Fabric { > Self::Openfabric(fabric_section) => fabric_section.id(), > Self::Ospf(fabric_section) => fabric_section.id(), > Self::WireGuard(fabric_section) => fabric_section.id(), > + Self::Bgp(fabric_section) => fabric_section.id(), > } > } > > @@ -195,6 +204,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(), > Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(), > Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(), > + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(), > } > } > > @@ -206,6 +216,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > } > } > > @@ -217,6 +228,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(), > Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(), > Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(), > + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(), > } > } > > @@ -228,6 +240,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > } > } > } > @@ -241,6 +254,7 @@ impl Validatable for Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.validate(), > Fabric::Ospf(fabric_section) => fabric_section.validate(), > Fabric::WireGuard(_fabric_section) => Ok(()), > + Fabric::Bgp(fabric_section) => fabric_section.validate(), > } > } > } > @@ -263,6 +277,12 @@ 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")] > @@ -271,6 +291,7 @@ pub enum FabricUpdater { > Ospf( as UpdaterType>::Updater), > #[serde(rename = "wireguard")] > WireGuard( as UpdaterType>::Updater), > + Bgp( as UpdaterType>::Updater), > } > > impl Updater for FabricUpdater { > @@ -279,6 +300,7 @@ impl Updater for FabricUpdater { > FabricUpdater::Openfabric(updater) => updater.is_empty(), > FabricUpdater::Ospf(updater) => updater.is_empty(), > FabricUpdater::WireGuard(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 f47a522..f85c547 100644 > --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs > @@ -11,6 +11,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}, > wireguard::WireGuardNode, > @@ -34,9 +35,11 @@ impl From
for FabricOrNode { > Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()), > Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()), > Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()), > + Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()), > + Section::WireGuardNode(node_section) => Self::Node(node_section.into()), > Section::OpenfabricNode(node_section) => Self::Node(node_section.into()), > Section::OspfNode(node_section) => Self::Node(node_section.into()), > - Section::WireGuardNode(node_section) => Self::Node(node_section.into()), > + Section::BgpNode(node_section) => Self::Node(node_section.into()), > } > } > } > @@ -68,10 +71,12 @@ pub enum Section { > OspfFabric(FabricSection), > #[serde(rename = "wireguard_fabric")] > WireGuardFabric(FabricSection), > + BgpFabric(FabricSection), > OpenfabricNode(NodeSection), > OspfNode(NodeSection), > #[serde(rename = "wireguard_node")] > WireGuardNode(NodeSection), > + BgpNode(NodeSection), > } > > impl From> for Section { > @@ -92,6 +97,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) > @@ -110,12 +121,19 @@ 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::WireGuard(fabric_section) => fabric_section.into(), > + Fabric::Bgp(fabric_section) => fabric_section.into(), > } > } > } > @@ -126,6 +144,7 @@ impl From for Section { > Node::Openfabric(node_section) => node_section.into(), > Node::Ospf(node_section) => node_section.into(), > Node::WireGuard(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 f2300ac..af15898 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::protocol::wireguard::WireGuardNode; > use crate::sdn::fabric::section_config::{ > fabric::{FabricId, FABRIC_ID_REGEX_STR}, > @@ -191,6 +192,7 @@ pub enum Node { > Ospf(NodeSection), > #[serde(rename = "wireguard")] > WireGuard(NodeSection), > + Bgp(NodeSection), > } > > impl Node { > @@ -200,6 +202,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.id(), > Node::Ospf(node_section) => node_section.id(), > Node::WireGuard(node_section) => node_section.id(), > + Node::Bgp(node_section) => node_section.id(), > } > } > > @@ -209,6 +212,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.ip(), > Node::Ospf(node_section) => node_section.ip(), > Node::WireGuard(node_section) => node_section.ip(), > + Node::Bgp(node_section) => node_section.ip(), > } > } > > @@ -218,6 +222,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.ip6(), > Node::Ospf(node_section) => node_section.ip6(), > Node::WireGuard(node_section) => node_section.ip6(), > + Node::Bgp(node_section) => node_section.ip6(), > } > } > } > @@ -230,6 +235,7 @@ impl Validatable for Node { > Node::Openfabric(node_section) => node_section.validate(), > Node::Ospf(node_section) => node_section.validate(), > Node::WireGuard(node_section) => node_section.validate(), > + Node::Bgp(node_section) => node_section.validate(), > } > } > } > @@ -252,6 +258,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, > @@ -273,6 +285,7 @@ pub mod api { > use proxmox_schema::{Updater, UpdaterType}; > > use crate::sdn::fabric::section_config::protocol::{ > + bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater}, > openfabric::{ > OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, > OpenfabricNodePropertiesUpdater, > @@ -338,6 +351,7 @@ pub mod api { > Ospf(NodeData), > #[serde(rename = "wireguard")] > WireGuard(NodeData), > + Bgp(NodeData), > } > > impl From for Node { > @@ -346,6 +360,7 @@ pub mod api { > super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), > super::Node::Ospf(node_section) => Self::Ospf(node_section.into()), > super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), > + super::Node::Bgp(node_section) => Self::Bgp(node_section.into()), > } > } > } > @@ -356,6 +371,7 @@ pub mod api { > Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), > Node::Ospf(node_section) => Self::Ospf(node_section.into()), > Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), > + Node::Bgp(node_section) => Self::Bgp(node_section.into()), > } > } > } > @@ -373,6 +389,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")] > @@ -410,6 +430,7 @@ pub mod api { > Ospf(NodeDataUpdater), > #[serde(rename = "wireguard")] > WireGuard(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..f6f55e2 > --- /dev/null > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs > @@ -0,0 +1,317 @@ > +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; > + > +use crate::sdn::prefix_list::PrefixListId; > +use crate::sdn::route_map::RouteMapId; > + > +#[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); potentially something for later, but I think we use ASN in several places - so we might want to add this to sdn-types instead and reuse it across the ve-rs crates? > +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) > + } > +} Is there a reason why proxmox_serde::perl::deserialize_u32 doesn't work? It should work the same afaict. > +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>, > + > + /// Route map to apply for incoming routes > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_map_in: Option, > + > + /// Route map to apply for outgoing routes > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_map_out: Option, > + > + /// By default only routes from the configured IP prefix are imported > + /// into the local routing table. This setting can be used to override the > + /// allowed IPs and import additional routes besides the configured IP > + /// prefix. > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_filter: Option, > +} > + > +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> { > + if self.ip_prefix().is_none() && self.ip6_prefix().is_none() { > + return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string())); > + } > + > + Ok(()) > + } > +} > + > +#[derive(Debug, Clone, Serialize, Deserialize)] > +#[serde(rename_all = "snake_case")] > +pub enum BgpDeletableProperties { > + Redistribute, > + RouteFilter, > + RouteMapIn, > + RouteMapOut, > +} > + > +#[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> { > + 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) > +} > + > +/// Resolves the BGP router-id for a node: the IPv4 address if set, > +/// otherwise an FNV-1a hash of the IPv6 address. > +pub fn bgp_router_id(node: &NodeSection) -> Option { > + node.ip() > + .or_else(|| node.ip6().map(|ipv6| router_id_from_ipv6(&ipv6))) > +} > 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 fd77426..c7adf0f 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,3 +1,4 @@ > +pub mod bgp; > pub mod openfabric; > pub mod ospf; > pub mod wireguard; > 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..0db0034 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap > @@ -0,0 +1,36 @@ > +--- > +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 > + neighbor test route-map pve_bgp_test_in in > + 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 > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +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..d7ed018 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap > @@ -0,0 +1,35 @@ > +--- > +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 > + neighbor test route-map pve_bgp_test_in in > + 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 > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +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..8dbb36b > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap > @@ -0,0 +1,37 @@ > +--- > +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 > + neighbor test route-map pve_bgp6_test_in in > + 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 > +! > +route-map pve_bgp6 permit 65535 > +exit > +! > +route-map pve_bgp6_test_in permit 10 > + match ipv6 address pve_bgp_test_ip6s > +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..a091148 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap > @@ -0,0 +1,36 @@ > +--- > +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 > + neighbor test route-map pve_bgp6_test_in in > + 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 > +! > +route-map pve_bgp6 permit 65535 > +exit > +! > +route-map pve_bgp6_test_in permit 10 > + match ipv6 address pve_bgp_test_ip6s > +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..226337f > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap > @@ -0,0 +1,42 @@ > +--- > +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 > + neighbor test route-map pve_bgp_test_in in > + 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 > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +exit > +! > +ip protocol bgp route-map pve_bgp