From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support
Date: Wed, 13 May 2026 14:29:29 +0200 [thread overview]
Message-ID: <ebd653ed-cfb2-4a9d-b131-7027f7e2b481@proxmox.com> (raw)
In-Reply-To: <20260512141305.199664-2-h.laimer@proxmox.com>
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<u32> 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<Redistribution> = 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<RouteMapMatch> =
> + 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<RouteMapMatch> =
> + 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<OpenfabricProperties, OpenfabricNodeProperties>),
> Ospf(Entry<OspfProperties, OspfNodeProperties>),
> WireGuard(Entry<WireGuardProperties, WireGuardNode>),
> + Bgp(Entry<BgpProperties, BgpNode>),
> }
>
> 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::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> {
> + 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<Fabric> 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<std::net::Ipv4Addr, &NodeId> = 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::<BgpPropertiesUpdater, BgpDeletableProperties> {
> + 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<WireGuardProperties> {
> type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>;
> }
>
> +impl UpdaterType for FabricSection<BgpProperties> {
> + type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>;
> +}
> +
> /// Enum containing all types of fabrics.
> ///
> /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
> @@ -169,6 +176,7 @@ pub enum Fabric {
> Ospf(FabricSection<OspfProperties>),
> #[serde(rename = "wireguard")]
> WireGuard(FabricSection<WireGuardProperties>),
> + Bgp(FabricSection<BgpProperties>),
> }
>
> 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<FabricSection<WireGuardProperties>> for Fabric {
> }
> }
>
> +impl From<FabricSection<BgpProperties>> for Fabric {
> + fn from(section: FabricSection<BgpProperties>) -> 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(<FabricSection<OspfProperties> as UpdaterType>::Updater),
> #[serde(rename = "wireguard")]
> WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater),
> + Bgp(<FabricSection<BgpProperties> 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<Section> for FabricOrNode<Fabric, Node> {
> 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<OspfProperties>),
> #[serde(rename = "wireguard_fabric")]
> WireGuardFabric(FabricSection<WireGuardProperties>),
> + BgpFabric(FabricSection<BgpProperties>),
> OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
> OspfNode(NodeSection<OspfNodeProperties>),
> #[serde(rename = "wireguard_node")]
> WireGuardNode(NodeSection<WireGuardNode>),
> + BgpNode(NodeSection<BgpNode>),
> }
>
> impl From<FabricSection<OpenfabricProperties>> for Section {
> @@ -92,6 +97,12 @@ impl From<FabricSection<WireGuardProperties>> for Section {
> }
> }
>
> +impl From<FabricSection<BgpProperties>> for Section {
> + fn from(section: FabricSection<BgpProperties>) -> Self {
> + Self::BgpFabric(section)
> + }
> +}
> +
> impl From<NodeSection<OpenfabricNodeProperties>> for Section {
> fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
> Self::OpenfabricNode(section)
> @@ -110,12 +121,19 @@ impl From<NodeSection<WireGuardNode>> for Section {
> }
> }
>
> +impl From<NodeSection<BgpNode>> for Section {
> + fn from(section: NodeSection<BgpNode>) -> Self {
> + Self::BgpNode(section)
> + }
> +}
> +
> impl From<Fabric> 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<Node> 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<OspfNodeProperties>),
> #[serde(rename = "wireguard")]
> WireGuard(NodeSection<WireGuardNode>),
> + Bgp(NodeSection<BgpNode>),
> }
>
> 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<NodeSection<WireGuardNode>> for Node {
> }
> }
>
> +impl From<NodeSection<BgpNode>> for Node {
> + fn from(value: NodeSection<BgpNode>) -> 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<OspfNodeProperties>),
> #[serde(rename = "wireguard")]
> WireGuard(NodeData<WireGuardNode>),
> + Bgp(NodeData<BgpNode>),
> }
>
> impl From<super::Node> 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<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>;
> }
>
> + impl UpdaterType for NodeData<BgpNodeProperties> {
> + type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>;
> + }
> +
> #[derive(Debug, Clone, Serialize, Deserialize)]
> pub struct NodeDataUpdater<T, D> {
> #[serde(skip_serializing_if = "Option::is_none")]
> @@ -410,6 +430,7 @@ pub mod api {
> Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
> #[serde(rename = "wireguard")]
> WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>),
> + Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>),
> }
>
> #[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<u32>,
> + /// Route MAP to use for filtering redistributed routes
> + #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")]
> + pub(crate) route_map: Option<RouteMapId>,
> +}
> +
> +#[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<D>(deserializer: D) -> Result<Self, D::Error>
> + 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<E: de::Error>(self, v: i64) -> Result<ASN, E> {
> + u32::try_from(v)
> + .map(ASN)
> + .map_err(|_| E::custom(format!("ASN out of range: {v}")))
> + }
> +
> + fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> {
> + u32::try_from(v)
> + .map(ASN)
> + .map_err(|_| E::custom(format!("ASN out of range: {v}")))
> + }
> +
> + fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> {
> + v.parse::<u32>()
> + .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<ASN>;
> +}
> +
> +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<PropertyString<BgpRedistribution>>,
> +
> + /// Route map to apply for incoming routes
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub(crate) route_map_in: Option<RouteMapId>,
> +
> + /// Route map to apply for outgoing routes
> + #[serde(skip_serializing_if = "Option::is_none")]
> + pub(crate) route_map_out: Option<RouteMapId>,
> +
> + /// 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<PrefixListId>,
> +}
> +
> +impl BgpProperties {
> + pub fn bfd(&self) -> bool {
> + self.bfd
> + }
> +}
> +
> +impl Validatable for FabricSection<BgpProperties> {
> + type Error = FabricConfigError;
> +
> + /// Validate the [`FabricSection<BgpProperties>`].
> + 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<Ipv4Addr>,
> +}
> +
> +#[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<BgpNode> {
> + 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<PropertyString<BgpInterfaceProperties>>,
> +}
> +
> +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<Item = &BgpInterfaceProperties> {
> + 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<Item = &mut BgpInterfaceProperties> {
> + self.interfaces
> + .iter_mut()
> + .map(|property_string| property_string.deref_mut())
> + }
> +}
> +
> +impl Validatable for NodeSection<BgpNodeProperties> {
> + type Error = FabricConfigError;
> +
> + /// Validate the [`NodeSection<BgpNodeProperties>`].
> + 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<BgpNode>) -> Option<StdIpv4Addr> {
> + 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
next prev parent reply other threads:[~2026-05-13 12:29 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer
2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer
2026-05-13 12:29 ` Stefan Hanreich [this message]
2026-05-13 13:00 ` Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-05-13 12:33 ` Stefan Hanreich
2026-05-13 13:02 ` Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer
2026-05-13 12:38 ` Stefan Hanreich
2026-05-12 14:13 ` [PATCH pve-docs v4 7/7] sdn: add bgp fabric section Hannes Laimer
2026-05-13 12:39 ` [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Stefan Hanreich
2026-05-13 18:43 ` superseded: " Hannes Laimer
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=ebd653ed-cfb2-4a9d-b131-7027f7e2b481@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox