From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs v2 1/7] sdn: fabric: add BGP protocol support
Date: Wed, 15 Apr 2026 13:11:28 +0200 [thread overview]
Message-ID: <20260415111134.124720-2-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260415111134.124720-1-h.laimer@proxmox.com>
From: Stefan Hanreich <s.hanreich@proxmox.com>
Add BGP as a fabric protocol for eBGP unnumbered underlays. Each node
has a mandatory, globally unique ASN for interface-based eBGP peering.
Unlike OSPF and OpenFabric, BGP does not have its own FRR daemon -
the fabric config needs to coexist with EVPN in a single 'router bgp'
block. To handle this, the fabric merges into an existing router
rather than replacing it, using local-as to present the per-node ASN
to underlay peers when the router already runs under the EVPN ASN.
For IPv6-only nodes, the BGP router-id is derived from the IPv6
address using FNV-1a, since router-id must be a 32-bit value.
Co-authored-by: Hannes Laimer <h.laimer@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
proxmox-frr/src/ser/bgp.rs | 85 +++++-
proxmox-ve-config/src/sdn/fabric/frr.rs | 254 +++++++++++++++-
proxmox-ve-config/src/sdn/fabric/mod.rs | 111 +++++++
.../src/sdn/fabric/section_config/fabric.rs | 22 ++
.../src/sdn/fabric/section_config/mod.rs | 19 ++
.../src/sdn/fabric/section_config/node.rs | 21 ++
.../sdn/fabric/section_config/protocol/bgp.rs | 286 ++++++++++++++++++
.../sdn/fabric/section_config/protocol/mod.rs | 1 +
.../tests/fabric/cfg/bgp_default/fabrics.cfg | 17 ++
.../fabric/cfg/bgp_ipv6_only/fabrics.cfg | 17 ++
proxmox-ve-config/tests/fabric/main.rs | 119 +++++++-
.../snapshots/fabric__bgp_default_pve.snap | 28 ++
.../snapshots/fabric__bgp_default_pve1.snap | 27 ++
.../snapshots/fabric__bgp_ipv6_only_pve.snap | 29 ++
.../snapshots/fabric__bgp_ipv6_only_pve1.snap | 28 ++
.../fabric__bgp_merge_with_evpn_pve.snap | 34 +++
16 files changed, 1089 insertions(+), 9 deletions(-)
create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
index 0bf4a1d..5d454fa 100644
--- a/proxmox-frr/src/ser/bgp.rs
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -43,9 +43,9 @@ pub enum LocalAsFlags {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub struct LocalAsSettings {
- asn: u32,
+ pub asn: u32,
#[serde(default)]
- mode: Option<LocalAsFlags>,
+ pub mode: Option<LocalAsFlags>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -159,9 +159,84 @@ pub struct CommonAddressFamilyOptions {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub struct AddressFamilies {
- ipv4_unicast: Option<Ipv4UnicastAF>,
- ipv6_unicast: Option<Ipv6UnicastAF>,
- l2vpn_evpn: Option<L2vpnEvpnAF>,
+ pub ipv4_unicast: Option<Ipv4UnicastAF>,
+ pub ipv6_unicast: Option<Ipv6UnicastAF>,
+ pub l2vpn_evpn: Option<L2vpnEvpnAF>,
+}
+
+impl AddressFamilies {
+ /// Merge another [`AddressFamilies`] into this one.
+ ///
+ /// For each address family: if `self` already has it, extend its neighbors, networks, and
+ /// redistribute lists. If `self` doesn't have it, take it from `other`.
+ pub fn merge(&mut self, other: AddressFamilies) {
+ match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) {
+ (Some(existing), Some(incoming)) => {
+ existing
+ .common_options
+ .neighbors
+ .extend(incoming.common_options.neighbors);
+ existing
+ .common_options
+ .import_vrf
+ .extend(incoming.common_options.import_vrf);
+ existing
+ .common_options
+ .custom_frr_config
+ .extend(incoming.common_options.custom_frr_config);
+ existing.networks.extend(incoming.networks);
+ existing.redistribute.extend(incoming.redistribute);
+ }
+ (None, Some(incoming)) => {
+ self.ipv4_unicast = Some(incoming);
+ }
+ _ => {}
+ }
+
+ match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) {
+ (Some(existing), Some(incoming)) => {
+ existing
+ .common_options
+ .neighbors
+ .extend(incoming.common_options.neighbors);
+ existing
+ .common_options
+ .import_vrf
+ .extend(incoming.common_options.import_vrf);
+ existing
+ .common_options
+ .custom_frr_config
+ .extend(incoming.common_options.custom_frr_config);
+ existing.networks.extend(incoming.networks);
+ existing.redistribute.extend(incoming.redistribute);
+ }
+ (None, Some(incoming)) => {
+ self.ipv6_unicast = Some(incoming);
+ }
+ _ => {}
+ }
+
+ // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this)
+ if self.l2vpn_evpn.is_none() {
+ self.l2vpn_evpn = other.l2vpn_evpn;
+ }
+ }
+}
+
+impl BgpRouter {
+ /// Merge a fabric-generated [`BgpRouter`] into an existing one.
+ ///
+ /// Appends the fabric's neighbor groups and merges address families. Keeps the existing
+ /// router's ASN, router-id, and other top-level settings. The caller is responsible for
+ /// setting `local_as` on the fabric's neighbor group if the ASNs differ.
+ pub fn merge_fabric(&mut self, other: BgpRouter) {
+ self.neighbor_groups.extend(other.neighbor_groups);
+ self.address_families.merge(other.address_families);
+
+ if self.default_ipv4_unicast.is_none() {
+ self.default_ipv4_unicast = other.default_ipv4_unicast;
+ }
+ }
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index b816ef6..373df80 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -2,19 +2,28 @@ use std::net::{IpAddr, Ipv4Addr};
use tracing;
+use proxmox_frr::ser::bgp::{
+ AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF,
+ Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs,
+ RedistributeProtocol, Redistribution,
+};
use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
use proxmox_frr::ser::route_map::{
- AccessAction, AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
+ AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
};
+use proxmox_frr::ser::AccessAction;
use proxmox_frr::ser::{
- self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
+ self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, VrfName,
};
-use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
use proxmox_sdn_types::net::Net;
use crate::common::valid::Valid;
+
+use crate::sdn::fabric::section_config::protocol::bgp::{router_id_from_ipv6, BgpNode};
use crate::sdn::fabric::section_config::protocol::{
+ bgp::BgpRedistributionSource,
openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
ospf::OspfInterfaceProperties,
};
@@ -277,6 +286,214 @@ pub fn build_fabric(
);
}
}
+ FabricEntry::Bgp(bgp_entry) => {
+ let Ok(node) = bgp_entry.node_section(¤t_node) else {
+ continue;
+ };
+
+ let BgpNode::Internal(properties) = node.properties() else {
+ continue;
+ };
+
+ let fabric = bgp_entry.fabric_section();
+
+ let local_asn = properties.asn().as_u32();
+
+ let mut bgp_interfaces = Vec::new();
+ for interface in properties.interfaces() {
+ bgp_interfaces.push(interface.name.as_str().try_into()?)
+ }
+
+ let neighbor_group = NeighborGroup {
+ name: FrrWord::new(fabric.id().to_string())?,
+ bfd: fabric.properties().bfd(),
+ remote_as: NeighborRemoteAs::External,
+ local_as: Default::default(),
+ interfaces: bgp_interfaces,
+ ips: Default::default(),
+ ebgp_multihop: Default::default(),
+ update_source: Default::default(),
+ };
+
+ let redistribute: Vec<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::new),
+ })
+ .collect();
+
+ let af_neighbors = vec![AddressFamilyNeighbor {
+ name: fabric.id().to_string(),
+ route_map_in: None,
+ route_map_out: None,
+ soft_reconfiguration_inbound: Some(true),
+ }];
+
+ let ipv4_family = node.ip().map(|ipv4| Ipv4UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: Default::default(),
+ neighbors: af_neighbors.clone(),
+ custom_frr_config: Default::default(),
+ },
+ redistribute: redistribute.clone(),
+ networks: vec![Ipv4Cidr::from(ipv4)],
+ });
+
+ let ipv6_family = node.ip6().map(|ipv6| Ipv6UnicastAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: Default::default(),
+ neighbors: af_neighbors,
+ custom_frr_config: Default::default(),
+ },
+ networks: vec![Ipv6Cidr::from(ipv6)],
+ redistribute,
+ });
+
+ let address_families = AddressFamilies {
+ ipv4_unicast: ipv4_family,
+ ipv6_unicast: ipv6_family,
+ ..Default::default()
+ };
+
+ let router_id = match (node.ip(), node.ip6()) {
+ (Some(ipv4), _) => ipv4,
+ (None, Some(ipv6)) => router_id_from_ipv6(&ipv6),
+ (None, None) => {
+ anyhow::bail!("BGP node must have an IPv4 or IPv6 address")
+ }
+ };
+
+ let mut router = BgpRouter {
+ asn: local_asn,
+ router_id,
+ neighbor_groups: vec![neighbor_group],
+ address_families,
+ coalesce_time: Default::default(),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: Default::default(),
+ graceful_restart_notification: Default::default(),
+ disable_ebgp_connected_route_check: Default::default(),
+ bestpath_as_path_multipath_relax: Default::default(),
+ custom_frr_config: Default::default(),
+ };
+
+ if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) {
+ // If the existing router uses a different ASN (e.g. the
+ // EVPN ASN), set local-as on the fabric neighbor group so
+ // the underlay peers see the correct per-node ASN.
+ if existing.asn != local_asn {
+ if let Some(ng) = router.neighbor_groups.first_mut() {
+ ng.local_as = Some(LocalAsSettings {
+ asn: local_asn,
+ mode: Some(LocalAsFlags::ReplaceAs),
+ });
+ }
+ }
+ existing.merge_fabric(router);
+ } else {
+ frr_config.bgp.vrf_router.insert(VrfName::Default, router);
+ }
+
+ // Create access-list and route-map for source address selection,
+ // so the kernel uses the loopback IP as source for fabric-learned routes.
+ if let Some(ipv4_prefix) = fabric.ip_prefix() {
+ let access_list_name =
+ AccessListName::new(format!("pve_bgp_{}_ips", fabric_id));
+
+ let rule = ser::route_map::AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::from(ipv4_prefix),
+ is_ipv6: false,
+ seq: None,
+ };
+
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+ let (routemap_name, routemap_rule) = build_bgp_routemap(
+ fabric_id,
+ IpAddr::from(node.ip().expect("node must have an ipv4 address")),
+ routemap_seq,
+ );
+
+ routemap_seq += 10;
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
+
+ if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+ {
+ routemap.v4 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Bgp,
+ IpProtocolRouteMap {
+ v4: Some(routemap_name),
+ v6: None,
+ },
+ );
+ }
+ }
+
+ if let Some(ipv6_prefix) = fabric.ip6_prefix() {
+ let access_list_name =
+ AccessListName::new(format!("pve_bgp_{}_ip6s", fabric_id));
+
+ let rule = ser::route_map::AccessListRule {
+ action: AccessAction::Permit,
+ network: Cidr::from(ipv6_prefix),
+ is_ipv6: true,
+ seq: None,
+ };
+
+ frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+ let (routemap_name, routemap_rule) = build_bgp_routemap(
+ fabric_id,
+ IpAddr::from(node.ip6().expect("node must have an ipv6 address")),
+ routemap_seq,
+ );
+
+ routemap_seq += 10;
+
+ if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+ routemap.push(routemap_rule)
+ } else {
+ frr_config
+ .routemaps
+ .insert(routemap_name.clone(), vec![routemap_rule]);
+ }
+
+ if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+ {
+ routemap.v6 = Some(routemap_name);
+ } else {
+ frr_config.protocol_routemaps.insert(
+ FrrProtocol::Bgp,
+ IpProtocolRouteMap {
+ v4: None,
+ v6: Some(routemap_name),
+ },
+ );
+ }
+ }
+ }
}
}
@@ -416,6 +633,37 @@ fn build_openfabric_routemap(
)
}
+/// Helper that builds a RouteMap for the BGP protocol.
+fn build_bgp_routemap(
+ fabric_id: &FabricId,
+ router_ip: IpAddr,
+ seq: u16,
+) -> (RouteMapName, RouteMapEntry) {
+ let routemap_name = match router_ip {
+ IpAddr::V4(_) => RouteMapName::new("pve_bgp".to_owned()),
+ IpAddr::V6(_) => RouteMapName::new("pve_bgp6".to_owned()),
+ };
+ (
+ routemap_name,
+ RouteMapEntry {
+ seq,
+ action: AccessAction::Permit,
+ matches: vec![match router_ip {
+ IpAddr::V4(_) => RouteMapMatch::IpAddressAccessList(AccessListName::new(format!(
+ "pve_bgp_{fabric_id}_ips"
+ ))),
+ IpAddr::V6(_) => RouteMapMatch::Ip6AddressAccessList(AccessListName::new(format!(
+ "pve_bgp_{fabric_id}_ip6s"
+ ))),
+ }],
+ sets: vec![RouteMapSet::Src(router_ip)],
+ custom_frr_config: Vec::new(),
+ call: None,
+ exit_action: None,
+ },
+ )
+}
+
/// Helper that builds a RouteMap for the OSPF protocol.
fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 677a309..f4c134b 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -20,6 +20,10 @@ use crate::sdn::fabric::section_config::node::{
api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
Node, NodeId, NodeSection,
};
+use crate::sdn::fabric::section_config::protocol::bgp::{
+ router_id_from_ipv6, BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater,
+ BgpProperties,
+};
use crate::sdn::fabric::section_config::protocol::openfabric::{
OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater,
@@ -64,6 +68,10 @@ pub enum FabricConfigError {
// this is technically possible, but we don't allow it
#[error("duplicate OSPF area")]
DuplicateOspfArea,
+ #[error("ASN {0} is already used by another BGP fabric node")]
+ DuplicateBgpAsn(u32),
+ #[error("BGP router-id collision: nodes have different IPv6 addresses but the same derived router-id {0}")]
+ DuplicateBgpRouterId(std::net::Ipv4Addr),
#[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")]
OverlappingIp4Prefix(String, String, String, String),
#[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")]
@@ -162,6 +170,33 @@ where
}
}
+impl Entry<BgpProperties, BgpNode> {
+ /// Get the BGP fabric config.
+ ///
+ /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+ /// so it is guaranteed that a [`FabricSection<BgpProperties>`] is returned.
+ pub fn fabric_section(&self) -> &FabricSection<BgpProperties> {
+ if let Fabric::Bgp(section) = &self.fabric {
+ return section;
+ }
+
+ unreachable!();
+ }
+
+ /// Get the BGP node config for the given node_id.
+ ///
+ /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+ /// so it is guaranteed that a [`NodeSection<BgpNode>`] is returned.
+ /// An error is returned if the node is not found.
+ pub fn node_section(&self, id: &NodeId) -> Result<&NodeSection<BgpNode>, FabricConfigError> {
+ if let Node::Bgp(section) = self.get_node(id)? {
+ return Ok(section);
+ }
+
+ unreachable!();
+ }
+}
+
impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
/// Get the OpenFabric fabric config.
///
@@ -230,6 +265,7 @@ impl Entry<OspfProperties, OspfNodeProperties> {
pub enum FabricEntry {
Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
Ospf(Entry<OspfProperties, OspfNodeProperties>),
+ Bgp(Entry<BgpProperties, BgpNode>),
}
impl FabricEntry {
@@ -241,6 +277,7 @@ impl FabricEntry {
entry.add_node(node_section)
}
(FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+ (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section),
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
@@ -251,6 +288,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.get_node(id),
FabricEntry::Ospf(entry) => entry.get_node(id),
+ FabricEntry::Bgp(entry) => entry.get_node(id),
}
}
@@ -260,6 +298,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+ FabricEntry::Bgp(entry) => entry.get_node_mut(id),
}
}
@@ -339,6 +378,46 @@ impl FabricEntry {
Ok(())
}
+ (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => {
+ let BgpNode::Internal(ref mut props) = node_section.properties else {
+ return Err(FabricConfigError::ProtocolMismatch);
+ };
+
+ let NodeDataUpdater::<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(),
+ }
+ }
+
+ Ok(())
+ }
_ => Err(FabricConfigError::ProtocolMismatch),
}
}
@@ -348,6 +427,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.nodes.iter(),
FabricEntry::Ospf(entry) => entry.nodes.iter(),
+ FabricEntry::Bgp(entry) => entry.nodes.iter(),
}
}
@@ -356,6 +436,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.delete_node(id),
FabricEntry::Ospf(entry) => entry.delete_node(id),
+ FabricEntry::Bgp(entry) => entry.delete_node(id),
}
}
@@ -365,6 +446,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => entry.into_pair(),
FabricEntry::Ospf(entry) => entry.into_pair(),
+ FabricEntry::Bgp(entry) => entry.into_pair(),
}
}
@@ -373,6 +455,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => &entry.fabric,
FabricEntry::Ospf(entry) => &entry.fabric,
+ FabricEntry::Bgp(entry) => &entry.fabric,
}
}
@@ -381,6 +464,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => &mut entry.fabric,
FabricEntry::Ospf(entry) => &mut entry.fabric,
+ FabricEntry::Bgp(entry) => &mut entry.fabric,
}
}
}
@@ -392,6 +476,7 @@ impl From<Fabric> for FabricEntry {
FabricEntry::Openfabric(Entry::new(fabric_section))
}
Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+ Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)),
}
}
}
@@ -513,6 +598,8 @@ impl Validatable for FabricConfig {
fn validate(&self) -> Result<(), FabricConfigError> {
let mut node_interfaces = HashSet::new();
let mut ospf_area = HashSet::new();
+ let mut bgp_asns = HashSet::new();
+ let mut bgp_router_ids = HashSet::new();
// Check for overlapping IP prefixes across fabrics
let fabrics: Vec<_> = self.fabrics.values().map(|f| f.fabric()).collect();
@@ -573,6 +660,30 @@ impl Validatable for FabricConfig {
return Err(FabricConfigError::DuplicateInterface);
}
}
+ Node::Bgp(node_section) => {
+ if let BgpNode::Internal(props) = node_section.properties() {
+ if !props.interfaces().all(|interface| {
+ node_interfaces.insert((node_id, interface.name().as_str()))
+ }) {
+ return Err(FabricConfigError::DuplicateInterface);
+ }
+
+ let asn = props.asn().as_u32();
+ if !bgp_asns.insert(asn) {
+ return Err(FabricConfigError::DuplicateBgpAsn(asn));
+ }
+
+ // IPv6-only nodes derive router-id via hash, check for collisions
+ if node_section.ip().is_none() {
+ if let Some(ipv6) = node_section.ip6() {
+ let rid = router_id_from_ipv6(&ipv6);
+ if !bgp_router_ids.insert(rid) {
+ return Err(FabricConfigError::DuplicateBgpRouterId(rid));
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 38911a6..9f41eae 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -8,6 +8,9 @@ use proxmox_schema::{
};
use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{
+ BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater,
+};
use crate::sdn::fabric::section_config::protocol::openfabric::{
OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
};
@@ -139,6 +142,10 @@ impl UpdaterType for FabricSection<OspfProperties> {
type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
}
+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
@@ -159,6 +166,7 @@ impl UpdaterType for FabricSection<OspfProperties> {
pub enum Fabric {
Openfabric(FabricSection<OpenfabricProperties>),
Ospf(FabricSection<OspfProperties>),
+ Bgp(FabricSection<BgpProperties>),
}
impl UpdaterType for Fabric {
@@ -173,6 +181,7 @@ impl Fabric {
match self {
Self::Openfabric(fabric_section) => fabric_section.id(),
Self::Ospf(fabric_section) => fabric_section.id(),
+ Self::Bgp(fabric_section) => fabric_section.id(),
}
}
@@ -183,6 +192,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
+ Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(),
}
}
@@ -193,6 +203,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
+ Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
}
}
@@ -203,6 +214,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
+ Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(),
}
}
@@ -213,6 +225,7 @@ impl Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
+ Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
}
}
}
@@ -225,6 +238,7 @@ impl Validatable for Fabric {
match self {
Fabric::Openfabric(fabric_section) => fabric_section.validate(),
Fabric::Ospf(fabric_section) => fabric_section.validate(),
+ Fabric::Bgp(fabric_section) => fabric_section.validate(),
}
}
}
@@ -241,12 +255,19 @@ impl From<FabricSection<OspfProperties>> 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")]
pub enum FabricUpdater {
Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
+ Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater),
}
impl Updater for FabricUpdater {
@@ -254,6 +275,7 @@ impl Updater for FabricUpdater {
match self {
FabricUpdater::Openfabric(updater) => updater.is_empty(),
FabricUpdater::Ospf(updater) => updater.is_empty(),
+ FabricUpdater::Bgp(updater) => updater.is_empty(),
}
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index d02d4ae..698dac9 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -10,6 +10,7 @@ use crate::sdn::fabric::section_config::{
fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR},
node::{Node, NodeSection, NODE_ID_REGEX_STR},
protocol::{
+ bgp::{BgpNode, BgpProperties},
openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
ospf::{OspfNodeProperties, OspfProperties},
},
@@ -31,8 +32,10 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
match section {
Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
+ Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()),
Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
Section::OspfNode(node_section) => Self::Node(node_section.into()),
+ Section::BgpNode(node_section) => Self::Node(node_section.into()),
}
}
}
@@ -62,8 +65,10 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION
pub enum Section {
OpenfabricFabric(FabricSection<OpenfabricProperties>),
OspfFabric(FabricSection<OspfProperties>),
+ BgpFabric(FabricSection<BgpProperties>),
OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
OspfNode(NodeSection<OspfNodeProperties>),
+ BgpNode(NodeSection<BgpNode>),
}
impl From<FabricSection<OpenfabricProperties>> for Section {
@@ -78,6 +83,12 @@ impl From<FabricSection<OspfProperties>> 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)
@@ -90,11 +101,18 @@ impl From<NodeSection<OspfNodeProperties>> 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::Bgp(fabric_section) => fabric_section.into(),
}
}
}
@@ -104,6 +122,7 @@ impl From<Node> for Section {
match node {
Node::Openfabric(node_section) => node_section.into(),
Node::Ospf(node_section) => node_section.into(),
+ Node::Bgp(node_section) => node_section.into(),
}
}
}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 17d2f0b..8f4564c 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -10,6 +10,7 @@ use proxmox_schema::{
};
use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties};
use crate::sdn::fabric::section_config::{
fabric::{FabricId, FABRIC_ID_REGEX_STR},
protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
@@ -186,6 +187,7 @@ impl<T: ApiType> ApiType for NodeSection<T> {
pub enum Node {
Openfabric(NodeSection<OpenfabricNodeProperties>),
Ospf(NodeSection<OspfNodeProperties>),
+ Bgp(NodeSection<BgpNode>),
}
impl Node {
@@ -194,6 +196,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.id(),
Node::Ospf(node_section) => node_section.id(),
+ Node::Bgp(node_section) => node_section.id(),
}
}
@@ -202,6 +205,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.ip(),
Node::Ospf(node_section) => node_section.ip(),
+ Node::Bgp(node_section) => node_section.ip(),
}
}
@@ -210,6 +214,7 @@ impl Node {
match self {
Node::Openfabric(node_section) => node_section.ip6(),
Node::Ospf(node_section) => node_section.ip6(),
+ Node::Bgp(node_section) => node_section.ip6(),
}
}
}
@@ -221,6 +226,7 @@ impl Validatable for Node {
match self {
Node::Openfabric(node_section) => node_section.validate(),
Node::Ospf(node_section) => node_section.validate(),
+ Node::Bgp(node_section) => node_section.validate(),
}
}
}
@@ -237,6 +243,12 @@ impl From<NodeSection<OspfNodeProperties>> 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,
@@ -258,6 +270,7 @@ pub mod api {
use proxmox_schema::{Updater, UpdaterType};
use crate::sdn::fabric::section_config::protocol::{
+ bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater},
openfabric::{
OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
OpenfabricNodePropertiesUpdater,
@@ -320,6 +333,7 @@ pub mod api {
pub enum Node {
Openfabric(NodeData<OpenfabricNodeProperties>),
Ospf(NodeData<OspfNodeProperties>),
+ Bgp(NodeData<BgpNode>),
}
impl From<super::Node> for Node {
@@ -327,6 +341,7 @@ pub mod api {
match value {
super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ super::Node::Bgp(node_section) => Self::Bgp(node_section.into()),
}
}
}
@@ -336,6 +351,7 @@ pub mod api {
match value {
Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+ Node::Bgp(node_section) => Self::Bgp(node_section.into()),
}
}
}
@@ -349,6 +365,10 @@ pub mod api {
type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
}
+ 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")]
@@ -384,6 +404,7 @@ pub mod api {
NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
),
Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+ 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..9b8e5fc
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
@@ -0,0 +1,286 @@
+use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr};
+use std::ops::{Deref, DerefMut};
+
+use proxmox_network_types::ip_address::api_types::Ipv4Addr;
+use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::fabric::FabricSection;
+use crate::sdn::fabric::section_config::interface::InterfaceName;
+use crate::sdn::fabric::section_config::node::NodeSection;
+use crate::sdn::fabric::FabricConfigError;
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+#[serde(rename_all = "lowercase")]
+/// Redistribution Sources for BGP fabric
+pub enum BgpRedistributionSource {
+ /// redistribute connected routes
+ Connected,
+ /// redistribute IS-IS routes
+ Isis,
+ /// redistribute kernel routes
+ Kernel,
+ /// redistribute openfabric routes
+ Openfabric,
+ /// redistribute ospfv2 routes
+ Ospf,
+ /// redistribute ospfv3 routes
+ Ospf6,
+ /// redistribute static routes
+ Static,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// A BGP redistribution target
+pub struct BgpRedistribution {
+ /// The source used for redistribution
+ pub(crate) source: BgpRedistributionSource,
+ /// The metric to apply to redistributed routes
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) metric: Option<u32>,
+ /// Route MAP to use for filtering redistributed routes
+ #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")]
+ pub(crate) route_map: Option<String>,
+}
+
+#[api(
+ type: Integer,
+ minimum: u32::MIN as i64,
+ maximum: u32::MAX as i64,
+)]
+#[derive(Debug, Clone, Serialize, Updater, Hash)]
+/// Autonomous system number as defined by RFC 6793
+pub struct ASN(u32);
+
+impl<'de> Deserialize<'de> for ASN {
+ fn deserialize<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)
+ }
+}
+
+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>>,
+}
+
+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> {
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum BgpDeletableProperties {}
+
+#[api]
+/// External Bgp Node
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct ExternalBgpNode {
+ peer_ip: Option<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> {
+ if self.ip().is_none() && self.ip6().is_none() {
+ return Err(FabricConfigError::NodeNoIp(self.id().to_string()));
+ }
+ Ok(())
+ }
+}
+
+#[api(
+ properties: {
+ interfaces: {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "Properties for an Bgp interface.",
+ format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA),
+ }
+ },
+ }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Bgp node.
+pub struct BgpNodeProperties {
+ /// Autonomous system number for this Node
+ pub(crate) asn: ASN,
+ /// Interfaces for this Node.
+ #[serde(default)]
+ pub(crate) interfaces: Vec<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)
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index c1ec847..8f918ef 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1,2 +1,3 @@
+pub mod bgp;
pub mod openfabric;
pub mod ospf;
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
new file mode 100644
index 0000000..bd434a7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+ bfd 0
+ ip_prefix 10.10.10.0/24
+
+bgp_node: test_pve
+ asn 65001
+ interfaces name=ens18
+ interfaces name=ens19
+ ip 10.10.10.1
+ role internal
+
+bgp_node: test_pve1
+ asn 65002
+ interfaces name=ens19
+ ip 10.10.10.2
+ role internal
+
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
new file mode 100644
index 0000000..f4581fb
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+ bfd 0
+ ip6_prefix fd00:10::/64
+
+bgp_node: test_pve
+ asn 65001
+ interfaces name=ens18
+ interfaces name=ens19
+ ip6 fd00:10::1
+ role internal
+
+bgp_node: test_pve1
+ asn 65002
+ interfaces name=ens19
+ ip6 fd00:10::2
+ role internal
+
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 95b2e62..49c5fcc 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -1,7 +1,9 @@
#![cfg(feature = "frr")]
+use std::net::Ipv4Addr;
use std::str::FromStr;
-use proxmox_frr::ser::{serializer::dump, FrrConfig};
+use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF};
+use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName};
use proxmox_ve_config::sdn::fabric::{
frr::build_fabric, section_config::node::NodeId, FabricConfig,
};
@@ -162,3 +164,118 @@ fn openfabric_ipv6_only() {
insta::assert_snapshot!(helper::reference_name!("pve"), output);
}
+
+#[test]
+fn bgp_default() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
+
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config.clone(),
+ &mut frr_config,
+ )
+ .unwrap();
+
+ let mut output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+ frr_config = FrrConfig::default();
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
+
+ output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn bgp_ipv6_only() {
+ let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+ let mut frr_config = FrrConfig::default();
+
+ build_fabric(
+ NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+ config.clone(),
+ &mut frr_config,
+ )
+ .unwrap();
+
+ let mut output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+ frr_config = FrrConfig::default();
+ build_fabric(
+ NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
+
+ output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+/// Test that build_fabric merges into an existing EVPN router and sets local-as
+/// when the ASNs differ.
+#[test]
+fn bgp_merge_with_evpn() {
+ let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg")
+ .expect("cannot find config file");
+ let config = FabricConfig::parse_section_config(&raw).unwrap();
+
+ // Pre-populate with an EVPN-like router using a different ASN
+ let mut frr_config = FrrConfig::default();
+ let evpn_router = BgpRouter {
+ asn: 65000,
+ router_id: Ipv4Addr::new(10, 10, 10, 1),
+ coalesce_time: Some(1000),
+ default_ipv4_unicast: Some(false),
+ hard_administrative_reset: None,
+ graceful_restart_notification: None,
+ disable_ebgp_connected_route_check: None,
+ bestpath_as_path_multipath_relax: None,
+ neighbor_groups: Vec::new(),
+ address_families: AddressFamilies {
+ ipv4_unicast: None,
+ ipv6_unicast: None,
+ l2vpn_evpn: Some(L2vpnEvpnAF {
+ common_options: CommonAddressFamilyOptions {
+ import_vrf: Vec::new(),
+ neighbors: Vec::new(),
+ custom_frr_config: Vec::new(),
+ },
+ advertise_all_vni: Some(true),
+ advertise_default_gw: None,
+ default_originate: Vec::new(),
+ advertise_ipv4_unicast: None,
+ advertise_ipv6_unicast: None,
+ autort_as: None,
+ route_targets: None,
+ }),
+ },
+ custom_frr_config: Vec::new(),
+ };
+ frr_config
+ .bgp
+ .vrf_router
+ .insert(VrfName::Default, evpn_router);
+
+ build_fabric(
+ NodeId::from_str("pve").expect("invalid nodeid"),
+ config,
+ &mut frr_config,
+ )
+ .unwrap();
+
+ let output = dump(&frr_config).expect("error dumping stuff");
+
+ insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
new file mode 100644
index 0000000..34aa2cc
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 10.10.10.1
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+ network 10.10.10.1/32
+ neighbor test activate
+ neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
new file mode 100644
index 0000000..a3e6b6d
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
@@ -0,0 +1,27 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 10.10.10.2
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+ network 10.10.10.2/32
+ neighbor test activate
+ neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.2
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
new file mode 100644
index 0000000..f335a4f
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
@@ -0,0 +1,29 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 5.76.46.251
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+ network fd00:10::1/128
+ neighbor test activate
+ neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::1
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
new file mode 100644
index 0000000..67628a9
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 6.76.48.142
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+ network fd00:10::2/128
+ neighbor test activate
+ neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::2
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
new file mode 100644
index 0000000..ef57cd6
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
@@ -0,0 +1,34 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65000
+ bgp router-id 10.10.10.1
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor test local-as 65001 no-prepend replace-as
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+ network 10.10.10.1/32
+ neighbor test activate
+ neighbor test soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+ advertise-all-vni
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol bgp route-map pve_bgp
--
2.47.3
next prev parent reply other threads:[~2026-04-15 11:11 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-15 11:11 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric Hannes Laimer
2026-04-15 11:11 ` Hannes Laimer [this message]
2026-04-15 11:11 ` [PATCH proxmox-perl-rs v2 2/7] sdn: fabrics: add BGP config generation Hannes Laimer
2026-04-15 11:11 ` [PATCH proxmox-perl-rs v2 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-04-15 11:11 ` [PATCH pve-network v2 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-04-15 11:11 ` [PATCH pve-network v2 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
2026-04-15 11:11 ` [PATCH pve-manager v2 6/7] ui: sdn: add BGP fabric support Hannes Laimer
2026-04-15 11:11 ` [PATCH pve-docs v2 7/7] sdn: add bgp fabric section 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=20260415111134.124720-2-h.laimer@proxmox.com \
--to=h.laimer@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.