From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support
Date: Fri, 27 Mar 2026 16:10:26 +0100 [thread overview]
Message-ID: <20260327151031.149360-2-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260327151031.149360-1-h.laimer@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.
Since the EVPN controller shares the same 'router bgp' block, the
fabric merges its neighbor group and address families into an existing
router config rather than replacing it. This is unlike OSPF and
OpenFabric which have their own separate protocol blocks.
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 | 66 +++++
proxmox-frr/src/ser/mod.rs | 1 +
proxmox-ve-config/src/sdn/fabric/frr.rs | 258 ++++++++++++++++-
proxmox-ve-config/src/sdn/fabric/mod.rs | 100 +++++++
.../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 | 266 ++++++++++++++++++
.../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 | 58 ++++
.../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 ++
16 files changed, 955 insertions(+), 3 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
diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
index 6eb7c57..45b0c2e 100644
--- a/proxmox-frr/src/ser/bgp.rs
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -142,6 +142,72 @@ pub struct AddressFamilies {
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.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.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.
+ 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)]
pub struct Vrf {
pub vni: Option<u32>,
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 692fb7f..107837b 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -113,6 +113,7 @@ impl InterfaceName {
Err(InterfaceNameError::TooLong)
}
}
+
fn from_string_unchecked(s: String) -> InterfaceName {
Self(s)
}
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index f2b7c72..f1e0a46 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,7 +1,11 @@
-use std::net::{IpAddr, Ipv4Addr};
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use tracing;
+use proxmox_frr::ser::bgp::{
+ AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF,
+ Ipv6UnicastAF, 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::{
@@ -9,13 +13,16 @@ use proxmox_frr::ser::route_map::{
RouteMapMatchInner, RouteMapName, RouteMapSet,
};
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::BgpNode;
use crate::sdn::fabric::section_config::protocol::{
+ bgp::BgpRedistributionSource,
openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
ospf::OspfInterfaceProperties,
};
@@ -278,6 +285,203 @@ 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 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) {
+ 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),
+ },
+ );
+ }
+ }
+ }
}
}
@@ -419,6 +623,39 @@ fn build_openfabric_routemap(
)
}
+/// Helper that builds a RouteMap for the BGP protocol.
+fn build_bgp_routemap(
+ fabric_id: &FabricId,
+ router_ip: IpAddr,
+ seq: u32,
+) -> (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::V4(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_bgp_{fabric_id}_ips"
+ ))),
+ )),
+ IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(
+ AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+ "pve_bgp_{fabric_id}_ip6s"
+ ))),
+ )),
+ }],
+ sets: vec![RouteMapSet::Src(router_ip)],
+ custom_frr_config: Vec::new(),
+ },
+ )
+}
+
/// Helper that builds a RouteMap for the OSPF protocol.
fn build_ospf_dummy_routemap(
fabric_id: &FabricId,
@@ -441,3 +678,18 @@ fn build_ospf_dummy_routemap(
Ok((routemap_name, routemap))
}
+
+/// 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. Collision probability follows the birthday
+/// bound: <0.01% at 10k routers, ~0.25% at 50k. Sequential and sparse
+/// loopback allocations both distribute well with the standard FNV constants.
+fn router_id_from_ipv6(addr: &Ipv6Addr) -> Ipv4Addr {
+ let mut hash: u32 = 0x811c9dc5;
+ for &byte in &addr.octets() {
+ hash ^= byte as u32;
+ hash = hash.wrapping_mul(0x01000193);
+ }
+ Ipv4Addr::from(hash)
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 677a309..4ad848a 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -20,6 +20,9 @@ use crate::sdn::fabric::section_config::node::{
api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
Node, NodeId, NodeSection,
};
+use crate::sdn::fabric::section_config::protocol::bgp::{
+ BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater, BgpProperties,
+};
use crate::sdn::fabric::section_config::protocol::openfabric::{
OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater,
@@ -64,6 +67,8 @@ pub enum FabricConfigError {
// this is technically possible, but we don't allow it
#[error("duplicate OSPF area")]
DuplicateOspfArea,
+ #[error("BGP fabric '{0}' has nodes with duplicate ASN {1}")]
+ DuplicateBgpAsn(String, u32),
#[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 +167,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 +262,7 @@ impl Entry<OspfProperties, OspfNodeProperties> {
pub enum FabricEntry {
Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
Ospf(Entry<OspfProperties, OspfNodeProperties>),
+ Bgp(Entry<BgpProperties, BgpNode>),
}
impl FabricEntry {
@@ -241,6 +274,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 +285,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 +295,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 +375,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 +424,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 +433,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 +443,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 +452,7 @@ impl FabricEntry {
match self {
FabricEntry::Openfabric(entry) => &entry.fabric,
FabricEntry::Ospf(entry) => &entry.fabric,
+ FabricEntry::Bgp(entry) => &entry.fabric,
}
}
@@ -381,6 +461,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 +473,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 +595,7 @@ 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();
// Check for overlapping IP prefixes across fabrics
let fabrics: Vec<_> = self.fabrics.values().map(|f| f.fabric()).collect();
@@ -573,6 +656,23 @@ 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(
+ entry.fabric().id().to_string(),
+ asn,
+ ));
+ }
+ }
+ }
}
}
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..652ffdd
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
@@ -0,0 +1,266 @@
+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> {
+ 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
+ }
+}
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..e1562b1 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -162,3 +162,61 @@ 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);
+}
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
--
2.47.3
next prev parent reply other threads:[~2026-03-27 15:11 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
2026-03-27 15:10 ` Hannes Laimer [this message]
2026-03-27 15:10 ` [PATCH proxmox-perl-rs 1/2] sdn: fabrics: add BGP config generation Hannes Laimer
2026-03-27 15:10 ` [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-network 1/2] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-manager 1/1] ui: sdn: add BGP fabric support 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=20260327151031.149360-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox