* [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric
@ 2026-05-12 14:12 Hannes Laimer
2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer
` (8 more replies)
0 siblings, 9 replies; 15+ messages in thread
From: Hannes Laimer @ 2026-05-12 14:12 UTC (permalink / raw)
To: pve-devel
This patch series adds BGP as a third fabric protocol alongside OpenFabric and
OSPF. It targets eBGP unnumbered underlays where each node has a unique ASN and
peers over physical interfaces without IP assignment on fabric links.
## Dependencies
This series is based on the route-maps, evpn-rework, wireguard, and
ospf-route-dist series ([3]-[6]).
## eBGP underlay
Each node gets its own ASN (e.g. 65001, 65002, 65003) and peers with its
neighbors using 'remote-as external' on unnumbered interfaces. The fabric
peer-group is named after the fabric ID and uses BFD when enabled.
## EVPN overlay
When the EVPN controller references a BGP fabric, VTEP sessions are iBGP,
consistent with how EVPN operates on OSPF and OpenFabric fabrics. The per-node
ASN is applied via 'local-as' on the underlay neighbor group.
## Single BGP process
FRR allows only one BGP instance per VRF, so the fabric underlay and the EVPN
overlay coexist in one 'router bgp' instance. The fabric generates its own
BgpRouter and merges into the existing one via merge_fabric(), appending
neighbor groups and address families without overwriting EVPN settings.
## IPv6-only support
For nodes with only an IPv6 address, the BGP router-id (which must be a 32-bit
value) is derived from the IPv6 address using FNV-1a hashing.
### Testing results for hash collisions
Scattered /64 n=1000 unique=1000 collisions=0 worst=1
Scattered /64 n=10000 unique=10000 collisions=0 worst=1
Scattered /64 n=100000 unique=99997 collisions=3 worst=2
Sequential /64 n=1000 unique=1000 collisions=0 worst=1
Sequential /64 n=10000 unique=10000 collisions=0 worst=1
Sequential /64 n=100000 unique=100000 collisions=0 worst=1
Spaced /64 (step 256) n=1000 unique=1000 collisions=0 worst=1
Spaced /64 (step 256) n=10000 unique=10000 collisions=0 worst=1
Spaced /64 (step 256) n=100000 unique=100000 collisions=0 worst=1
Sparse multi-/48 n=1000 unique=1000 collisions=0 worst=1
Sparse multi-/48 n=10000 unique=10000 collisions=0 worst=1
Sparse multi-/48 n=100000 unique=100000 collisions=0 worst=1
Only the random assignment in a /64 prefix caused a tiny amount of collisions,
and having 100k routers with randomly assigned IPs is not really typical. So
FNV-1a does seem like a good choice here. (generally I'm open to alternative
approaches for getting router-ids on nodes with no ipv4 ips)
Thanks a lot @Stefan for the base of this series!
v4:
- split route-map: `pve_bgp` (zebra `ip protocol bgp` filter) for set-src +
catch-all, `pve_bgp_<id>_in` (per-fabric peer-inbound) for filtering
- added `accept-ra 0` and `ip6-forward 1` on BGP fabric interfaces
- dropped v3's patch 6/8 (EVPN underlay prefix check)
- dropped the `router_id` field references. With prefixes always
present, the router-id is always derivable
- rebase onto the latest versions of the
route-maps[3]/wireguard[4]/evpn[5]/ospf-route-dist[6] series
v3, thanks @Gabriel and @Stefan for the (mostly off-list) feedback on v2!:
- fixed exit-node routing: moved the set-src route-map from `ip
protocol bgp` to the fabric peer-group's inbound direction, so EVPN
VRF imports aren't dropped by the filter's implicit deny
- renamed the route-map to be per-fabric (pve_bgp_<id>)
- added optional router_id field on BGP nodes (required when the fabric
has no prefix), enabling prefix-less BGP fabrics
- dropped the per-node ASN uniqueness check
- ui: show `router_id` field only when the fabric has no prefix
v2, thanks @Gabriel and @Stefan for the (off-list) feedback on v1!:
- switched EVPN overlay from eBGP to iBGP
- rebased onto Stefan's evpn[1]/route-maps[2] series
- made LocalAsSettings fields pub (needed for Rust-side construction)
- added router-id collision validation for IPv6-only nodes
- added docs section
[1] https://lore.proxmox.com/pve-devel/20260414163315.419384-1-s.hanreich@proxmox.com/
[2] https://lore.proxmox.com/pve-devel/20260401143957.386809-1-s.hanreich@proxmox.com/
[3] https://lore.proxmox.com/pve-devel/20260511090202.60323-1-s.hanreich@proxmox.com/
[4] https://lore.proxmox.com/pve-devel/20260507124008.417223-1-s.hanreich@proxmox.com/
[5] https://lore.proxmox.com/pve-devel/20260504162501.425135-1-s.hanreich@proxmox.com/
[6] https://lore.proxmox.com/pve-devel/20260504163157.429628-1-s.hanreich@proxmox.com/
proxmox-ve-rs:
Stefan Hanreich (1):
sdn: fabric: add BGP protocol support
proxmox-frr/src/ser/bgp.rs | 87 ++++-
proxmox-ve-config/src/sdn/fabric/frr.rs | 304 ++++++++++++++++-
proxmox-ve-config/src/sdn/fabric/mod.rs | 169 +++++++++-
.../src/sdn/fabric/section_config/fabric.rs | 22 ++
.../src/sdn/fabric/section_config/mod.rs | 21 +-
.../src/sdn/fabric/section_config/node.rs | 21 ++
.../sdn/fabric/section_config/protocol/bgp.rs | 317 ++++++++++++++++++
.../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 | 36 ++
.../snapshots/fabric__bgp_default_pve1.snap | 35 ++
.../snapshots/fabric__bgp_ipv6_only_pve.snap | 37 ++
.../snapshots/fabric__bgp_ipv6_only_pve1.snap | 36 ++
.../fabric__bgp_merge_with_evpn_pve.snap | 42 +++
16 files changed, 1268 insertions(+), 13 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
proxmox-perl-rs:
Hannes Laimer (1):
sdn: fabrics: add BGP status endpoints
Stefan Hanreich (1):
sdn: fabrics: add BGP config generation
pve-rs/src/bindings/sdn/fabrics.rs | 123 ++++++++++++++++++++++++++++-
pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++-
2 files changed, 225 insertions(+), 4 deletions(-)
pve-network:
Hannes Laimer (2):
sdn: fabrics: register bgp as a fabric protocol type
test: evpn: add integration test for EVPN over BGP fabric
src/PVE/Network/SDN/Fabrics.pm | 62 +++++++++++++-
src/PVE/Network/SDN/RouteMaps.pm | 15 ++++
.../bgp_fabric/expected_controller_config | 73 ++++++++++++++++
.../evpn/bgp_fabric/expected_sdn_interfaces | 60 +++++++++++++
src/test/zones/evpn/bgp_fabric/interfaces | 6 ++
src/test/zones/evpn/bgp_fabric/sdn_config | 85 +++++++++++++++++++
6 files changed, 300 insertions(+), 1 deletion(-)
create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config
create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces
create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces
create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config
pve-manager:
Hannes Laimer (1):
ui: sdn: add BGP fabric support
www/manager6/Makefile | 3 +
www/manager6/sdn/FabricsView.js | 12 ++++
www/manager6/sdn/fabrics/FabricEdit.js | 12 +++-
www/manager6/sdn/fabrics/NodeEdit.js | 1 +
www/manager6/sdn/fabrics/bgp/FabricEdit.js | 67 +++++++++++++++++++
.../sdn/fabrics/bgp/InterfacePanel.js | 15 +++++
www/manager6/sdn/fabrics/bgp/NodeEdit.js | 23 +++++++
7 files changed, 130 insertions(+), 3 deletions(-)
create mode 100644 www/manager6/sdn/fabrics/bgp/FabricEdit.js
create mode 100644 www/manager6/sdn/fabrics/bgp/InterfacePanel.js
create mode 100644 www/manager6/sdn/fabrics/bgp/NodeEdit.js
pve-docs:
Hannes Laimer (1):
sdn: add bgp fabric section
pvesdn.adoc | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 96 insertions(+)
Summary over all repositories:
32 files changed, 2019 insertions(+), 21 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer @ 2026-05-12 14:12 ` Hannes Laimer 2026-05-13 12:29 ` Stefan Hanreich 2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation Hannes Laimer ` (7 subsequent siblings) 8 siblings, 1 reply; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:12 UTC (permalink / raw) To: pve-devel 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. FRR allows only one BGP instance per VRF. The fabric underlay and the EVPN overlay therefore have to coexist in the default VRF's BGP instance, so the fabric merges into the 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 | 87 ++++- proxmox-ve-config/src/sdn/fabric/frr.rs | 304 ++++++++++++++++- proxmox-ve-config/src/sdn/fabric/mod.rs | 169 +++++++++- .../src/sdn/fabric/section_config/fabric.rs | 22 ++ .../src/sdn/fabric/section_config/mod.rs | 21 +- .../src/sdn/fabric/section_config/node.rs | 21 ++ .../sdn/fabric/section_config/protocol/bgp.rs | 317 ++++++++++++++++++ .../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 | 36 ++ .../snapshots/fabric__bgp_default_pve1.snap | 35 ++ .../snapshots/fabric__bgp_ipv6_only_pve.snap | 37 ++ .../snapshots/fabric__bgp_ipv6_only_pve1.snap | 36 ++ .../fabric__bgp_merge_with_evpn_pve.snap | 42 +++ 16 files changed, 1268 insertions(+), 13 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 c1b4466..ddb5ae6 100644 --- a/proxmox-frr/src/ser/bgp.rs +++ b/proxmox-frr/src/ser/bgp.rs @@ -43,15 +43,15 @@ 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)] pub struct NeighborGroup { pub name: FrrWord, - #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] pub bfd: bool, #[serde(default)] pub local_as: Option<LocalAsSettings>, @@ -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 40e346f..d70d5aa 100644 --- a/proxmox-ve-config/src/sdn/fabric/frr.rs +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs @@ -2,15 +2,25 @@ use std::net::{IpAddr, Ipv4Addr}; use tracing; +use proxmox_frr::ser::bgp::{ + AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF, + Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs, + RedistributeProtocol, Redistribution, +}; use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName}; use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRedistribution, OspfRouter}; -use proxmox_frr::ser::route_map::{AccessListName, RouteMapEntry, RouteMapMatch, RouteMapSet}; -use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName}; -use proxmox_network_types::ip_address::Cidr; +use proxmox_frr::ser::route_map::{ + AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet, +}; +use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, VrfName}; +use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr}; use proxmox_sdn_types::net::Net; use crate::common::valid::Valid; + +use crate::sdn::fabric::section_config::protocol::bgp::{bgp_router_id, BgpNode}; use crate::sdn::fabric::section_config::protocol::{ + bgp::BgpRedistributionSource, openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, ospf::OspfInterfaceProperties, }; @@ -289,6 +299,294 @@ pub fn build_fabric( protocol_routemap.v4 = Some(routemap_name); } FabricEntry::WireGuard(_) => {} // not a frr fabric + FabricEntry::Bgp(bgp_entry) => { + let Ok(node) = bgp_entry.node_section(¤t_node) else { + continue; + }; + + let BgpNode::Internal(properties) = node.properties() else { + continue; + }; + + let fabric = bgp_entry.fabric_section(); + + let local_asn = properties.asn().as_u32(); + + let mut bgp_interfaces = Vec::new(); + for interface in properties.interfaces() { + bgp_interfaces.push(interface.name.as_str().try_into()?) + } + + let neighbor_group = NeighborGroup { + name: FrrWord::new(fabric.id().to_string())?, + bfd: fabric.properties().bfd(), + remote_as: NeighborRemoteAs::External, + local_as: Default::default(), + interfaces: bgp_interfaces, + ips: Default::default(), + ebgp_multihop: Default::default(), + update_source: Default::default(), + }; + + let redistribute: Vec<Redistribution> = fabric + .properties() + .redistribute + .iter() + .map(|redistribution| Redistribution { + protocol: match redistribution.source { + BgpRedistributionSource::Ospf => RedistributeProtocol::Ospf, + BgpRedistributionSource::Connected => RedistributeProtocol::Connected, + BgpRedistributionSource::Isis => RedistributeProtocol::Isis, + BgpRedistributionSource::Kernel => RedistributeProtocol::Kernel, + BgpRedistributionSource::Openfabric => RedistributeProtocol::Openfabric, + BgpRedistributionSource::Ospf6 => RedistributeProtocol::Ospf6, + BgpRedistributionSource::Static => RedistributeProtocol::Static, + }, + metric: redistribution.metric, + route_map: redistribution.route_map.clone().map(RouteMapName::from), + }) + .collect(); + + let mut address_families = AddressFamilies::default(); + + if let Some(ip) = node.ip() { + // Build the prefix matcher (route_filter prefix-list, or + // an auto access-list from ip_prefix). Reused for both the + // pve_bgp set-src clause and the per-peer inbound filter + // below, so they stay in sync. + let inbound_matchers: Vec<RouteMapMatch> = + if let Some(prefix_list_id) = &fabric.properties().route_filter { + vec![RouteMapMatch::IpAddressPrefixList( + prefix_list_id.clone().into(), + )] + } else if let Some(cidr) = fabric.ip_prefix() { + let access_list_name = + AccessListName::new(format!("pve_bgp_{fabric_id}_ips")); + + let rule = ser::route_map::AccessListRule { + action: ser::route_map::AccessAction::Permit, + network: Cidr::from(cidr), + is_ipv6: false, + seq: None, + }; + + frr_config + .access_lists + .insert(access_list_name.clone(), vec![rule]); + + vec![RouteMapMatch::IpAddressAccessList(access_list_name)] + } else { + Vec::new() + }; + + // Per-peer inbound filter: permit prefixes matching the fabric's filter, + // implicit deny everything else. Stops a misbehaving fabric peer from leaking + // prefixes outside its declared range into BGP at all. If the user configured + // a custom route_map_in, it is chained via FRR's `call` action so it only sees + // prefixes that already passed the fabric-prefix filter. + let auto_in_routemap = if !inbound_matchers.is_empty() { + let name = + ser::route_map::RouteMapName::new(format!("pve_bgp_{fabric_id}_in")); + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); + in_routemap.push(RouteMapEntry { + seq: 10, + action: ser::route_map::AccessAction::Permit, + matches: inbound_matchers.clone(), + sets: Vec::new(), + custom_frr_config: Vec::new(), + call: fabric + .properties() + .route_map_in + .clone() + .map(RouteMapName::from), + exit_action: None, + }); + Some(name) + } else { + None + }; + + address_families.ipv4_unicast = Some(Ipv4UnicastAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Default::default(), + neighbors: vec![AddressFamilyNeighbor { + name: fabric.id().to_string(), + route_map_in: auto_in_routemap, + route_map_out: fabric + .properties() + .route_map_out + .clone() + .map(RouteMapName::from), + soft_reconfiguration_inbound: Some(true), + }], + custom_frr_config: Default::default(), + }, + redistribute: redistribute.clone(), + networks: vec![Ipv4Cidr::from(ip)], + }); + + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp".to_owned()); + let routemap = frr_config + .routemaps + .entry(routemap_name.clone()) + .or_default(); + + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); + routemap_seq += 10; + routemap_entry.matches = inbound_matchers; + + routemap.push(routemap_entry); + + let protocol_routemap = frr_config + .protocol_routemaps + .entry(FrrProtocol::Bgp) + .or_default(); + + protocol_routemap.v4 = Some(routemap_name); + } + + if let Some(ip) = node.ip6() { + let inbound_matchers: Vec<RouteMapMatch> = + if let Some(prefix_list_id) = &fabric.properties().route_filter { + vec![RouteMapMatch::Ip6AddressPrefixList( + prefix_list_id.clone().into(), + )] + } else if let Some(cidr) = fabric.ip6_prefix() { + let access_list_name = + AccessListName::new(format!("pve_bgp_{fabric_id}_ip6s")); + + let rule = ser::route_map::AccessListRule { + action: ser::route_map::AccessAction::Permit, + network: Cidr::from(cidr), + is_ipv6: true, + seq: None, + }; + + frr_config + .access_lists + .insert(access_list_name.clone(), vec![rule]); + + vec![RouteMapMatch::Ip6AddressAccessList(access_list_name)] + } else { + Vec::new() + }; + + let auto_in_routemap = if !inbound_matchers.is_empty() { + let name = + ser::route_map::RouteMapName::new(format!("pve_bgp6_{fabric_id}_in")); + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); + in_routemap.push(RouteMapEntry { + seq: 10, + action: ser::route_map::AccessAction::Permit, + matches: inbound_matchers.clone(), + sets: Vec::new(), + custom_frr_config: Vec::new(), + call: fabric + .properties() + .route_map_in + .clone() + .map(RouteMapName::from), + exit_action: None, + }); + Some(name) + } else { + None + }; + + address_families.ipv6_unicast = Some(Ipv6UnicastAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Default::default(), + neighbors: vec![AddressFamilyNeighbor { + name: fabric.id().to_string(), + route_map_in: auto_in_routemap, + route_map_out: fabric + .properties() + .route_map_out + .clone() + .map(RouteMapName::from), + soft_reconfiguration_inbound: Some(true), + }], + custom_frr_config: Default::default(), + }, + networks: vec![Ipv6Cidr::from(ip)], + redistribute, + }); + + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp6".to_owned()); + let routemap = frr_config + .routemaps + .entry(routemap_name.clone()) + .or_default(); + + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); + routemap_seq += 10; + routemap_entry.matches = inbound_matchers; + + routemap.push(routemap_entry); + + let protocol_routemap = frr_config + .protocol_routemaps + .entry(FrrProtocol::Bgp) + .or_default(); + + protocol_routemap.v6 = Some(routemap_name); + }; + + let router_id = bgp_router_id(&node) + .ok_or_else(|| anyhow::anyhow!("BGP node must have ip or ip6 set"))?; + + let mut router = BgpRouter { + asn: local_asn, + router_id, + neighbor_groups: vec![neighbor_group], + address_families, + coalesce_time: Default::default(), + default_ipv4_unicast: Some(false), + hard_administrative_reset: Default::default(), + graceful_restart_notification: Default::default(), + disable_ebgp_connected_route_check: Default::default(), + bestpath_as_path_multipath_relax: Default::default(), + custom_frr_config: Default::default(), + }; + + if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) { + // If the existing router uses a different ASN (e.g. the + // EVPN ASN), set local-as on the fabric neighbor group so + // the underlay peers see the correct per-node ASN. + if existing.asn != local_asn { + if let Some(ng) = router.neighbor_groups.first_mut() { + ng.local_as = Some(LocalAsSettings { + asn: local_asn, + mode: Some(LocalAsFlags::ReplaceAs), + }); + } + } + existing.merge_fabric(router); + } else { + frr_config.bgp.vrf_router.insert(VrfName::Default, router); + } + } + } + } + + // Append a trailing permit-all to the BGP route-maps so non-fabric BGP + // routes (e.g. EVPN-imported VRF routes) reach the kernel unchanged. + // Without this, the implicit deny at the end of the route-map would drop + // them. + for routemap_name in [ + ser::route_map::RouteMapName::new("pve_bgp".to_owned()), + ser::route_map::RouteMapName::new("pve_bgp6".to_owned()), + ] { + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { + routemap.push(RouteMapEntry { + seq: 65535, + action: ser::route_map::AccessAction::Permit, + matches: Vec::new(), + sets: Vec::new(), + custom_frr_config: Vec::new(), + call: None, + exit_action: None, + }); } } diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs index 5c20ec8..b21d335 100644 --- a/proxmox-ve-config/src/sdn/fabric/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs @@ -2,7 +2,7 @@ pub mod frr; pub mod section_config; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::marker::PhantomData; use std::ops::Deref; @@ -21,6 +21,10 @@ use crate::sdn::fabric::section_config::node::{ api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater}, Node, NodeId, NodeSection, }; +use crate::sdn::fabric::section_config::protocol::bgp::{ + bgp_router_id, BgpDeletableProperties, BgpNode, BgpNodeDeletableProperties, + BgpNodePropertiesUpdater, BgpProperties, BgpPropertiesUpdater, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater, @@ -69,6 +73,8 @@ pub enum FabricConfigError { // this is technically possible, but we don't allow it #[error("duplicate OSPF area")] DuplicateOspfArea, + #[error("BGP router-id collision: multiple nodes resolve to the same router-id {0}")] + DuplicateBgpRouterId(std::net::Ipv4Addr), #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")] OverlappingIp4Prefix(String, String, String, String), #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")] @@ -201,6 +207,7 @@ macro_rules! impl_entry { impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties); impl_entry!(Ospf, OspfProperties, OspfNodeProperties); impl_entry!(WireGuard, WireGuardProperties, WireGuardNode); +impl_entry!(Bgp, BgpProperties, BgpNode); /// All possible entries in a [`FabricConfig`]. /// @@ -211,6 +218,7 @@ pub enum FabricEntry { Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>), Ospf(Entry<OspfProperties, OspfNodeProperties>), WireGuard(Entry<WireGuardProperties, WireGuardNode>), + Bgp(Entry<BgpProperties, BgpNode>), } impl FabricEntry { @@ -225,6 +233,7 @@ impl FabricEntry { (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => { entry.add_node(node_section) } + (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section), _ => Err(FabricConfigError::ProtocolMismatch), } } @@ -236,6 +245,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => entry.get_node(id), FabricEntry::Ospf(entry) => entry.get_node(id), FabricEntry::WireGuard(entry) => entry.get_node(id), + FabricEntry::Bgp(entry) => entry.get_node(id), } } @@ -246,6 +256,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => entry.get_node_mut(id), FabricEntry::Ospf(entry) => entry.get_node_mut(id), FabricEntry::WireGuard(entry) => entry.get_node_mut(id), + FabricEntry::Bgp(entry) => entry.get_node_mut(id), } } @@ -392,6 +403,8 @@ impl FabricEntry { _ => continue, } } + + Ok(()) } ( WireGuardNode::External(external_wire_guard_node), @@ -422,8 +435,48 @@ impl FabricEntry { _ => continue, } } + + Ok(()) + } + _ => Err(FabricConfigError::ProtocolMismatch), + } + } + (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => { + let BgpNode::Internal(ref mut props) = node_section.properties else { + return Err(FabricConfigError::ProtocolMismatch); + }; + + let NodeDataUpdater::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> { + ip, + ip6, + properties: BgpNodePropertiesUpdater { asn, interfaces }, + delete, + } = updater; + + if let Some(ip) = ip { + node_section.ip = Some(ip); + } + + if let Some(ip) = ip6 { + node_section.ip6 = Some(ip); + } + + if let Some(asn) = asn { + props.asn = asn; + } + + if let Some(interfaces) = interfaces { + props.interfaces = interfaces; + } + + for property in delete { + match property { + NodeDeletableProperties::Ip => node_section.ip = None, + NodeDeletableProperties::Ip6 => node_section.ip6 = None, + NodeDeletableProperties::Protocol( + BgpNodeDeletableProperties::Interfaces, + ) => props.interfaces = Vec::new(), } - _ => return Err(FabricConfigError::ProtocolMismatch), } Ok(()) @@ -438,6 +491,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => entry.nodes.iter(), FabricEntry::Ospf(entry) => entry.nodes.iter(), FabricEntry::WireGuard(entry) => entry.nodes.iter(), + FabricEntry::Bgp(entry) => entry.nodes.iter(), } } @@ -447,6 +501,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => entry.delete_node(id), FabricEntry::Ospf(entry) => entry.delete_node(id), FabricEntry::WireGuard(entry) => entry.delete_node(id), + FabricEntry::Bgp(entry) => entry.delete_node(id), } } @@ -457,6 +512,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => entry.into_pair(), FabricEntry::Ospf(entry) => entry.into_pair(), FabricEntry::WireGuard(entry) => entry.into_pair(), + FabricEntry::Bgp(entry) => entry.into_pair(), } } @@ -466,6 +522,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => &entry.fabric, FabricEntry::Ospf(entry) => &entry.fabric, FabricEntry::WireGuard(entry) => &entry.fabric, + FabricEntry::Bgp(entry) => &entry.fabric, } } @@ -475,6 +532,7 @@ impl FabricEntry { FabricEntry::Openfabric(entry) => &mut entry.fabric, FabricEntry::Ospf(entry) => &mut entry.fabric, FabricEntry::WireGuard(entry) => &mut entry.fabric, + FabricEntry::Bgp(entry) => &mut entry.fabric, } } } @@ -487,6 +545,7 @@ impl From<Fabric> for FabricEntry { } Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)), Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)), + Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)), } } } @@ -500,6 +559,8 @@ impl Validatable for FabricEntry { /// - Node IP addresses are within their respective fabric IP prefix ranges /// - IP addresses are unique across all nodes in the fabric /// - Each node passes its own validation checks + /// - For BGP fabrics, derived router-ids are unique across nodes (catches + /// FNV-1a hash collisions for IPv6-only nodes) fn validate(&self) -> Result<(), FabricConfigError> { let fabric = self.fabric(); @@ -607,6 +668,27 @@ impl Validatable for FabricEntry { } } + // Per-node IPs are unique by the checks above. Router-ids can still + // collide when at least one node falls back to FNV-1a on its IPv6 + // address (the hash is 32 bits wide, so two distinct IPv6 addresses + // can map to the same router-id). + if let FabricEntry::Bgp(bgp_entry) = self { + let mut seen_router_ids: HashMap<std::net::Ipv4Addr, &NodeId> = HashMap::new(); + for (node_id, node) in &bgp_entry.nodes { + let Node::Bgp(node_section) = node else { + continue; + }; + if !matches!(node_section.properties(), BgpNode::Internal(_)) { + continue; + } + if let Some(router_id) = bgp_router_id(node_section) { + if seen_router_ids.insert(router_id, node_id).is_some() { + return Err(FabricConfigError::DuplicateBgpRouterId(router_id)); + } + } + } + } + fabric.validate() } } @@ -712,6 +794,15 @@ impl Validatable for FabricConfig { } } } + Node::Bgp(node_section) => { + if let BgpNode::Internal(props) = node_section.properties() { + if !props.interfaces().all(|interface| { + node_interfaces.insert((node_id, interface.name().as_str())) + }) { + return Err(FabricConfigError::DuplicateInterface); + } + } + } } } @@ -939,6 +1030,80 @@ impl FabricConfig { Ok(()) } + (Fabric::Bgp(fabric_section), FabricUpdater::Bgp(updater)) => { + let FabricSectionUpdater::<BgpPropertiesUpdater, BgpDeletableProperties> { + ip_prefix, + ip6_prefix, + properties: + BgpPropertiesUpdater { + bfd, + redistribute, + route_map_in, + route_map_out, + route_filter, + }, + delete, + } = updater; + + if let Some(prefix) = ip_prefix { + fabric_section.ip_prefix = Some(prefix); + } + + if let Some(prefix) = ip6_prefix { + fabric_section.ip6_prefix = Some(prefix); + } + + if let Some(bfd) = bfd { + fabric_section.properties.bfd = bfd; + } + + if let Some(redistribute) = redistribute { + fabric_section.properties.redistribute = redistribute; + } + + if let Some(route_map_in) = route_map_in { + fabric_section.properties.route_map_in = Some(route_map_in); + } + + if let Some(route_map_out) = route_map_out { + fabric_section.properties.route_map_out = Some(route_map_out); + } + + if let Some(route_filter) = route_filter { + fabric_section.properties.route_filter = Some(route_filter); + } + + for property in delete { + match property { + FabricDeletableProperties::IpPrefix => { + fabric_section.ip_prefix = None; + } + FabricDeletableProperties::Ip6Prefix => { + fabric_section.ip6_prefix = None; + } + FabricDeletableProperties::Protocol( + BgpDeletableProperties::Redistribute, + ) => { + fabric_section.properties.redistribute = Vec::new(); + } + FabricDeletableProperties::Protocol( + BgpDeletableProperties::RouteFilter, + ) => { + fabric_section.properties.route_filter = None; + } + FabricDeletableProperties::Protocol(BgpDeletableProperties::RouteMapIn) => { + fabric_section.properties.route_map_in = None; + } + FabricDeletableProperties::Protocol( + BgpDeletableProperties::RouteMapOut, + ) => { + fabric_section.properties.route_map_out = None; + } + } + } + + Ok(()) + } _ => Err(FabricConfigError::ProtocolMismatch), } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs index e92074c..efa186a 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs @@ -8,6 +8,9 @@ use proxmox_schema::{ }; use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::protocol::bgp::{ + BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater, +}; use crate::sdn::fabric::section_config::protocol::openfabric::{ OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater, }; @@ -147,6 +150,10 @@ impl UpdaterType for FabricSection<WireGuardProperties> { type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>; } +impl UpdaterType for FabricSection<BgpProperties> { + type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>; +} + /// Enum containing all types of fabrics. /// /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the @@ -169,6 +176,7 @@ pub enum Fabric { Ospf(FabricSection<OspfProperties>), #[serde(rename = "wireguard")] WireGuard(FabricSection<WireGuardProperties>), + Bgp(FabricSection<BgpProperties>), } impl UpdaterType for Fabric { @@ -184,6 +192,7 @@ impl Fabric { Self::Openfabric(fabric_section) => fabric_section.id(), Self::Ospf(fabric_section) => fabric_section.id(), Self::WireGuard(fabric_section) => fabric_section.id(), + Self::Bgp(fabric_section) => fabric_section.id(), } } @@ -195,6 +204,7 @@ impl Fabric { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(), Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(), + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(), } } @@ -206,6 +216,7 @@ impl Fabric { Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), } } @@ -217,6 +228,7 @@ impl Fabric { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(), Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(), + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(), } } @@ -228,6 +240,7 @@ impl Fabric { Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), } } } @@ -241,6 +254,7 @@ impl Validatable for Fabric { Fabric::Openfabric(fabric_section) => fabric_section.validate(), Fabric::Ospf(fabric_section) => fabric_section.validate(), Fabric::WireGuard(_fabric_section) => Ok(()), + Fabric::Bgp(fabric_section) => fabric_section.validate(), } } } @@ -263,6 +277,12 @@ impl From<FabricSection<WireGuardProperties>> for Fabric { } } +impl From<FabricSection<BgpProperties>> for Fabric { + fn from(section: FabricSection<BgpProperties>) -> Self { + Fabric::Bgp(section) + } +} + /// Enum containing all updater types for fabrics #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "protocol")] @@ -271,6 +291,7 @@ pub enum FabricUpdater { Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater), #[serde(rename = "wireguard")] WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater), + Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater), } impl Updater for FabricUpdater { @@ -279,6 +300,7 @@ impl Updater for FabricUpdater { FabricUpdater::Openfabric(updater) => updater.is_empty(), FabricUpdater::Ospf(updater) => updater.is_empty(), FabricUpdater::WireGuard(updater) => updater.is_empty(), + FabricUpdater::Bgp(updater) => updater.is_empty(), } } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs index f47a522..f85c547 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs @@ -11,6 +11,7 @@ use crate::sdn::fabric::section_config::{ fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR}, node::{Node, NodeSection, NODE_ID_REGEX_STR}, protocol::{ + bgp::{BgpNode, BgpProperties}, openfabric::{OpenfabricNodeProperties, OpenfabricProperties}, ospf::{OspfNodeProperties, OspfProperties}, wireguard::WireGuardNode, @@ -34,9 +35,11 @@ impl From<Section> for FabricOrNode<Fabric, Node> { Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()), Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()), + Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()), + Section::WireGuardNode(node_section) => Self::Node(node_section.into()), Section::OpenfabricNode(node_section) => Self::Node(node_section.into()), Section::OspfNode(node_section) => Self::Node(node_section.into()), - Section::WireGuardNode(node_section) => Self::Node(node_section.into()), + Section::BgpNode(node_section) => Self::Node(node_section.into()), } } } @@ -68,10 +71,12 @@ pub enum Section { OspfFabric(FabricSection<OspfProperties>), #[serde(rename = "wireguard_fabric")] WireGuardFabric(FabricSection<WireGuardProperties>), + BgpFabric(FabricSection<BgpProperties>), OpenfabricNode(NodeSection<OpenfabricNodeProperties>), OspfNode(NodeSection<OspfNodeProperties>), #[serde(rename = "wireguard_node")] WireGuardNode(NodeSection<WireGuardNode>), + BgpNode(NodeSection<BgpNode>), } impl From<FabricSection<OpenfabricProperties>> for Section { @@ -92,6 +97,12 @@ impl From<FabricSection<WireGuardProperties>> for Section { } } +impl From<FabricSection<BgpProperties>> for Section { + fn from(section: FabricSection<BgpProperties>) -> Self { + Self::BgpFabric(section) + } +} + impl From<NodeSection<OpenfabricNodeProperties>> for Section { fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self { Self::OpenfabricNode(section) @@ -110,12 +121,19 @@ impl From<NodeSection<WireGuardNode>> for Section { } } +impl From<NodeSection<BgpNode>> for Section { + fn from(section: NodeSection<BgpNode>) -> Self { + Self::BgpNode(section) + } +} + impl From<Fabric> for Section { fn from(fabric: Fabric) -> Self { match fabric { Fabric::Openfabric(fabric_section) => fabric_section.into(), Fabric::Ospf(fabric_section) => fabric_section.into(), Fabric::WireGuard(fabric_section) => fabric_section.into(), + Fabric::Bgp(fabric_section) => fabric_section.into(), } } } @@ -126,6 +144,7 @@ impl From<Node> for Section { Node::Openfabric(node_section) => node_section.into(), Node::Ospf(node_section) => node_section.into(), Node::WireGuard(node_section) => node_section.into(), + Node::Bgp(node_section) => node_section.into(), } } } diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs index f2300ac..af15898 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs @@ -10,6 +10,7 @@ use proxmox_schema::{ }; use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties}; use crate::sdn::fabric::section_config::protocol::wireguard::WireGuardNode; use crate::sdn::fabric::section_config::{ fabric::{FabricId, FABRIC_ID_REGEX_STR}, @@ -191,6 +192,7 @@ pub enum Node { Ospf(NodeSection<OspfNodeProperties>), #[serde(rename = "wireguard")] WireGuard(NodeSection<WireGuardNode>), + Bgp(NodeSection<BgpNode>), } impl Node { @@ -200,6 +202,7 @@ impl Node { Node::Openfabric(node_section) => node_section.id(), Node::Ospf(node_section) => node_section.id(), Node::WireGuard(node_section) => node_section.id(), + Node::Bgp(node_section) => node_section.id(), } } @@ -209,6 +212,7 @@ impl Node { Node::Openfabric(node_section) => node_section.ip(), Node::Ospf(node_section) => node_section.ip(), Node::WireGuard(node_section) => node_section.ip(), + Node::Bgp(node_section) => node_section.ip(), } } @@ -218,6 +222,7 @@ impl Node { Node::Openfabric(node_section) => node_section.ip6(), Node::Ospf(node_section) => node_section.ip6(), Node::WireGuard(node_section) => node_section.ip6(), + Node::Bgp(node_section) => node_section.ip6(), } } } @@ -230,6 +235,7 @@ impl Validatable for Node { Node::Openfabric(node_section) => node_section.validate(), Node::Ospf(node_section) => node_section.validate(), Node::WireGuard(node_section) => node_section.validate(), + Node::Bgp(node_section) => node_section.validate(), } } } @@ -252,6 +258,12 @@ impl From<NodeSection<WireGuardNode>> for Node { } } +impl From<NodeSection<BgpNode>> for Node { + fn from(value: NodeSection<BgpNode>) -> Self { + Self::Bgp(value) + } +} + /// API types for SDN fabric node configurations. /// /// This module provides specialized types that are used for API interactions when retrieving, @@ -273,6 +285,7 @@ pub mod api { use proxmox_schema::{Updater, UpdaterType}; use crate::sdn::fabric::section_config::protocol::{ + bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater}, openfabric::{ OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, OpenfabricNodePropertiesUpdater, @@ -338,6 +351,7 @@ pub mod api { Ospf(NodeData<OspfNodeProperties>), #[serde(rename = "wireguard")] WireGuard(NodeData<WireGuardNode>), + Bgp(NodeData<BgpNode>), } impl From<super::Node> for Node { @@ -346,6 +360,7 @@ pub mod api { super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), super::Node::Ospf(node_section) => Self::Ospf(node_section.into()), super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), + super::Node::Bgp(node_section) => Self::Bgp(node_section.into()), } } } @@ -356,6 +371,7 @@ pub mod api { Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), Node::Ospf(node_section) => Self::Ospf(node_section.into()), Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), + Node::Bgp(node_section) => Self::Bgp(node_section.into()), } } } @@ -373,6 +389,10 @@ pub mod api { type Updater = NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>; } + impl UpdaterType for NodeData<BgpNodeProperties> { + type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>; + } + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeDataUpdater<T, D> { #[serde(skip_serializing_if = "Option::is_none")] @@ -410,6 +430,7 @@ pub mod api { Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>), #[serde(rename = "wireguard")] WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>), + Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>), } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs new file mode 100644 index 0000000..f6f55e2 --- /dev/null +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs @@ -0,0 +1,317 @@ +use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr}; +use std::ops::{Deref, DerefMut}; + +use proxmox_network_types::ip_address::api_types::Ipv4Addr; +use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType}; +use serde::{Deserialize, Serialize}; + +use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater}; + +use crate::common::valid::Validatable; +use crate::sdn::fabric::section_config::fabric::FabricSection; +use crate::sdn::fabric::section_config::interface::InterfaceName; +use crate::sdn::fabric::section_config::node::NodeSection; +use crate::sdn::fabric::FabricConfigError; + +use crate::sdn::prefix_list::PrefixListId; +use crate::sdn::route_map::RouteMapId; + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +#[serde(rename_all = "lowercase")] +/// Redistribution Sources for BGP fabric +pub enum BgpRedistributionSource { + /// redistribute connected routes + Connected, + /// redistribute IS-IS routes + Isis, + /// redistribute kernel routes + Kernel, + /// redistribute openfabric routes + Openfabric, + /// redistribute ospfv2 routes + Ospf, + /// redistribute ospfv3 routes + Ospf6, + /// redistribute static routes + Static, +} + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// A BGP redistribution target +pub struct BgpRedistribution { + /// The source used for redistribution + pub(crate) source: BgpRedistributionSource, + /// The metric to apply to redistributed routes + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) metric: Option<u32>, + /// Route MAP to use for filtering redistributed routes + #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")] + pub(crate) route_map: Option<RouteMapId>, +} + +#[api( + type: Integer, + minimum: u32::MIN as i64, + maximum: u32::MAX as i64, +)] +#[derive(Debug, Clone, Serialize, Updater, Hash)] +/// Autonomous system number as defined by RFC 6793 +pub struct ASN(u32); + +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>>, + + /// Route map to apply for incoming routes + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) route_map_in: Option<RouteMapId>, + + /// Route map to apply for outgoing routes + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) route_map_out: Option<RouteMapId>, + + /// By default only routes from the configured IP prefix are imported + /// into the local routing table. This setting can be used to override the + /// allowed IPs and import additional routes besides the configured IP + /// prefix. + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) route_filter: Option<PrefixListId>, +} + +impl BgpProperties { + pub fn bfd(&self) -> bool { + self.bfd + } +} + +impl Validatable for FabricSection<BgpProperties> { + type Error = FabricConfigError; + + /// Validate the [`FabricSection<BgpProperties>`]. + fn validate(&self) -> Result<(), Self::Error> { + if self.ip_prefix().is_none() && self.ip6_prefix().is_none() { + return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string())); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum BgpDeletableProperties { + Redistribute, + RouteFilter, + RouteMapIn, + RouteMapOut, +} + +#[api] +/// External Bgp Node +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +pub struct ExternalBgpNode { + peer_ip: Option<Ipv4Addr>, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] +#[serde(rename_all = "snake_case", tag = "role")] +pub enum BgpNode { + Internal(BgpNodeProperties), + External(ExternalBgpNode), +} + +impl ApiType for BgpNode { + const API_SCHEMA: Schema = OneOfSchema::new( + "BGP node", + &( + "role", + false, + &StringSchema::new("internal or external").schema(), + ), + &[ + ("external", &ExternalBgpNode::API_SCHEMA), + ("internal", &BgpNodeProperties::API_SCHEMA), + ], + ) + .schema(); +} + +impl Validatable for NodeSection<BgpNode> { + type Error = FabricConfigError; + + fn validate(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[api( + properties: { + interfaces: { + type: Array, + optional: true, + items: { + type: String, + description: "Properties for an Bgp interface.", + format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA), + } + }, + } +)] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// Properties for an Bgp node. +pub struct BgpNodeProperties { + /// Autonomous system number for this Node + pub(crate) asn: ASN, + /// Interfaces for this Node. + #[serde(default)] + pub(crate) interfaces: Vec<PropertyString<BgpInterfaceProperties>>, +} + +impl BgpNodeProperties { + /// Returns the ASN for this node. + pub fn asn(&self) -> &ASN { + &self.asn + } + + /// Returns an iterator over all the interfaces. + pub fn interfaces(&self) -> impl Iterator<Item = &BgpInterfaceProperties> { + self.interfaces + .iter() + .map(|property_string| property_string.deref()) + } + + /// Returns an iterator over all the interfaces (mutable). + pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut BgpInterfaceProperties> { + self.interfaces + .iter_mut() + .map(|property_string| property_string.deref_mut()) + } +} + +impl Validatable for NodeSection<BgpNodeProperties> { + type Error = FabricConfigError; + + /// Validate the [`NodeSection<BgpNodeProperties>`]. + fn validate(&self) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum BgpNodeDeletableProperties { + Interfaces, +} + +#[api] +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] +/// Properties for an BGP interface. +pub struct BgpInterfaceProperties { + pub(crate) name: InterfaceName, +} + +impl BgpInterfaceProperties { + /// Get the name of the BGP interface. + pub fn name(&self) -> &InterfaceName { + &self.name + } + + /// Set the name of the interface. + pub fn set_name(&mut self, name: InterfaceName) { + self.name = name + } +} + +/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a. +/// +/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the +/// full 16 octets down to 4 bytes. Typical loopback allocations (sequential +/// within a prefix, sparse across /48s) produce zero collisions up to 100k +/// nodes in testing -- well below the random birthday bound (~1% at 10k) +/// because structured addresses spread well under FNV-1a. +pub fn router_id_from_ipv6(addr: &Ipv6Addr) -> StdIpv4Addr { + let mut hash: u32 = 0x811c9dc5; + for &byte in &addr.octets() { + hash ^= byte as u32; + hash = hash.wrapping_mul(0x01000193); + } + StdIpv4Addr::from(hash) +} + +/// Resolves the BGP router-id for a node: the IPv4 address if set, +/// otherwise an FNV-1a hash of the IPv6 address. +pub fn bgp_router_id(node: &NodeSection<BgpNode>) -> Option<StdIpv4Addr> { + node.ip() + .or_else(|| node.ip6().map(|ipv6| router_id_from_ipv6(&ipv6))) +} diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs index fd77426..c7adf0f 100644 --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs @@ -1,3 +1,4 @@ +pub mod bgp; pub mod openfabric; pub mod ospf; pub mod wireguard; diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg new file mode 100644 index 0000000..bd434a7 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg @@ -0,0 +1,17 @@ +bgp_fabric: test + bfd 0 + ip_prefix 10.10.10.0/24 + +bgp_node: test_pve + asn 65001 + interfaces name=ens18 + interfaces name=ens19 + ip 10.10.10.1 + role internal + +bgp_node: test_pve1 + asn 65002 + interfaces name=ens19 + ip 10.10.10.2 + role internal + diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg new file mode 100644 index 0000000..f4581fb --- /dev/null +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg @@ -0,0 +1,17 @@ +bgp_fabric: test + bfd 0 + ip6_prefix fd00:10::/64 + +bgp_node: test_pve + asn 65001 + interfaces name=ens18 + interfaces name=ens19 + ip6 fd00:10::1 + role internal + +bgp_node: test_pve1 + asn 65002 + interfaces name=ens19 + ip6 fd00:10::2 + role internal + diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs index 95b2e62..49c5fcc 100644 --- a/proxmox-ve-config/tests/fabric/main.rs +++ b/proxmox-ve-config/tests/fabric/main.rs @@ -1,7 +1,9 @@ #![cfg(feature = "frr")] +use std::net::Ipv4Addr; use std::str::FromStr; -use proxmox_frr::ser::{serializer::dump, FrrConfig}; +use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF}; +use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName}; use proxmox_ve_config::sdn::fabric::{ frr::build_fabric, section_config::node::NodeId, FabricConfig, }; @@ -162,3 +164,118 @@ fn openfabric_ipv6_only() { insta::assert_snapshot!(helper::reference_name!("pve"), output); } + +#[test] +fn bgp_default() { + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); + let mut frr_config = FrrConfig::default(); + + build_fabric( + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), + config.clone(), + &mut frr_config, + ) + .unwrap(); + + let mut output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); + + frr_config = FrrConfig::default(); + build_fabric( + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve1"), output); +} + +#[test] +fn bgp_ipv6_only() { + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); + let mut frr_config = FrrConfig::default(); + + build_fabric( + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), + config.clone(), + &mut frr_config, + ) + .unwrap(); + + let mut output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); + + frr_config = FrrConfig::default(); + build_fabric( + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve1"), output); +} + +/// Test that build_fabric merges into an existing EVPN router and sets local-as +/// when the ASNs differ. +#[test] +fn bgp_merge_with_evpn() { + let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg") + .expect("cannot find config file"); + let config = FabricConfig::parse_section_config(&raw).unwrap(); + + // Pre-populate with an EVPN-like router using a different ASN + let mut frr_config = FrrConfig::default(); + let evpn_router = BgpRouter { + asn: 65000, + router_id: Ipv4Addr::new(10, 10, 10, 1), + coalesce_time: Some(1000), + default_ipv4_unicast: Some(false), + hard_administrative_reset: None, + graceful_restart_notification: None, + disable_ebgp_connected_route_check: None, + bestpath_as_path_multipath_relax: None, + neighbor_groups: Vec::new(), + address_families: AddressFamilies { + ipv4_unicast: None, + ipv6_unicast: None, + l2vpn_evpn: Some(L2vpnEvpnAF { + common_options: CommonAddressFamilyOptions { + import_vrf: Vec::new(), + neighbors: Vec::new(), + custom_frr_config: Vec::new(), + }, + advertise_all_vni: Some(true), + advertise_default_gw: None, + default_originate: Vec::new(), + advertise_ipv4_unicast: None, + advertise_ipv6_unicast: None, + autort_as: None, + route_targets: None, + }), + }, + custom_frr_config: Vec::new(), + }; + frr_config + .bgp + .vrf_router + .insert(VrfName::Default, evpn_router); + + build_fabric( + NodeId::from_str("pve").expect("invalid nodeid"), + config, + &mut frr_config, + ) + .unwrap(); + + let output = dump(&frr_config).expect("error dumping stuff"); + + insta::assert_snapshot!(helper::reference_name!("pve"), output); +} diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap new file mode 100644 index 0000000..0db0034 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap @@ -0,0 +1,36 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65001 + bgp router-id 10.10.10.1 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp_test_in in + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +route-map pve_bgp permit 65535 +exit +! +route-map pve_bgp_test_in permit 10 + match ip address pve_bgp_test_ips +exit +! +ip protocol bgp route-map pve_bgp diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap new file mode 100644 index 0000000..d7ed018 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap @@ -0,0 +1,35 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65002 + bgp router-id 10.10.10.2 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.2/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp_test_in in + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.2 +exit +! +route-map pve_bgp permit 65535 +exit +! +route-map pve_bgp_test_in permit 10 + match ip address pve_bgp_test_ips +exit +! +ip protocol bgp route-map pve_bgp diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap new file mode 100644 index 0000000..8dbb36b --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap @@ -0,0 +1,37 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65001 + bgp router-id 5.76.46.251 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv6 unicast + network fd00:10::1/128 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp6_test_in in + exit-address-family +exit +! +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 +! +route-map pve_bgp6 permit 100 + match ipv6 address pve_bgp_test_ip6s + set src fd00:10::1 +exit +! +route-map pve_bgp6 permit 65535 +exit +! +route-map pve_bgp6_test_in permit 10 + match ipv6 address pve_bgp_test_ip6s +exit +! +! +ipv6 protocol bgp route-map pve_bgp6 diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap new file mode 100644 index 0000000..a091148 --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap @@ -0,0 +1,36 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65002 + bgp router-id 6.76.48.142 + no bgp default ipv4-unicast + neighbor test peer-group + neighbor test remote-as external + neighbor ens19 interface peer-group test + ! + address-family ipv6 unicast + network fd00:10::2/128 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp6_test_in in + exit-address-family +exit +! +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 +! +route-map pve_bgp6 permit 100 + match ipv6 address pve_bgp_test_ip6s + set src fd00:10::2 +exit +! +route-map pve_bgp6 permit 65535 +exit +! +route-map pve_bgp6_test_in permit 10 + match ipv6 address pve_bgp_test_ip6s +exit +! +! +ipv6 protocol bgp route-map pve_bgp6 diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap new file mode 100644 index 0000000..226337f --- /dev/null +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap @@ -0,0 +1,42 @@ +--- +source: proxmox-ve-config/tests/fabric/main.rs +expression: output +--- +! +router bgp 65000 + bgp router-id 10.10.10.1 + no bgp default ipv4-unicast + coalesce-time 1000 + neighbor test peer-group + neighbor test remote-as external + neighbor test local-as 65001 no-prepend replace-as + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp_test_in in + exit-address-family + ! + address-family l2vpn evpn + advertise-all-vni + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +route-map pve_bgp permit 65535 +exit +! +route-map pve_bgp_test_in permit 10 + match ip address pve_bgp_test_ips +exit +! +ip protocol bgp route-map pve_bgp -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support 2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer @ 2026-05-13 12:29 ` Stefan Hanreich 2026-05-13 13:00 ` Hannes Laimer 0 siblings, 1 reply; 15+ messages in thread From: Stefan Hanreich @ 2026-05-13 12:29 UTC (permalink / raw) To: pve-devel On 5/12/26 4:12 PM, Hannes Laimer wrote: > +impl AddressFamilies { > + /// Merge another [`AddressFamilies`] into this one. > + /// > + /// For each address family: if `self` already has it, extend its neighbors, networks, and > + /// redistribute lists. If `self` doesn't have it, take it from `other`. > + pub fn merge(&mut self, other: AddressFamilies) { maybe extend is the better name, as it mirrors existing conventions from std::Vec? > + match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) { > + (Some(existing), Some(incoming)) => { > + existing > + .common_options > + .neighbors > + .extend(incoming.common_options.neighbors); > + existing > + .common_options > + .import_vrf > + .extend(incoming.common_options.import_vrf); > + existing > + .common_options > + .custom_frr_config > + .extend(incoming.common_options.custom_frr_config); > + existing.networks.extend(incoming.networks); > + existing.redistribute.extend(incoming.redistribute); > + } > + (None, Some(incoming)) => { > + self.ipv4_unicast = Some(incoming); > + } > + _ => {} > + } > + > + match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) { > + (Some(existing), Some(incoming)) => { > + existing > + .common_options > + .neighbors > + .extend(incoming.common_options.neighbors); > + existing > + .common_options > + .import_vrf > + .extend(incoming.common_options.import_vrf); > + existing > + .common_options > + .custom_frr_config > + .extend(incoming.common_options.custom_frr_config); > + existing.networks.extend(incoming.networks); > + existing.redistribute.extend(incoming.redistribute); > + } > + (None, Some(incoming)) => { > + self.ipv6_unicast = Some(incoming); > + } > + _ => {} > + } > + > + // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this) > + if self.l2vpn_evpn.is_none() { > + self.l2vpn_evpn = other.l2vpn_evpn; > + } > + } > +} > + > +impl BgpRouter { > + /// Merge a fabric-generated [`BgpRouter`] into an existing one. > + /// > + /// Appends the fabric's neighbor groups and merges address families. Keeps the existing > + /// router's ASN, router-id, and other top-level settings. The caller is responsible for > + /// setting `local_as` on the fabric's neighbor group if the ASNs differ. > + pub fn merge_fabric(&mut self, other: BgpRouter) { > + self.neighbor_groups.extend(other.neighbor_groups); > + self.address_families.merge(other.address_families); > + > + if self.default_ipv4_unicast.is_none() { > + self.default_ipv4_unicast = other.default_ipv4_unicast; > + } > + } > } > > #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] > diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs > index 40e346f..d70d5aa 100644 > --- a/proxmox-ve-config/src/sdn/fabric/frr.rs > +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs > @@ -2,15 +2,25 @@ use std::net::{IpAddr, Ipv4Addr}; > > use tracing; > > +use proxmox_frr::ser::bgp::{ > + AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF, > + Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs, > + RedistributeProtocol, Redistribution, > +}; > use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName}; > use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRedistribution, OspfRouter}; > -use proxmox_frr::ser::route_map::{AccessListName, RouteMapEntry, RouteMapMatch, RouteMapSet}; > -use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName}; > -use proxmox_network_types::ip_address::Cidr; > +use proxmox_frr::ser::route_map::{ > + AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet, > +}; > +use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, VrfName}; > +use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr}; > use proxmox_sdn_types::net::Net; > > use crate::common::valid::Valid; > + > +use crate::sdn::fabric::section_config::protocol::bgp::{bgp_router_id, BgpNode}; > use crate::sdn::fabric::section_config::protocol::{ > + bgp::BgpRedistributionSource, > openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties}, > ospf::OspfInterfaceProperties, > }; > @@ -289,6 +299,294 @@ pub fn build_fabric( > protocol_routemap.v4 = Some(routemap_name); > } > FabricEntry::WireGuard(_) => {} // not a frr fabric > + FabricEntry::Bgp(bgp_entry) => { > + let Ok(node) = bgp_entry.node_section(¤t_node) else { > + continue; > + }; > + > + let BgpNode::Internal(properties) = node.properties() else { > + continue; > + }; > + > + let fabric = bgp_entry.fabric_section(); > + > + let local_asn = properties.asn().as_u32(); makes me wonder if implementing AsRef<u32> would simplify handling ASN throughout the code? > + > + let mut bgp_interfaces = Vec::new(); > + for interface in properties.interfaces() { > + bgp_interfaces.push(interface.name.as_str().try_into()?) > + } > + > + let neighbor_group = NeighborGroup { > + name: FrrWord::new(fabric.id().to_string())?, > + bfd: fabric.properties().bfd(), > + remote_as: NeighborRemoteAs::External, > + local_as: Default::default(), > + interfaces: bgp_interfaces, > + ips: Default::default(), > + ebgp_multihop: Default::default(), > + update_source: Default::default(), > + }; > + > + let redistribute: Vec<Redistribution> = fabric > + .properties() > + .redistribute > + .iter() > + .map(|redistribution| Redistribution { > + protocol: match redistribution.source { > + BgpRedistributionSource::Ospf => RedistributeProtocol::Ospf, > + BgpRedistributionSource::Connected => RedistributeProtocol::Connected, > + BgpRedistributionSource::Isis => RedistributeProtocol::Isis, > + BgpRedistributionSource::Kernel => RedistributeProtocol::Kernel, > + BgpRedistributionSource::Openfabric => RedistributeProtocol::Openfabric, > + BgpRedistributionSource::Ospf6 => RedistributeProtocol::Ospf6, > + BgpRedistributionSource::Static => RedistributeProtocol::Static, > + }, > + metric: redistribution.metric, > + route_map: redistribution.route_map.clone().map(RouteMapName::from), > + }) > + .collect(); > + > + let mut address_families = AddressFamilies::default(); > + > + if let Some(ip) = node.ip() { > + // Build the prefix matcher (route_filter prefix-list, or > + // an auto access-list from ip_prefix). Reused for both the > + // pve_bgp set-src clause and the per-peer inbound filter > + // below, so they stay in sync. > + let inbound_matchers: Vec<RouteMapMatch> = > + if let Some(prefix_list_id) = &fabric.properties().route_filter { > + vec![RouteMapMatch::IpAddressPrefixList( > + prefix_list_id.clone().into(), > + )] > + } else if let Some(cidr) = fabric.ip_prefix() { > + let access_list_name = > + AccessListName::new(format!("pve_bgp_{fabric_id}_ips")); > + > + let rule = ser::route_map::AccessListRule { > + action: ser::route_map::AccessAction::Permit, > + network: Cidr::from(cidr), > + is_ipv6: false, > + seq: None, > + }; > + > + frr_config > + .access_lists > + .insert(access_list_name.clone(), vec![rule]); > + > + vec![RouteMapMatch::IpAddressAccessList(access_list_name)] > + } else { > + Vec::new() > + }; > + > + // Per-peer inbound filter: permit prefixes matching the fabric's filter, > + // implicit deny everything else. Stops a misbehaving fabric peer from leaking > + // prefixes outside its declared range into BGP at all. If the user configured > + // a custom route_map_in, it is chained via FRR's `call` action so it only sees > + // prefixes that already passed the fabric-prefix filter. > + let auto_in_routemap = if !inbound_matchers.is_empty() { > + let name = > + ser::route_map::RouteMapName::new(format!("pve_bgp_{fabric_id}_in")); > + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); > + in_routemap.push(RouteMapEntry { > + seq: 10, > + action: ser::route_map::AccessAction::Permit, > + matches: inbound_matchers.clone(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: fabric > + .properties() > + .route_map_in > + .clone() > + .map(RouteMapName::from), > + exit_action: None, > + }); > + Some(name) > + } else { > + None > + }; > + > + address_families.ipv4_unicast = Some(Ipv4UnicastAF { > + common_options: CommonAddressFamilyOptions { > + import_vrf: Default::default(), > + neighbors: vec![AddressFamilyNeighbor { > + name: fabric.id().to_string(), > + route_map_in: auto_in_routemap, > + route_map_out: fabric > + .properties() > + .route_map_out > + .clone() > + .map(RouteMapName::from), > + soft_reconfiguration_inbound: Some(true), > + }], > + custom_frr_config: Default::default(), > + }, > + redistribute: redistribute.clone(), > + networks: vec![Ipv4Cidr::from(ip)], > + }); > + > + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp".to_owned()); > + let routemap = frr_config > + .routemaps > + .entry(routemap_name.clone()) > + .or_default(); > + > + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); > + routemap_seq += 10; > + routemap_entry.matches = inbound_matchers; > + > + routemap.push(routemap_entry); > + > + let protocol_routemap = frr_config > + .protocol_routemaps > + .entry(FrrProtocol::Bgp) > + .or_default(); > + > + protocol_routemap.v4 = Some(routemap_name); > + } > + > + if let Some(ip) = node.ip6() { > + let inbound_matchers: Vec<RouteMapMatch> = > + if let Some(prefix_list_id) = &fabric.properties().route_filter { > + vec![RouteMapMatch::Ip6AddressPrefixList( > + prefix_list_id.clone().into(), > + )] > + } else if let Some(cidr) = fabric.ip6_prefix() { > + let access_list_name = > + AccessListName::new(format!("pve_bgp_{fabric_id}_ip6s")); > + > + let rule = ser::route_map::AccessListRule { > + action: ser::route_map::AccessAction::Permit, > + network: Cidr::from(cidr), > + is_ipv6: true, > + seq: None, > + }; > + > + frr_config > + .access_lists > + .insert(access_list_name.clone(), vec![rule]); > + > + vec![RouteMapMatch::Ip6AddressAccessList(access_list_name)] > + } else { > + Vec::new() > + }; > + > + let auto_in_routemap = if !inbound_matchers.is_empty() { > + let name = > + ser::route_map::RouteMapName::new(format!("pve_bgp6_{fabric_id}_in")); > + let in_routemap = frr_config.routemaps.entry(name.clone()).or_default(); > + in_routemap.push(RouteMapEntry { > + seq: 10, > + action: ser::route_map::AccessAction::Permit, > + matches: inbound_matchers.clone(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: fabric > + .properties() > + .route_map_in > + .clone() > + .map(RouteMapName::from), > + exit_action: None, > + }); > + Some(name) > + } else { > + None > + }; > + > + address_families.ipv6_unicast = Some(Ipv6UnicastAF { > + common_options: CommonAddressFamilyOptions { > + import_vrf: Default::default(), > + neighbors: vec![AddressFamilyNeighbor { > + name: fabric.id().to_string(), > + route_map_in: auto_in_routemap, > + route_map_out: fabric > + .properties() > + .route_map_out > + .clone() > + .map(RouteMapName::from), > + soft_reconfiguration_inbound: Some(true), > + }], > + custom_frr_config: Default::default(), > + }, > + networks: vec![Ipv6Cidr::from(ip)], > + redistribute, > + }); > + > + let routemap_name = ser::route_map::RouteMapName::new("pve_bgp6".to_owned()); > + let routemap = frr_config > + .routemaps > + .entry(routemap_name.clone()) > + .or_default(); > + > + let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq); > + routemap_seq += 10; > + routemap_entry.matches = inbound_matchers; > + > + routemap.push(routemap_entry); > + > + let protocol_routemap = frr_config > + .protocol_routemaps > + .entry(FrrProtocol::Bgp) > + .or_default(); > + > + protocol_routemap.v6 = Some(routemap_name); > + }; > + > + let router_id = bgp_router_id(&node) > + .ok_or_else(|| anyhow::anyhow!("BGP node must have ip or ip6 set"))?; > + > + let mut router = BgpRouter { > + asn: local_asn, > + router_id, > + neighbor_groups: vec![neighbor_group], > + address_families, > + coalesce_time: Default::default(), > + default_ipv4_unicast: Some(false), > + hard_administrative_reset: Default::default(), > + graceful_restart_notification: Default::default(), > + disable_ebgp_connected_route_check: Default::default(), > + bestpath_as_path_multipath_relax: Default::default(), > + custom_frr_config: Default::default(), > + }; > + > + if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) { > + // If the existing router uses a different ASN (e.g. the > + // EVPN ASN), set local-as on the fabric neighbor group so > + // the underlay peers see the correct per-node ASN. > + if existing.asn != local_asn { > + if let Some(ng) = router.neighbor_groups.first_mut() { > + ng.local_as = Some(LocalAsSettings { > + asn: local_asn, > + mode: Some(LocalAsFlags::ReplaceAs), > + }); > + } > + } > + existing.merge_fabric(router); > + } else { > + frr_config.bgp.vrf_router.insert(VrfName::Default, router); > + } > + } > + } > + } > + > + // Append a trailing permit-all to the BGP route-maps so non-fabric BGP > + // routes (e.g. EVPN-imported VRF routes) reach the kernel unchanged. > + // Without this, the implicit deny at the end of the route-map would drop > + // them. > + for routemap_name in [ > + ser::route_map::RouteMapName::new("pve_bgp".to_owned()), > + ser::route_map::RouteMapName::new("pve_bgp6".to_owned()), > + ] { > + if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) { > + routemap.push(RouteMapEntry { > + seq: 65535, > + action: ser::route_map::AccessAction::Permit, > + matches: Vec::new(), > + sets: Vec::new(), > + custom_frr_config: Vec::new(), > + call: None, > + exit_action: None, > + }); > } > } > > diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs > index 5c20ec8..b21d335 100644 > --- a/proxmox-ve-config/src/sdn/fabric/mod.rs > +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs > @@ -2,7 +2,7 @@ > pub mod frr; > pub mod section_config; > > -use std::collections::{BTreeMap, HashSet}; > +use std::collections::{BTreeMap, HashMap, HashSet}; > use std::marker::PhantomData; > use std::ops::Deref; > > @@ -21,6 +21,10 @@ use crate::sdn::fabric::section_config::node::{ > api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater}, > Node, NodeId, NodeSection, > }; > +use crate::sdn::fabric::section_config::protocol::bgp::{ > + bgp_router_id, BgpDeletableProperties, BgpNode, BgpNodeDeletableProperties, > + BgpNodePropertiesUpdater, BgpProperties, BgpPropertiesUpdater, > +}; > use crate::sdn::fabric::section_config::protocol::openfabric::{ > OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, > OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater, > @@ -69,6 +73,8 @@ pub enum FabricConfigError { > // this is technically possible, but we don't allow it > #[error("duplicate OSPF area")] > DuplicateOspfArea, > + #[error("BGP router-id collision: multiple nodes resolve to the same router-id {0}")] > + DuplicateBgpRouterId(std::net::Ipv4Addr), > #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")] > OverlappingIp4Prefix(String, String, String, String), > #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")] > @@ -201,6 +207,7 @@ macro_rules! impl_entry { > impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties); > impl_entry!(Ospf, OspfProperties, OspfNodeProperties); > impl_entry!(WireGuard, WireGuardProperties, WireGuardNode); > +impl_entry!(Bgp, BgpProperties, BgpNode); > > /// All possible entries in a [`FabricConfig`]. > /// > @@ -211,6 +218,7 @@ pub enum FabricEntry { > Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>), > Ospf(Entry<OspfProperties, OspfNodeProperties>), > WireGuard(Entry<WireGuardProperties, WireGuardNode>), > + Bgp(Entry<BgpProperties, BgpNode>), > } > > impl FabricEntry { > @@ -225,6 +233,7 @@ impl FabricEntry { > (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => { > entry.add_node(node_section) > } > + (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section), > _ => Err(FabricConfigError::ProtocolMismatch), > } > } > @@ -236,6 +245,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.get_node(id), > FabricEntry::Ospf(entry) => entry.get_node(id), > FabricEntry::WireGuard(entry) => entry.get_node(id), > + FabricEntry::Bgp(entry) => entry.get_node(id), > } > } > > @@ -246,6 +256,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.get_node_mut(id), > FabricEntry::Ospf(entry) => entry.get_node_mut(id), > FabricEntry::WireGuard(entry) => entry.get_node_mut(id), > + FabricEntry::Bgp(entry) => entry.get_node_mut(id), > } > } > > @@ -392,6 +403,8 @@ impl FabricEntry { > _ => continue, > } > } > + > + Ok(()) > } > ( > WireGuardNode::External(external_wire_guard_node), > @@ -422,8 +435,48 @@ impl FabricEntry { > _ => continue, > } > } > + > + Ok(()) > + } > + _ => Err(FabricConfigError::ProtocolMismatch), > + } > + } > + (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => { > + let BgpNode::Internal(ref mut props) = node_section.properties else { > + return Err(FabricConfigError::ProtocolMismatch); > + }; > + > + let NodeDataUpdater::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> { > + ip, > + ip6, > + properties: BgpNodePropertiesUpdater { asn, interfaces }, > + delete, > + } = updater; > + > + if let Some(ip) = ip { > + node_section.ip = Some(ip); > + } > + > + if let Some(ip) = ip6 { > + node_section.ip6 = Some(ip); > + } > + > + if let Some(asn) = asn { > + props.asn = asn; > + } > + > + if let Some(interfaces) = interfaces { > + props.interfaces = interfaces; > + } > + > + for property in delete { > + match property { > + NodeDeletableProperties::Ip => node_section.ip = None, > + NodeDeletableProperties::Ip6 => node_section.ip6 = None, > + NodeDeletableProperties::Protocol( > + BgpNodeDeletableProperties::Interfaces, > + ) => props.interfaces = Vec::new(), > } > - _ => return Err(FabricConfigError::ProtocolMismatch), > } > > Ok(()) > @@ -438,6 +491,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.nodes.iter(), > FabricEntry::Ospf(entry) => entry.nodes.iter(), > FabricEntry::WireGuard(entry) => entry.nodes.iter(), > + FabricEntry::Bgp(entry) => entry.nodes.iter(), > } > } > > @@ -447,6 +501,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.delete_node(id), > FabricEntry::Ospf(entry) => entry.delete_node(id), > FabricEntry::WireGuard(entry) => entry.delete_node(id), > + FabricEntry::Bgp(entry) => entry.delete_node(id), > } > } > > @@ -457,6 +512,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => entry.into_pair(), > FabricEntry::Ospf(entry) => entry.into_pair(), > FabricEntry::WireGuard(entry) => entry.into_pair(), > + FabricEntry::Bgp(entry) => entry.into_pair(), > } > } > > @@ -466,6 +522,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => &entry.fabric, > FabricEntry::Ospf(entry) => &entry.fabric, > FabricEntry::WireGuard(entry) => &entry.fabric, > + FabricEntry::Bgp(entry) => &entry.fabric, > } > } > > @@ -475,6 +532,7 @@ impl FabricEntry { > FabricEntry::Openfabric(entry) => &mut entry.fabric, > FabricEntry::Ospf(entry) => &mut entry.fabric, > FabricEntry::WireGuard(entry) => &mut entry.fabric, > + FabricEntry::Bgp(entry) => &mut entry.fabric, > } > } > } > @@ -487,6 +545,7 @@ impl From<Fabric> for FabricEntry { > } > Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)), > Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)), > + Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)), > } > } > } > @@ -500,6 +559,8 @@ impl Validatable for FabricEntry { > /// - Node IP addresses are within their respective fabric IP prefix ranges > /// - IP addresses are unique across all nodes in the fabric > /// - Each node passes its own validation checks > + /// - For BGP fabrics, derived router-ids are unique across nodes (catches > + /// FNV-1a hash collisions for IPv6-only nodes) > fn validate(&self) -> Result<(), FabricConfigError> { > let fabric = self.fabric(); > > @@ -607,6 +668,27 @@ impl Validatable for FabricEntry { > } > } > > + // Per-node IPs are unique by the checks above. Router-ids can still > + // collide when at least one node falls back to FNV-1a on its IPv6 > + // address (the hash is 32 bits wide, so two distinct IPv6 addresses > + // can map to the same router-id). > + if let FabricEntry::Bgp(bgp_entry) = self { > + let mut seen_router_ids: HashMap<std::net::Ipv4Addr, &NodeId> = HashMap::new(); > + for (node_id, node) in &bgp_entry.nodes { > + let Node::Bgp(node_section) = node else { > + continue; > + }; > + if !matches!(node_section.properties(), BgpNode::Internal(_)) { > + continue; > + } > + if let Some(router_id) = bgp_router_id(node_section) { > + if seen_router_ids.insert(router_id, node_id).is_some() { > + return Err(FabricConfigError::DuplicateBgpRouterId(router_id)); > + } > + } > + } > + } > + > fabric.validate() > } > } > @@ -712,6 +794,15 @@ impl Validatable for FabricConfig { > } > } > } > + Node::Bgp(node_section) => { > + if let BgpNode::Internal(props) = node_section.properties() { > + if !props.interfaces().all(|interface| { > + node_interfaces.insert((node_id, interface.name().as_str())) > + }) { > + return Err(FabricConfigError::DuplicateInterface); > + } > + } > + } > } > } > > @@ -939,6 +1030,80 @@ impl FabricConfig { > > Ok(()) > } > + (Fabric::Bgp(fabric_section), FabricUpdater::Bgp(updater)) => { > + let FabricSectionUpdater::<BgpPropertiesUpdater, BgpDeletableProperties> { > + ip_prefix, > + ip6_prefix, > + properties: > + BgpPropertiesUpdater { > + bfd, > + redistribute, > + route_map_in, > + route_map_out, > + route_filter, > + }, > + delete, > + } = updater; > + > + if let Some(prefix) = ip_prefix { > + fabric_section.ip_prefix = Some(prefix); > + } > + > + if let Some(prefix) = ip6_prefix { > + fabric_section.ip6_prefix = Some(prefix); > + } > + > + if let Some(bfd) = bfd { > + fabric_section.properties.bfd = bfd; > + } > + > + if let Some(redistribute) = redistribute { > + fabric_section.properties.redistribute = redistribute; > + } > + > + if let Some(route_map_in) = route_map_in { > + fabric_section.properties.route_map_in = Some(route_map_in); > + } > + > + if let Some(route_map_out) = route_map_out { > + fabric_section.properties.route_map_out = Some(route_map_out); > + } > + > + if let Some(route_filter) = route_filter { > + fabric_section.properties.route_filter = Some(route_filter); > + } > + > + for property in delete { > + match property { > + FabricDeletableProperties::IpPrefix => { > + fabric_section.ip_prefix = None; > + } > + FabricDeletableProperties::Ip6Prefix => { > + fabric_section.ip6_prefix = None; > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::Redistribute, > + ) => { > + fabric_section.properties.redistribute = Vec::new(); > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::RouteFilter, > + ) => { > + fabric_section.properties.route_filter = None; > + } > + FabricDeletableProperties::Protocol(BgpDeletableProperties::RouteMapIn) => { > + fabric_section.properties.route_map_in = None; > + } > + FabricDeletableProperties::Protocol( > + BgpDeletableProperties::RouteMapOut, > + ) => { > + fabric_section.properties.route_map_out = None; > + } > + } > + } > + > + Ok(()) > + } > _ => Err(FabricConfigError::ProtocolMismatch), > } > } > diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs > index e92074c..efa186a 100644 > --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs > @@ -8,6 +8,9 @@ use proxmox_schema::{ > }; > > use crate::common::valid::Validatable; > +use crate::sdn::fabric::section_config::protocol::bgp::{ > + BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater, > +}; > use crate::sdn::fabric::section_config::protocol::openfabric::{ > OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater, > }; > @@ -147,6 +150,10 @@ impl UpdaterType for FabricSection<WireGuardProperties> { > type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>; > } > > +impl UpdaterType for FabricSection<BgpProperties> { > + type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>; > +} > + > /// Enum containing all types of fabrics. > /// > /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the > @@ -169,6 +176,7 @@ pub enum Fabric { > Ospf(FabricSection<OspfProperties>), > #[serde(rename = "wireguard")] > WireGuard(FabricSection<WireGuardProperties>), > + Bgp(FabricSection<BgpProperties>), > } > > impl UpdaterType for Fabric { > @@ -184,6 +192,7 @@ impl Fabric { > Self::Openfabric(fabric_section) => fabric_section.id(), > Self::Ospf(fabric_section) => fabric_section.id(), > Self::WireGuard(fabric_section) => fabric_section.id(), > + Self::Bgp(fabric_section) => fabric_section.id(), > } > } > > @@ -195,6 +204,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(), > Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(), > Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(), > + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(), > } > } > > @@ -206,6 +216,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > + Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr), > } > } > > @@ -217,6 +228,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(), > Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(), > Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(), > + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(), > } > } > > @@ -228,6 +240,7 @@ impl Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > + Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr), > } > } > } > @@ -241,6 +254,7 @@ impl Validatable for Fabric { > Fabric::Openfabric(fabric_section) => fabric_section.validate(), > Fabric::Ospf(fabric_section) => fabric_section.validate(), > Fabric::WireGuard(_fabric_section) => Ok(()), > + Fabric::Bgp(fabric_section) => fabric_section.validate(), > } > } > } > @@ -263,6 +277,12 @@ impl From<FabricSection<WireGuardProperties>> for Fabric { > } > } > > +impl From<FabricSection<BgpProperties>> for Fabric { > + fn from(section: FabricSection<BgpProperties>) -> Self { > + Fabric::Bgp(section) > + } > +} > + > /// Enum containing all updater types for fabrics > #[derive(Debug, Clone, Serialize, Deserialize)] > #[serde(rename_all = "snake_case", tag = "protocol")] > @@ -271,6 +291,7 @@ pub enum FabricUpdater { > Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater), > #[serde(rename = "wireguard")] > WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater), > + Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater), > } > > impl Updater for FabricUpdater { > @@ -279,6 +300,7 @@ impl Updater for FabricUpdater { > FabricUpdater::Openfabric(updater) => updater.is_empty(), > FabricUpdater::Ospf(updater) => updater.is_empty(), > FabricUpdater::WireGuard(updater) => updater.is_empty(), > + FabricUpdater::Bgp(updater) => updater.is_empty(), > } > } > } > diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs > index f47a522..f85c547 100644 > --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs > @@ -11,6 +11,7 @@ use crate::sdn::fabric::section_config::{ > fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR}, > node::{Node, NodeSection, NODE_ID_REGEX_STR}, > protocol::{ > + bgp::{BgpNode, BgpProperties}, > openfabric::{OpenfabricNodeProperties, OpenfabricProperties}, > ospf::{OspfNodeProperties, OspfProperties}, > wireguard::WireGuardNode, > @@ -34,9 +35,11 @@ impl From<Section> for FabricOrNode<Fabric, Node> { > Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()), > Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()), > Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()), > + Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()), > + Section::WireGuardNode(node_section) => Self::Node(node_section.into()), > Section::OpenfabricNode(node_section) => Self::Node(node_section.into()), > Section::OspfNode(node_section) => Self::Node(node_section.into()), > - Section::WireGuardNode(node_section) => Self::Node(node_section.into()), > + Section::BgpNode(node_section) => Self::Node(node_section.into()), > } > } > } > @@ -68,10 +71,12 @@ pub enum Section { > OspfFabric(FabricSection<OspfProperties>), > #[serde(rename = "wireguard_fabric")] > WireGuardFabric(FabricSection<WireGuardProperties>), > + BgpFabric(FabricSection<BgpProperties>), > OpenfabricNode(NodeSection<OpenfabricNodeProperties>), > OspfNode(NodeSection<OspfNodeProperties>), > #[serde(rename = "wireguard_node")] > WireGuardNode(NodeSection<WireGuardNode>), > + BgpNode(NodeSection<BgpNode>), > } > > impl From<FabricSection<OpenfabricProperties>> for Section { > @@ -92,6 +97,12 @@ impl From<FabricSection<WireGuardProperties>> for Section { > } > } > > +impl From<FabricSection<BgpProperties>> for Section { > + fn from(section: FabricSection<BgpProperties>) -> Self { > + Self::BgpFabric(section) > + } > +} > + > impl From<NodeSection<OpenfabricNodeProperties>> for Section { > fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self { > Self::OpenfabricNode(section) > @@ -110,12 +121,19 @@ impl From<NodeSection<WireGuardNode>> for Section { > } > } > > +impl From<NodeSection<BgpNode>> for Section { > + fn from(section: NodeSection<BgpNode>) -> Self { > + Self::BgpNode(section) > + } > +} > + > impl From<Fabric> for Section { > fn from(fabric: Fabric) -> Self { > match fabric { > Fabric::Openfabric(fabric_section) => fabric_section.into(), > Fabric::Ospf(fabric_section) => fabric_section.into(), > Fabric::WireGuard(fabric_section) => fabric_section.into(), > + Fabric::Bgp(fabric_section) => fabric_section.into(), > } > } > } > @@ -126,6 +144,7 @@ impl From<Node> for Section { > Node::Openfabric(node_section) => node_section.into(), > Node::Ospf(node_section) => node_section.into(), > Node::WireGuard(node_section) => node_section.into(), > + Node::Bgp(node_section) => node_section.into(), > } > } > } > diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs > index f2300ac..af15898 100644 > --- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs > @@ -10,6 +10,7 @@ use proxmox_schema::{ > }; > > use crate::common::valid::Validatable; > +use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties}; > use crate::sdn::fabric::section_config::protocol::wireguard::WireGuardNode; > use crate::sdn::fabric::section_config::{ > fabric::{FabricId, FABRIC_ID_REGEX_STR}, > @@ -191,6 +192,7 @@ pub enum Node { > Ospf(NodeSection<OspfNodeProperties>), > #[serde(rename = "wireguard")] > WireGuard(NodeSection<WireGuardNode>), > + Bgp(NodeSection<BgpNode>), > } > > impl Node { > @@ -200,6 +202,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.id(), > Node::Ospf(node_section) => node_section.id(), > Node::WireGuard(node_section) => node_section.id(), > + Node::Bgp(node_section) => node_section.id(), > } > } > > @@ -209,6 +212,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.ip(), > Node::Ospf(node_section) => node_section.ip(), > Node::WireGuard(node_section) => node_section.ip(), > + Node::Bgp(node_section) => node_section.ip(), > } > } > > @@ -218,6 +222,7 @@ impl Node { > Node::Openfabric(node_section) => node_section.ip6(), > Node::Ospf(node_section) => node_section.ip6(), > Node::WireGuard(node_section) => node_section.ip6(), > + Node::Bgp(node_section) => node_section.ip6(), > } > } > } > @@ -230,6 +235,7 @@ impl Validatable for Node { > Node::Openfabric(node_section) => node_section.validate(), > Node::Ospf(node_section) => node_section.validate(), > Node::WireGuard(node_section) => node_section.validate(), > + Node::Bgp(node_section) => node_section.validate(), > } > } > } > @@ -252,6 +258,12 @@ impl From<NodeSection<WireGuardNode>> for Node { > } > } > > +impl From<NodeSection<BgpNode>> for Node { > + fn from(value: NodeSection<BgpNode>) -> Self { > + Self::Bgp(value) > + } > +} > + > /// API types for SDN fabric node configurations. > /// > /// This module provides specialized types that are used for API interactions when retrieving, > @@ -273,6 +285,7 @@ pub mod api { > use proxmox_schema::{Updater, UpdaterType}; > > use crate::sdn::fabric::section_config::protocol::{ > + bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater}, > openfabric::{ > OpenfabricNodeDeletableProperties, OpenfabricNodeProperties, > OpenfabricNodePropertiesUpdater, > @@ -338,6 +351,7 @@ pub mod api { > Ospf(NodeData<OspfNodeProperties>), > #[serde(rename = "wireguard")] > WireGuard(NodeData<WireGuardNode>), > + Bgp(NodeData<BgpNode>), > } > > impl From<super::Node> for Node { > @@ -346,6 +360,7 @@ pub mod api { > super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), > super::Node::Ospf(node_section) => Self::Ospf(node_section.into()), > super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), > + super::Node::Bgp(node_section) => Self::Bgp(node_section.into()), > } > } > } > @@ -356,6 +371,7 @@ pub mod api { > Node::Openfabric(node_section) => Self::Openfabric(node_section.into()), > Node::Ospf(node_section) => Self::Ospf(node_section.into()), > Node::WireGuard(node_section) => Self::WireGuard(node_section.into()), > + Node::Bgp(node_section) => Self::Bgp(node_section.into()), > } > } > } > @@ -373,6 +389,10 @@ pub mod api { > type Updater = NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>; > } > > + impl UpdaterType for NodeData<BgpNodeProperties> { > + type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>; > + } > + > #[derive(Debug, Clone, Serialize, Deserialize)] > pub struct NodeDataUpdater<T, D> { > #[serde(skip_serializing_if = "Option::is_none")] > @@ -410,6 +430,7 @@ pub mod api { > Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>), > #[serde(rename = "wireguard")] > WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>), > + Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>), > } > > #[derive(Debug, Clone, Serialize, Deserialize)] > diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs > new file mode 100644 > index 0000000..f6f55e2 > --- /dev/null > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs > @@ -0,0 +1,317 @@ > +use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr}; > +use std::ops::{Deref, DerefMut}; > + > +use proxmox_network_types::ip_address::api_types::Ipv4Addr; > +use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType}; > +use serde::{Deserialize, Serialize}; > + > +use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater}; > + > +use crate::common::valid::Validatable; > +use crate::sdn::fabric::section_config::fabric::FabricSection; > +use crate::sdn::fabric::section_config::interface::InterfaceName; > +use crate::sdn::fabric::section_config::node::NodeSection; > +use crate::sdn::fabric::FabricConfigError; > + > +use crate::sdn::prefix_list::PrefixListId; > +use crate::sdn::route_map::RouteMapId; > + > +#[api] > +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] > +#[serde(rename_all = "lowercase")] > +/// Redistribution Sources for BGP fabric > +pub enum BgpRedistributionSource { > + /// redistribute connected routes > + Connected, > + /// redistribute IS-IS routes > + Isis, > + /// redistribute kernel routes > + Kernel, > + /// redistribute openfabric routes > + Openfabric, > + /// redistribute ospfv2 routes > + Ospf, > + /// redistribute ospfv3 routes > + Ospf6, > + /// redistribute static routes > + Static, > +} > + > +#[api] > +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] > +/// A BGP redistribution target > +pub struct BgpRedistribution { > + /// The source used for redistribution > + pub(crate) source: BgpRedistributionSource, > + /// The metric to apply to redistributed routes > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) metric: Option<u32>, > + /// Route MAP to use for filtering redistributed routes > + #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")] > + pub(crate) route_map: Option<RouteMapId>, > +} > + > +#[api( > + type: Integer, > + minimum: u32::MIN as i64, > + maximum: u32::MAX as i64, > +)] > +#[derive(Debug, Clone, Serialize, Updater, Hash)] > +/// Autonomous system number as defined by RFC 6793 > +pub struct ASN(u32); potentially something for later, but I think we use ASN in several places - so we might want to add this to sdn-types instead and reuse it across the ve-rs crates? > +impl<'de> Deserialize<'de> for ASN { > + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> > + where > + D: serde::Deserializer<'de>, > + { > + use serde::de::{self, Visitor}; > + > + struct AsnVisitor; > + > + impl<'de> Visitor<'de> for AsnVisitor { > + type Value = ASN; > + > + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { > + formatter.write_str("a u32 or string containing a u32") > + } > + > + fn visit_i64<E: de::Error>(self, v: i64) -> Result<ASN, E> { > + u32::try_from(v) > + .map(ASN) > + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) > + } > + > + fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> { > + u32::try_from(v) > + .map(ASN) > + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) > + } > + > + fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> { > + v.parse::<u32>() > + .map(ASN) > + .map_err(|_| E::custom(format!("invalid ASN: {v}"))) > + } > + } > + > + deserializer.deserialize_any(AsnVisitor) > + } > +} Is there a reason why proxmox_serde::perl::deserialize_u32 doesn't work? It should work the same afaict. > +impl UpdaterType for ASN { > + type Updater = Option<ASN>; > +} > + > +impl ASN { > + pub fn as_u32(&self) -> u32 { > + self.0 > + } > +} > + > +#[api( > + properties: { > + redistribute: { > + type: Array, > + optional: true, > + items: { > + type: String, > + description: "A BGP redistribution source", > + format: &ApiStringFormat::PropertyString(&BgpRedistribution::API_SCHEMA), > + } > + } > + }, > +)] > +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] > +/// Properties for an Bgp fabric. > +pub struct BgpProperties { > + /// enable BFD for this fabric > + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] > + pub(crate) bfd: bool, > + /// redistribution configuration for this fabric > + #[serde(default, skip_serializing_if = "Vec::is_empty")] > + #[updater(serde(skip_serializing_if = "Option::is_none"))] > + pub(crate) redistribute: Vec<PropertyString<BgpRedistribution>>, > + > + /// Route map to apply for incoming routes > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_map_in: Option<RouteMapId>, > + > + /// Route map to apply for outgoing routes > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_map_out: Option<RouteMapId>, > + > + /// By default only routes from the configured IP prefix are imported > + /// into the local routing table. This setting can be used to override the > + /// allowed IPs and import additional routes besides the configured IP > + /// prefix. > + #[serde(skip_serializing_if = "Option::is_none")] > + pub(crate) route_filter: Option<PrefixListId>, > +} > + > +impl BgpProperties { > + pub fn bfd(&self) -> bool { > + self.bfd > + } > +} > + > +impl Validatable for FabricSection<BgpProperties> { > + type Error = FabricConfigError; > + > + /// Validate the [`FabricSection<BgpProperties>`]. > + fn validate(&self) -> Result<(), Self::Error> { > + if self.ip_prefix().is_none() && self.ip6_prefix().is_none() { > + return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string())); > + } > + > + Ok(()) > + } > +} > + > +#[derive(Debug, Clone, Serialize, Deserialize)] > +#[serde(rename_all = "snake_case")] > +pub enum BgpDeletableProperties { > + Redistribute, > + RouteFilter, > + RouteMapIn, > + RouteMapOut, > +} > + > +#[api] > +/// External Bgp Node > +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] > +pub struct ExternalBgpNode { > + peer_ip: Option<Ipv4Addr>, > +} > + > +#[derive(Debug, Clone, Serialize, Deserialize, Hash)] > +#[serde(rename_all = "snake_case", tag = "role")] > +pub enum BgpNode { > + Internal(BgpNodeProperties), > + External(ExternalBgpNode), > +} > + > +impl ApiType for BgpNode { > + const API_SCHEMA: Schema = OneOfSchema::new( > + "BGP node", > + &( > + "role", > + false, > + &StringSchema::new("internal or external").schema(), > + ), > + &[ > + ("external", &ExternalBgpNode::API_SCHEMA), > + ("internal", &BgpNodeProperties::API_SCHEMA), > + ], > + ) > + .schema(); > +} > + > +impl Validatable for NodeSection<BgpNode> { > + type Error = FabricConfigError; > + > + fn validate(&self) -> Result<(), Self::Error> { > + Ok(()) > + } > +} > + > +#[api( > + properties: { > + interfaces: { > + type: Array, > + optional: true, > + items: { > + type: String, > + description: "Properties for an Bgp interface.", > + format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA), > + } > + }, > + } > +)] > +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] > +/// Properties for an Bgp node. > +pub struct BgpNodeProperties { > + /// Autonomous system number for this Node > + pub(crate) asn: ASN, > + /// Interfaces for this Node. > + #[serde(default)] > + pub(crate) interfaces: Vec<PropertyString<BgpInterfaceProperties>>, > +} > + > +impl BgpNodeProperties { > + /// Returns the ASN for this node. > + pub fn asn(&self) -> &ASN { > + &self.asn > + } > + > + /// Returns an iterator over all the interfaces. > + pub fn interfaces(&self) -> impl Iterator<Item = &BgpInterfaceProperties> { > + self.interfaces > + .iter() > + .map(|property_string| property_string.deref()) > + } > + > + /// Returns an iterator over all the interfaces (mutable). > + pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut BgpInterfaceProperties> { > + self.interfaces > + .iter_mut() > + .map(|property_string| property_string.deref_mut()) > + } > +} > + > +impl Validatable for NodeSection<BgpNodeProperties> { > + type Error = FabricConfigError; > + > + /// Validate the [`NodeSection<BgpNodeProperties>`]. > + fn validate(&self) -> Result<(), Self::Error> { > + Ok(()) > + } > +} > + > +#[derive(Debug, Clone, Serialize, Deserialize)] > +#[serde(rename_all = "snake_case", untagged)] > +pub enum BgpNodeDeletableProperties { > + Interfaces, > +} > + > +#[api] > +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)] > +/// Properties for an BGP interface. > +pub struct BgpInterfaceProperties { > + pub(crate) name: InterfaceName, > +} > + > +impl BgpInterfaceProperties { > + /// Get the name of the BGP interface. > + pub fn name(&self) -> &InterfaceName { > + &self.name > + } > + > + /// Set the name of the interface. > + pub fn set_name(&mut self, name: InterfaceName) { > + self.name = name > + } > +} > + > +/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a. > +/// > +/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the > +/// full 16 octets down to 4 bytes. Typical loopback allocations (sequential > +/// within a prefix, sparse across /48s) produce zero collisions up to 100k > +/// nodes in testing -- well below the random birthday bound (~1% at 10k) > +/// because structured addresses spread well under FNV-1a. > +pub fn router_id_from_ipv6(addr: &Ipv6Addr) -> StdIpv4Addr { > + let mut hash: u32 = 0x811c9dc5; > + for &byte in &addr.octets() { > + hash ^= byte as u32; > + hash = hash.wrapping_mul(0x01000193); > + } > + StdIpv4Addr::from(hash) > +} > + > +/// Resolves the BGP router-id for a node: the IPv4 address if set, > +/// otherwise an FNV-1a hash of the IPv6 address. > +pub fn bgp_router_id(node: &NodeSection<BgpNode>) -> Option<StdIpv4Addr> { > + node.ip() > + .or_else(|| node.ip6().map(|ipv6| router_id_from_ipv6(&ipv6))) > +} > diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs > index fd77426..c7adf0f 100644 > --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs > +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs > @@ -1,3 +1,4 @@ > +pub mod bgp; > pub mod openfabric; > pub mod ospf; > pub mod wireguard; > diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg > new file mode 100644 > index 0000000..bd434a7 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg > @@ -0,0 +1,17 @@ > +bgp_fabric: test > + bfd 0 > + ip_prefix 10.10.10.0/24 > + > +bgp_node: test_pve > + asn 65001 > + interfaces name=ens18 > + interfaces name=ens19 > + ip 10.10.10.1 > + role internal > + > +bgp_node: test_pve1 > + asn 65002 > + interfaces name=ens19 > + ip 10.10.10.2 > + role internal > + > diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg > new file mode 100644 > index 0000000..f4581fb > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg > @@ -0,0 +1,17 @@ > +bgp_fabric: test > + bfd 0 > + ip6_prefix fd00:10::/64 > + > +bgp_node: test_pve > + asn 65001 > + interfaces name=ens18 > + interfaces name=ens19 > + ip6 fd00:10::1 > + role internal > + > +bgp_node: test_pve1 > + asn 65002 > + interfaces name=ens19 > + ip6 fd00:10::2 > + role internal > + > diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs > index 95b2e62..49c5fcc 100644 > --- a/proxmox-ve-config/tests/fabric/main.rs > +++ b/proxmox-ve-config/tests/fabric/main.rs > @@ -1,7 +1,9 @@ > #![cfg(feature = "frr")] > +use std::net::Ipv4Addr; > use std::str::FromStr; > > -use proxmox_frr::ser::{serializer::dump, FrrConfig}; > +use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF}; > +use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName}; > use proxmox_ve_config::sdn::fabric::{ > frr::build_fabric, section_config::node::NodeId, FabricConfig, > }; > @@ -162,3 +164,118 @@ fn openfabric_ipv6_only() { > > insta::assert_snapshot!(helper::reference_name!("pve"), output); > } > + > +#[test] > +fn bgp_default() { > + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); > + let mut frr_config = FrrConfig::default(); > + > + build_fabric( > + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), > + config.clone(), > + &mut frr_config, > + ) > + .unwrap(); > + > + let mut output = dump(&frr_config).expect("error dumping stuff"); > + > + insta::assert_snapshot!(helper::reference_name!("pve"), output); > + > + frr_config = FrrConfig::default(); > + build_fabric( > + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), > + config, > + &mut frr_config, > + ) > + .unwrap(); > + > + output = dump(&frr_config).expect("error dumping stuff"); > + > + insta::assert_snapshot!(helper::reference_name!("pve1"), output); > +} > + > +#[test] > +fn bgp_ipv6_only() { > + let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap(); > + let mut frr_config = FrrConfig::default(); > + > + build_fabric( > + NodeId::from_string("pve".to_owned()).expect("invalid nodeid"), > + config.clone(), > + &mut frr_config, > + ) > + .unwrap(); > + > + let mut output = dump(&frr_config).expect("error dumping stuff"); > + > + insta::assert_snapshot!(helper::reference_name!("pve"), output); > + > + frr_config = FrrConfig::default(); > + build_fabric( > + NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"), > + config, > + &mut frr_config, > + ) > + .unwrap(); > + > + output = dump(&frr_config).expect("error dumping stuff"); > + > + insta::assert_snapshot!(helper::reference_name!("pve1"), output); > +} > + > +/// Test that build_fabric merges into an existing EVPN router and sets local-as > +/// when the ASNs differ. > +#[test] > +fn bgp_merge_with_evpn() { > + let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg") > + .expect("cannot find config file"); > + let config = FabricConfig::parse_section_config(&raw).unwrap(); > + > + // Pre-populate with an EVPN-like router using a different ASN > + let mut frr_config = FrrConfig::default(); > + let evpn_router = BgpRouter { > + asn: 65000, > + router_id: Ipv4Addr::new(10, 10, 10, 1), > + coalesce_time: Some(1000), > + default_ipv4_unicast: Some(false), > + hard_administrative_reset: None, > + graceful_restart_notification: None, > + disable_ebgp_connected_route_check: None, > + bestpath_as_path_multipath_relax: None, > + neighbor_groups: Vec::new(), > + address_families: AddressFamilies { > + ipv4_unicast: None, > + ipv6_unicast: None, > + l2vpn_evpn: Some(L2vpnEvpnAF { > + common_options: CommonAddressFamilyOptions { > + import_vrf: Vec::new(), > + neighbors: Vec::new(), > + custom_frr_config: Vec::new(), > + }, > + advertise_all_vni: Some(true), > + advertise_default_gw: None, > + default_originate: Vec::new(), > + advertise_ipv4_unicast: None, > + advertise_ipv6_unicast: None, > + autort_as: None, > + route_targets: None, > + }), > + }, > + custom_frr_config: Vec::new(), > + }; > + frr_config > + .bgp > + .vrf_router > + .insert(VrfName::Default, evpn_router); > + > + build_fabric( > + NodeId::from_str("pve").expect("invalid nodeid"), > + config, > + &mut frr_config, > + ) > + .unwrap(); > + > + let output = dump(&frr_config).expect("error dumping stuff"); > + > + insta::assert_snapshot!(helper::reference_name!("pve"), output); > +} > diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap > new file mode 100644 > index 0000000..0db0034 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap > @@ -0,0 +1,36 @@ > +--- > +source: proxmox-ve-config/tests/fabric/main.rs > +expression: output > +--- > +! > +router bgp 65001 > + bgp router-id 10.10.10.1 > + no bgp default ipv4-unicast > + neighbor test peer-group > + neighbor test remote-as external > + neighbor ens18 interface peer-group test > + neighbor ens19 interface peer-group test > + ! > + address-family ipv4 unicast > + network 10.10.10.1/32 > + neighbor test activate > + neighbor test soft-reconfiguration inbound > + neighbor test route-map pve_bgp_test_in in > + exit-address-family > +exit > +! > +access-list pve_bgp_test_ips permit 10.10.10.0/24 > +! > +route-map pve_bgp permit 100 > + match ip address pve_bgp_test_ips > + set src 10.10.10.1 > +exit > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +exit > +! > +ip protocol bgp route-map pve_bgp > diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap > new file mode 100644 > index 0000000..d7ed018 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap > @@ -0,0 +1,35 @@ > +--- > +source: proxmox-ve-config/tests/fabric/main.rs > +expression: output > +--- > +! > +router bgp 65002 > + bgp router-id 10.10.10.2 > + no bgp default ipv4-unicast > + neighbor test peer-group > + neighbor test remote-as external > + neighbor ens19 interface peer-group test > + ! > + address-family ipv4 unicast > + network 10.10.10.2/32 > + neighbor test activate > + neighbor test soft-reconfiguration inbound > + neighbor test route-map pve_bgp_test_in in > + exit-address-family > +exit > +! > +access-list pve_bgp_test_ips permit 10.10.10.0/24 > +! > +route-map pve_bgp permit 100 > + match ip address pve_bgp_test_ips > + set src 10.10.10.2 > +exit > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +exit > +! > +ip protocol bgp route-map pve_bgp > diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap > new file mode 100644 > index 0000000..8dbb36b > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap > @@ -0,0 +1,37 @@ > +--- > +source: proxmox-ve-config/tests/fabric/main.rs > +expression: output > +--- > +! > +router bgp 65001 > + bgp router-id 5.76.46.251 > + no bgp default ipv4-unicast > + neighbor test peer-group > + neighbor test remote-as external > + neighbor ens18 interface peer-group test > + neighbor ens19 interface peer-group test > + ! > + address-family ipv6 unicast > + network fd00:10::1/128 > + neighbor test activate > + neighbor test soft-reconfiguration inbound > + neighbor test route-map pve_bgp6_test_in in > + exit-address-family > +exit > +! > +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 > +! > +route-map pve_bgp6 permit 100 > + match ipv6 address pve_bgp_test_ip6s > + set src fd00:10::1 > +exit > +! > +route-map pve_bgp6 permit 65535 > +exit > +! > +route-map pve_bgp6_test_in permit 10 > + match ipv6 address pve_bgp_test_ip6s > +exit > +! > +! > +ipv6 protocol bgp route-map pve_bgp6 > diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap > new file mode 100644 > index 0000000..a091148 > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap > @@ -0,0 +1,36 @@ > +--- > +source: proxmox-ve-config/tests/fabric/main.rs > +expression: output > +--- > +! > +router bgp 65002 > + bgp router-id 6.76.48.142 > + no bgp default ipv4-unicast > + neighbor test peer-group > + neighbor test remote-as external > + neighbor ens19 interface peer-group test > + ! > + address-family ipv6 unicast > + network fd00:10::2/128 > + neighbor test activate > + neighbor test soft-reconfiguration inbound > + neighbor test route-map pve_bgp6_test_in in > + exit-address-family > +exit > +! > +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64 > +! > +route-map pve_bgp6 permit 100 > + match ipv6 address pve_bgp_test_ip6s > + set src fd00:10::2 > +exit > +! > +route-map pve_bgp6 permit 65535 > +exit > +! > +route-map pve_bgp6_test_in permit 10 > + match ipv6 address pve_bgp_test_ip6s > +exit > +! > +! > +ipv6 protocol bgp route-map pve_bgp6 > diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap > new file mode 100644 > index 0000000..226337f > --- /dev/null > +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap > @@ -0,0 +1,42 @@ > +--- > +source: proxmox-ve-config/tests/fabric/main.rs > +expression: output > +--- > +! > +router bgp 65000 > + bgp router-id 10.10.10.1 > + no bgp default ipv4-unicast > + coalesce-time 1000 > + neighbor test peer-group > + neighbor test remote-as external > + neighbor test local-as 65001 no-prepend replace-as > + neighbor ens18 interface peer-group test > + neighbor ens19 interface peer-group test > + ! > + address-family ipv4 unicast > + network 10.10.10.1/32 > + neighbor test activate > + neighbor test soft-reconfiguration inbound > + neighbor test route-map pve_bgp_test_in in > + exit-address-family > + ! > + address-family l2vpn evpn > + advertise-all-vni > + exit-address-family > +exit > +! > +access-list pve_bgp_test_ips permit 10.10.10.0/24 > +! > +route-map pve_bgp permit 100 > + match ip address pve_bgp_test_ips > + set src 10.10.10.1 > +exit > +! > +route-map pve_bgp permit 65535 > +exit > +! > +route-map pve_bgp_test_in permit 10 > + match ip address pve_bgp_test_ips > +exit > +! > +ip protocol bgp route-map pve_bgp ^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support 2026-05-13 12:29 ` Stefan Hanreich @ 2026-05-13 13:00 ` Hannes Laimer 0 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-13 13:00 UTC (permalink / raw) To: Stefan Hanreich, pve-devel thanks, will address in v5! On 2026-05-13 14:30, Stefan Hanreich wrote: > On 5/12/26 4:12 PM, Hannes Laimer wrote: >> +impl AddressFamilies { >> + /// Merge another [`AddressFamilies`] into this one. >> + /// >> + /// For each address family: if `self` already has it, extend its neighbors, networks, and >> + /// redistribute lists. If `self` doesn't have it, take it from `other`. >> + pub fn merge(&mut self, other: AddressFamilies) { > > maybe extend is the better name, as it mirrors existing conventions from > std::Vec? > makes sense, can do [..] >> } >> FabricEntry::WireGuard(_) => {} // not a frr fabric >> + FabricEntry::Bgp(bgp_entry) => { >> + let Ok(node) = bgp_entry.node_section(¤t_node) else { >> + continue; >> + }; >> + >> + let BgpNode::Internal(properties) = node.properties() else { >> + continue; >> + }; >> + >> + let fabric = bgp_entry.fabric_section(); >> + >> + let local_asn = properties.asn().as_u32(); > > makes me wonder if implementing AsRef<u32> would simplify handling ASN > throughout the code? > good point, probably also `Deref<Target=u32>` [..] >> + >> +#[api( >> + type: Integer, >> + minimum: u32::MIN as i64, >> + maximum: u32::MAX as i64, >> +)] >> +#[derive(Debug, Clone, Serialize, Updater, Hash)] >> +/// Autonomous system number as defined by RFC 6793 >> +pub struct ASN(u32); > > potentially something for later, but I think we use ASN in several > places - so we might want to add this to sdn-types instead and reuse it > across the ve-rs crates? > yes, sounds like a good follow-up that covers all places then [..] >> +impl<'de> Deserialize<'de> for ASN { >> + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> >> + where >> + D: serde::Deserializer<'de>, >> + { >> + use serde::de::{self, Visitor}; >> + >> + struct AsnVisitor; >> + >> + impl<'de> Visitor<'de> for AsnVisitor { >> + type Value = ASN; >> + >> + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { >> + formatter.write_str("a u32 or string containing a u32") >> + } >> + >> + fn visit_i64<E: de::Error>(self, v: i64) -> Result<ASN, E> { >> + u32::try_from(v) >> + .map(ASN) >> + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) >> + } >> + >> + fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> { >> + u32::try_from(v) >> + .map(ASN) >> + .map_err(|_| E::custom(format!("ASN out of range: {v}"))) >> + } >> + >> + fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> { >> + v.parse::<u32>() >> + .map(ASN) >> + .map_err(|_| E::custom(format!("invalid ASN: {v}"))) >> + } >> + } >> + >> + deserializer.deserialize_any(AsnVisitor) >> + } >> +} > > Is there a reason why proxmox_serde::perl::deserialize_u32 doesn't work? > It should work the same afaict. > i remeber there was a problem with the information that this is an int being lost when passed through perlmod, but i tried to reproduce this again and could not. so no reason :) [..] ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer 2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer ` (6 subsequent siblings) 8 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel From: Stefan Hanreich <s.hanreich@proxmox.com> Add BGP support to the fabric config generation pipeline. This includes interface name mapping and network interface output for BGP unnumbered links. Co-authored-by: Hannes Laimer <h.laimer@proxmox.com> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com> --- pve-rs/src/bindings/sdn/fabrics.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index 1ebfde1..f914094 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -32,6 +32,7 @@ pub mod pve_rs_sdn_fabrics { use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName; use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId}; use proxmox_ve_config::sdn::fabric::section_config::Section; + use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry}; use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder; @@ -406,6 +407,15 @@ pub mod pve_rs_sdn_fabrics { } } ConfigNode::WireGuard(_) => {} + ConfigNode::Bgp(node_section) => { + if let BgpNode::Internal(properties) = node_section.properties_mut() { + for interface in properties.interfaces_mut() { + if let Some(mapped_name) = map_name(&mapping, interface.name())? { + interface.set_name(mapped_name); + } + } + } + } } } @@ -499,6 +509,8 @@ pub mod pve_rs_sdn_fabrics { daemons.insert("fabricd"); } FabricEntry::WireGuard(_) => {} // not a frr fabric + // bgpd is enabled by default in /etc/frr/daemons + FabricEntry::Bgp(_) => {} }; } @@ -719,6 +731,22 @@ pub mod pve_rs_sdn_fabrics { } } } + ConfigNode::Bgp(node_section) => { + if let BgpNode::Internal(properties) = node_section.properties() { + for interface in properties.interfaces() { + let name = interface.name(); + writeln!(interfaces)?; + writeln!(interfaces, "auto {name}")?; + writeln!(interfaces, "iface {name} inet manual")?; + writeln!(interfaces, "\tip-forward 1")?; + writeln!(interfaces, "\tip6-forward 1")?; + // BGP unnumbered uses RAs to discover peer link-local + // addresses. frr listens for them itself, but the kernel + // would otherwise install RA-derived routes we don't want. + writeln!(interfaces, "\taccept-ra 0")?; + } + } + } } } -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer 2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer 2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-13 12:33 ` Stefan Hanreich 2026-05-12 14:13 ` [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer ` (5 subsequent siblings) 8 siblings, 1 reply; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel Expose BGP fabric status through the existing fabric status API. Routes are fetched for both IPv4 and IPv6, and neighbor/interface state is derived from BGP session info. Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- pve-rs/src/bindings/sdn/fabrics.rs | 97 +++++++++++++++++++++++++- pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++++++- 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs index f914094..0189ecd 100644 --- a/pve-rs/src/bindings/sdn/fabrics.rs +++ b/pve-rs/src/bindings/sdn/fabrics.rs @@ -12,8 +12,8 @@ pub mod pve_rs_sdn_fabrics { use std::process::Command; use std::sync::Mutex; - use anyhow::{Context, Error, format_err}; - use openssl::hash::{MessageDigest, hash}; + use anyhow::{format_err, Context, Error}; + use openssl::hash::{hash, MessageDigest}; use proxmox_ve_config::sdn::fabric::section_config::node::api::{Node, NodeUpdater}; use serde::{Deserialize, Serialize}; @@ -31,8 +31,8 @@ pub mod pve_rs_sdn_fabrics { }; use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName; use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId}; - use proxmox_ve_config::sdn::fabric::section_config::Section; use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; + use proxmox_ve_config::sdn::fabric::section_config::Section; use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry}; use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder; @@ -840,6 +840,35 @@ pub mod pve_rs_sdn_fabrics { status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename()) } FabricEntry::WireGuard(_) => Ok(Vec::new()), + FabricEntry::Bgp(_) => { + let bgp_ipv4_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route bgp json'"]) + .output()? + .stdout, + )?; + + let bgp_ipv6_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) + .output()? + .stdout, + )?; + + let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&bgp_ipv4_routes_string) + .with_context(|| "error parsing bgp ipv4 routes")? + }; + if !bgp_ipv6_routes_string.is_empty() { + let bgp_ipv6_routes: proxmox_frr::de::Routes = + serde_json::from_str(&bgp_ipv6_routes_string) + .with_context(|| "error parsing bgp ipv6 routes")?; + bgp_routes.0.extend(bgp_ipv6_routes.0); + } + status::get_routes(fabric_id, config, bgp_routes, proxmox_sys::nodename()) + } } } @@ -899,6 +928,23 @@ pub mod pve_rs_sdn_fabrics { .map(|v| v.into()) } FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())), + FabricEntry::Bgp(_) => { + let bgp_neighbors_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) + .output()? + .stdout, + )?; + let bgp_neighbors: std::collections::BTreeMap<String, status::BgpNeighborInfo> = + if bgp_neighbors_string.is_empty() { + std::collections::BTreeMap::new() + } else { + serde_json::from_str(&bgp_neighbors_string) + .with_context(|| "error parsing bgp neighbors")? + }; + + status::get_neighbors_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) + } } } @@ -959,6 +1005,23 @@ pub mod pve_rs_sdn_fabrics { .map(|v| v.into()) } FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())), + FabricEntry::Bgp(_) => { + let bgp_neighbors_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) + .output()? + .stdout, + )?; + let bgp_neighbors: std::collections::BTreeMap<String, status::BgpNeighborInfo> = + if bgp_neighbors_string.is_empty() { + std::collections::BTreeMap::new() + } else { + serde_json::from_str(&bgp_neighbors_string) + .with_context(|| "error parsing bgp neighbors")? + }; + + status::get_interfaces_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) + } } } @@ -1019,9 +1082,37 @@ pub mod pve_rs_sdn_fabrics { .with_context(|| "error parsing ospf routes")? }; + let bgp_ipv4_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ip route bgp json'"]) + .output()? + .stdout, + )?; + + let bgp_ipv6_routes_string = String::from_utf8( + Command::new("sh") + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) + .output()? + .stdout, + )?; + + let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() { + proxmox_frr::de::Routes::default() + } else { + serde_json::from_str(&bgp_ipv4_routes_string) + .with_context(|| "error parsing bgp ipv4 routes")? + }; + if !bgp_ipv6_routes_string.is_empty() { + let bgp_ipv6_routes: proxmox_frr::de::Routes = + serde_json::from_str(&bgp_ipv6_routes_string) + .with_context(|| "error parsing bgp ipv6 routes")?; + bgp_routes.0.extend(bgp_ipv6_routes.0); + } + let route_status = status::RoutesParsed { openfabric: openfabric_routes, ospf: ospf_routes, + bgp: bgp_routes, }; status::get_status(config, route_status, proxmox_sys::nodename()) diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs index 132a0f4..f661d74 100644 --- a/pve-rs/src/sdn/status.rs +++ b/pve-rs/src/sdn/status.rs @@ -6,14 +6,15 @@ use proxmox_network_types::mac_address::MacAddress; use serde::{Deserialize, Serialize}; use proxmox_frr::de::{self}; +use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; use proxmox_ve_config::sdn::fabric::section_config::protocol::ospf::{ OspfNodeProperties, OspfProperties, }; use proxmox_ve_config::{ common::valid::Valid, sdn::fabric::{ + section_config::{fabric::FabricId, node::Node as ConfigNode, node::NodeId, Section}, Entry, FabricConfig, - section_config::{Section, fabric::FabricId, node::Node as ConfigNode, node::NodeId}, }, }; @@ -90,12 +91,33 @@ mod wireguard { } /// Common NeighborStatus that contains either OSPF or Openfabric neighbors +mod bgp { + use serde::Serialize; + + /// The status of a BGP neighbor. + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct NeighborStatus { + pub neighbor: String, + pub status: String, + pub uptime: String, + } + + /// The status of a BGP fabric interface. + #[derive(Debug, Serialize, PartialEq, Eq)] + pub struct InterfaceStatus { + pub name: String, + pub state: super::InterfaceState, + } +} + +/// Common NeighborStatus that contains either OSPF, Openfabric, or BGP neighbors #[derive(Debug, Serialize)] #[serde(untagged)] pub enum NeighborStatus { Openfabric(Vec<openfabric::NeighborStatus>), Ospf(Vec<ospf::NeighborStatus>), WireGuard(Vec<wireguard::NeighborStatus>), + Bgp(Vec<bgp::NeighborStatus>), } impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus { @@ -108,14 +130,20 @@ impl From<Vec<ospf::NeighborStatus>> for NeighborStatus { NeighborStatus::Ospf(value) } } +impl From<Vec<bgp::NeighborStatus>> for NeighborStatus { + fn from(value: Vec<bgp::NeighborStatus>) -> Self { + NeighborStatus::Bgp(value) + } +} -/// Common InterfaceStatus that contains either OSPF or Openfabric interfaces +/// Common InterfaceStatus that contains either OSPF, Openfabric, or BGP interfaces #[derive(Debug, Serialize)] #[serde(untagged)] pub enum InterfaceStatus { Openfabric(Vec<openfabric::InterfaceStatus>), Ospf(Vec<ospf::InterfaceStatus>), WireGuard(Vec<wireguard::InterfaceStatus>), + Bgp(Vec<bgp::InterfaceStatus>), } impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus { @@ -128,6 +156,11 @@ impl From<Vec<ospf::InterfaceStatus>> for InterfaceStatus { InterfaceStatus::Ospf(value) } } +impl From<Vec<bgp::InterfaceStatus>> for InterfaceStatus { + fn from(value: Vec<bgp::InterfaceStatus>) -> Self { + InterfaceStatus::Bgp(value) + } +} /// The status of a route. /// @@ -148,6 +181,8 @@ pub enum Protocol { Ospf, /// WireGuard WireGuard, + /// BGP + Bgp, } /// The status of a fabric. @@ -186,6 +221,8 @@ pub struct RoutesParsed { pub openfabric: de::Routes, /// All ospf routes in FRR pub ospf: de::Routes, + /// All bgp routes in FRR + pub bgp: de::Routes, } /// Config used to parse the fabric part of the running-config @@ -231,6 +268,10 @@ pub fn get_routes( .map(|i| i.name().as_str()) .collect(), ConfigNode::WireGuard(_) => HashSet::new(), + ConfigNode::Bgp(n) => match n.properties() { + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), + BgpNode::External(_) => HashSet::new(), + }, }; let dummy_interface = format!("dummy_{}", fabric_id.as_str()); @@ -422,6 +463,62 @@ pub fn get_interfaces_ospf( Ok(stats) } +/// Convert the `show bgp neighbors json` output into a list of [`bgp::NeighborStatus`]. +/// +/// BGP neighbors are filtered by the fabric's peer-group name (which matches the fabric ID). +pub fn get_neighbors_bgp( + fabric_id: FabricId, + neighbors: BTreeMap<String, BgpNeighborInfo>, +) -> Result<Vec<bgp::NeighborStatus>, anyhow::Error> { + let mut stats = Vec::new(); + + for (peer_name, info) in &neighbors { + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { + stats.push(bgp::NeighborStatus { + neighbor: peer_name.clone(), + status: info.bgp_state.clone(), + uptime: info.bgp_timer_up_string.clone().unwrap_or_default(), + }); + } + } + + Ok(stats) +} + +/// Convert the `show bgp neighbors json` output into a list of [`bgp::InterfaceStatus`]. +/// +/// For BGP unnumbered, each interface peer maps to a fabric interface. +pub fn get_interfaces_bgp( + fabric_id: FabricId, + neighbors: BTreeMap<String, BgpNeighborInfo>, +) -> Result<Vec<bgp::InterfaceStatus>, anyhow::Error> { + let mut stats = Vec::new(); + + for (peer_name, info) in &neighbors { + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { + stats.push(bgp::InterfaceStatus { + name: peer_name.clone(), + state: if info.bgp_state == "Established" { + InterfaceState::Up + } else { + InterfaceState::Down + }, + }); + } + } + + Ok(stats) +} + +/// Minimal BGP neighbor info from `show bgp neighbors json` +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BgpNeighborInfo { + pub bgp_state: String, + pub peer_group: Option<String>, + pub bgp_timer_up_string: Option<String>, +} + /// Get the status for each fabric using the parsed routes from frr /// /// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every @@ -444,6 +541,7 @@ pub fn get_status( ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0), ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0), ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()), + ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0), }; // get interfaces @@ -459,6 +557,10 @@ pub fn get_status( .map(|i| i.name().as_str()) .collect(), ConfigNode::WireGuard(_n) => HashSet::new(), + ConfigNode::Bgp(n) => match n.properties() { + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), + BgpNode::External(_) => HashSet::new(), + }, }; // determine status by checking if any routes exist for our interfaces -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints 2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer @ 2026-05-13 12:33 ` Stefan Hanreich 2026-05-13 13:02 ` Hannes Laimer 0 siblings, 1 reply; 15+ messages in thread From: Stefan Hanreich @ 2026-05-13 12:33 UTC (permalink / raw) To: pve-devel On 5/12/26 4:12 PM, Hannes Laimer wrote: > Expose BGP fabric status through the existing fabric status API. > Routes are fetched for both IPv4 and IPv6, and neighbor/interface > state is derived from BGP session info. > > Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> > --- > pve-rs/src/bindings/sdn/fabrics.rs | 97 +++++++++++++++++++++++++- > pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++++++- > 2 files changed, 198 insertions(+), 5 deletions(-) > > diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs > index f914094..0189ecd 100644 > --- a/pve-rs/src/bindings/sdn/fabrics.rs > +++ b/pve-rs/src/bindings/sdn/fabrics.rs > @@ -12,8 +12,8 @@ pub mod pve_rs_sdn_fabrics { > use std::process::Command; > use std::sync::Mutex; > > - use anyhow::{Context, Error, format_err}; > - use openssl::hash::{MessageDigest, hash}; > + use anyhow::{format_err, Context, Error}; > + use openssl::hash::{hash, MessageDigest}; > use proxmox_ve_config::sdn::fabric::section_config::node::api::{Node, NodeUpdater}; > use serde::{Deserialize, Serialize}; > > @@ -31,8 +31,8 @@ pub mod pve_rs_sdn_fabrics { > }; > use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName; > use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId}; > - use proxmox_ve_config::sdn::fabric::section_config::Section; > use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; > + use proxmox_ve_config::sdn::fabric::section_config::Section; > use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry}; > use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder; > > @@ -840,6 +840,35 @@ pub mod pve_rs_sdn_fabrics { > status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename()) > } > FabricEntry::WireGuard(_) => Ok(Vec::new()), > + FabricEntry::Bgp(_) => { > + let bgp_ipv4_routes_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show ip route bgp json'"]) > + .output()? > + .stdout, > + )?; > + > + let bgp_ipv6_routes_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) > + .output()? > + .stdout, > + )?; pre-existing but something that really irks me is that we pollute the vtysh history with our pvestatd invocations. It's possible to set VTYSH_HISTFILE=/dev/null to avoid writing the commands to history - so we could do that here for all vtysh invocations? I can prepare a patch for OSPF / Openfabric. > + let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() { > + proxmox_frr::de::Routes::default() > + } else { > + serde_json::from_str(&bgp_ipv4_routes_string) > + .with_context(|| "error parsing bgp ipv4 routes")? > + }; > + if !bgp_ipv6_routes_string.is_empty() { > + let bgp_ipv6_routes: proxmox_frr::de::Routes = > + serde_json::from_str(&bgp_ipv6_routes_string) > + .with_context(|| "error parsing bgp ipv6 routes")?; > + bgp_routes.0.extend(bgp_ipv6_routes.0); > + } > + status::get_routes(fabric_id, config, bgp_routes, proxmox_sys::nodename()) > + } > } > } > > @@ -899,6 +928,23 @@ pub mod pve_rs_sdn_fabrics { > .map(|v| v.into()) > } > FabricEntry::WireGuard(_) => Ok(status::NeighborStatus::WireGuard(Vec::new())), > + FabricEntry::Bgp(_) => { > + let bgp_neighbors_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) here as well > + .output()? > + .stdout, > + )?; > + let bgp_neighbors: std::collections::BTreeMap<String, status::BgpNeighborInfo> = > + if bgp_neighbors_string.is_empty() { > + std::collections::BTreeMap::new() > + } else { > + serde_json::from_str(&bgp_neighbors_string) > + .with_context(|| "error parsing bgp neighbors")? > + }; > + > + status::get_neighbors_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) > + } > } > } > > @@ -959,6 +1005,23 @@ pub mod pve_rs_sdn_fabrics { > .map(|v| v.into()) > } > FabricEntry::WireGuard(_) => Ok(status::InterfaceStatus::WireGuard(Vec::new())), > + FabricEntry::Bgp(_) => { > + let bgp_neighbors_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show bgp neighbors json'"]) here as well > + .output()? > + .stdout, > + )?; > + let bgp_neighbors: std::collections::BTreeMap<String, status::BgpNeighborInfo> = > + if bgp_neighbors_string.is_empty() { > + std::collections::BTreeMap::new() > + } else { > + serde_json::from_str(&bgp_neighbors_string) > + .with_context(|| "error parsing bgp neighbors")? > + }; > + > + status::get_interfaces_bgp(fabric_id, bgp_neighbors).map(|v| v.into()) > + } > } > } > > @@ -1019,9 +1082,37 @@ pub mod pve_rs_sdn_fabrics { > .with_context(|| "error parsing ospf routes")? > }; > > + let bgp_ipv4_routes_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show ip route bgp json'"]) here as well > + .output()? > + .stdout, > + )?; > + > + let bgp_ipv6_routes_string = String::from_utf8( > + Command::new("sh") > + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) here as well > + .output()? > + .stdout, > + )?; > + > + let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() { > + proxmox_frr::de::Routes::default() > + } else { > + serde_json::from_str(&bgp_ipv4_routes_string) > + .with_context(|| "error parsing bgp ipv4 routes")? > + }; > + if !bgp_ipv6_routes_string.is_empty() { > + let bgp_ipv6_routes: proxmox_frr::de::Routes = > + serde_json::from_str(&bgp_ipv6_routes_string) > + .with_context(|| "error parsing bgp ipv6 routes")?; > + bgp_routes.0.extend(bgp_ipv6_routes.0); > + } > + > let route_status = status::RoutesParsed { > openfabric: openfabric_routes, > ospf: ospf_routes, > + bgp: bgp_routes, > }; > > status::get_status(config, route_status, proxmox_sys::nodename()) > diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs > index 132a0f4..f661d74 100644 > --- a/pve-rs/src/sdn/status.rs > +++ b/pve-rs/src/sdn/status.rs > @@ -6,14 +6,15 @@ use proxmox_network_types::mac_address::MacAddress; > use serde::{Deserialize, Serialize}; > > use proxmox_frr::de::{self}; > +use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; > use proxmox_ve_config::sdn::fabric::section_config::protocol::ospf::{ > OspfNodeProperties, OspfProperties, > }; > use proxmox_ve_config::{ > common::valid::Valid, > sdn::fabric::{ > + section_config::{fabric::FabricId, node::Node as ConfigNode, node::NodeId, Section}, > Entry, FabricConfig, > - section_config::{Section, fabric::FabricId, node::Node as ConfigNode, node::NodeId}, > }, > }; > > @@ -90,12 +91,33 @@ mod wireguard { > } > > /// Common NeighborStatus that contains either OSPF or Openfabric neighbors > +mod bgp { > + use serde::Serialize; > + > + /// The status of a BGP neighbor. > + #[derive(Debug, Serialize, PartialEq, Eq)] > + pub struct NeighborStatus { > + pub neighbor: String, > + pub status: String, > + pub uptime: String, > + } > + > + /// The status of a BGP fabric interface. > + #[derive(Debug, Serialize, PartialEq, Eq)] > + pub struct InterfaceStatus { > + pub name: String, > + pub state: super::InterfaceState, > + } > +} > + > +/// Common NeighborStatus that contains either OSPF, Openfabric, or BGP neighbors > #[derive(Debug, Serialize)] > #[serde(untagged)] > pub enum NeighborStatus { > Openfabric(Vec<openfabric::NeighborStatus>), > Ospf(Vec<ospf::NeighborStatus>), > WireGuard(Vec<wireguard::NeighborStatus>), > + Bgp(Vec<bgp::NeighborStatus>), > } > > impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus { > @@ -108,14 +130,20 @@ impl From<Vec<ospf::NeighborStatus>> for NeighborStatus { > NeighborStatus::Ospf(value) > } > } > +impl From<Vec<bgp::NeighborStatus>> for NeighborStatus { > + fn from(value: Vec<bgp::NeighborStatus>) -> Self { > + NeighborStatus::Bgp(value) > + } > +} > > -/// Common InterfaceStatus that contains either OSPF or Openfabric interfaces > +/// Common InterfaceStatus that contains either OSPF, Openfabric, or BGP interfaces > #[derive(Debug, Serialize)] > #[serde(untagged)] > pub enum InterfaceStatus { > Openfabric(Vec<openfabric::InterfaceStatus>), > Ospf(Vec<ospf::InterfaceStatus>), > WireGuard(Vec<wireguard::InterfaceStatus>), > + Bgp(Vec<bgp::InterfaceStatus>), > } > > impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus { > @@ -128,6 +156,11 @@ impl From<Vec<ospf::InterfaceStatus>> for InterfaceStatus { > InterfaceStatus::Ospf(value) > } > } > +impl From<Vec<bgp::InterfaceStatus>> for InterfaceStatus { > + fn from(value: Vec<bgp::InterfaceStatus>) -> Self { > + InterfaceStatus::Bgp(value) > + } > +} > > /// The status of a route. > /// > @@ -148,6 +181,8 @@ pub enum Protocol { > Ospf, > /// WireGuard > WireGuard, > + /// BGP > + Bgp, > } > > /// The status of a fabric. > @@ -186,6 +221,8 @@ pub struct RoutesParsed { > pub openfabric: de::Routes, > /// All ospf routes in FRR > pub ospf: de::Routes, > + /// All bgp routes in FRR > + pub bgp: de::Routes, > } > > /// Config used to parse the fabric part of the running-config > @@ -231,6 +268,10 @@ pub fn get_routes( > .map(|i| i.name().as_str()) > .collect(), > ConfigNode::WireGuard(_) => HashSet::new(), > + ConfigNode::Bgp(n) => match n.properties() { > + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), > + BgpNode::External(_) => HashSet::new(), > + }, > }; > > let dummy_interface = format!("dummy_{}", fabric_id.as_str()); > @@ -422,6 +463,62 @@ pub fn get_interfaces_ospf( > Ok(stats) > } > > +/// Convert the `show bgp neighbors json` output into a list of [`bgp::NeighborStatus`]. > +/// > +/// BGP neighbors are filtered by the fabric's peer-group name (which matches the fabric ID). > +pub fn get_neighbors_bgp( > + fabric_id: FabricId, > + neighbors: BTreeMap<String, BgpNeighborInfo>, > +) -> Result<Vec<bgp::NeighborStatus>, anyhow::Error> { > + let mut stats = Vec::new(); > + > + for (peer_name, info) in &neighbors { > + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { > + stats.push(bgp::NeighborStatus { > + neighbor: peer_name.clone(), > + status: info.bgp_state.clone(), > + uptime: info.bgp_timer_up_string.clone().unwrap_or_default(), > + }); > + } > + } > + > + Ok(stats) > +} > + > +/// Convert the `show bgp neighbors json` output into a list of [`bgp::InterfaceStatus`]. > +/// > +/// For BGP unnumbered, each interface peer maps to a fabric interface. > +pub fn get_interfaces_bgp( > + fabric_id: FabricId, > + neighbors: BTreeMap<String, BgpNeighborInfo>, > +) -> Result<Vec<bgp::InterfaceStatus>, anyhow::Error> { > + let mut stats = Vec::new(); > + > + for (peer_name, info) in &neighbors { > + if info.peer_group.as_deref() == Some(fabric_id.as_str()) { > + stats.push(bgp::InterfaceStatus { > + name: peer_name.clone(), > + state: if info.bgp_state == "Established" { > + InterfaceState::Up > + } else { > + InterfaceState::Down > + }, > + }); > + } > + } > + > + Ok(stats) > +} > + > +/// Minimal BGP neighbor info from `show bgp neighbors json` > +#[derive(Debug, Deserialize)] > +#[serde(rename_all = "camelCase")] > +pub struct BgpNeighborInfo { > + pub bgp_state: String, > + pub peer_group: Option<String>, > + pub bgp_timer_up_string: Option<String>, > +} > + > /// Get the status for each fabric using the parsed routes from frr > /// > /// Using the parsed routes we get from frr, filter and map them to a HashMap mapping every > @@ -444,6 +541,7 @@ pub fn get_status( > ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0), > ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0), > ConfigNode::WireGuard(_) => (Protocol::WireGuard, &BTreeMap::new()), > + ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0), > }; > > // get interfaces > @@ -459,6 +557,10 @@ pub fn get_status( > .map(|i| i.name().as_str()) > .collect(), > ConfigNode::WireGuard(_n) => HashSet::new(), > + ConfigNode::Bgp(n) => match n.properties() { > + BgpNode::Internal(props) => props.interfaces().map(|i| i.name().as_str()).collect(), > + BgpNode::External(_) => HashSet::new(), > + }, > }; > > // determine status by checking if any routes exist for our interfaces ^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints 2026-05-13 12:33 ` Stefan Hanreich @ 2026-05-13 13:02 ` Hannes Laimer 0 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-13 13:02 UTC (permalink / raw) To: Stefan Hanreich, pve-devel On 2026-05-13 14:33, Stefan Hanreich wrote: > On 5/12/26 4:12 PM, Hannes Laimer wrote: >> Expose BGP fabric status through the existing fabric status API. >> Routes are fetched for both IPv4 and IPv6, and neighbor/interface >> state is derived from BGP session info. >> >> Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> >> --- >> pve-rs/src/bindings/sdn/fabrics.rs | 97 +++++++++++++++++++++++++- >> pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++++++- >> 2 files changed, 198 insertions(+), 5 deletions(-) >> >> diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs >> index f914094..0189ecd 100644 >> --- a/pve-rs/src/bindings/sdn/fabrics.rs >> +++ b/pve-rs/src/bindings/sdn/fabrics.rs >> @@ -12,8 +12,8 @@ pub mod pve_rs_sdn_fabrics { >> use std::process::Command; >> use std::sync::Mutex; >> >> - use anyhow::{Context, Error, format_err}; >> - use openssl::hash::{MessageDigest, hash}; >> + use anyhow::{format_err, Context, Error}; >> + use openssl::hash::{hash, MessageDigest}; >> use proxmox_ve_config::sdn::fabric::section_config::node::api::{Node, NodeUpdater}; >> use serde::{Deserialize, Serialize}; >> >> @@ -31,8 +31,8 @@ pub mod pve_rs_sdn_fabrics { >> }; >> use proxmox_ve_config::sdn::fabric::section_config::interface::InterfaceName; >> use proxmox_ve_config::sdn::fabric::section_config::node::{Node as ConfigNode, NodeId}; >> - use proxmox_ve_config::sdn::fabric::section_config::Section; >> use proxmox_ve_config::sdn::fabric::section_config::protocol::bgp::BgpNode; >> + use proxmox_ve_config::sdn::fabric::section_config::Section; >> use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry}; >> use proxmox_ve_config::sdn::wireguard::WireGuardConfigBuilder; >> >> @@ -840,6 +840,35 @@ pub mod pve_rs_sdn_fabrics { >> status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename()) >> } >> FabricEntry::WireGuard(_) => Ok(Vec::new()), >> + FabricEntry::Bgp(_) => { >> + let bgp_ipv4_routes_string = String::from_utf8( >> + Command::new("sh") >> + .args(["-c", "vtysh -c 'show ip route bgp json'"]) >> + .output()? >> + .stdout, >> + )?; >> + >> + let bgp_ipv6_routes_string = String::from_utf8( >> + Command::new("sh") >> + .args(["-c", "vtysh -c 'show ipv6 route bgp json'"]) >> + .output()? >> + .stdout, >> + )?; > > pre-existing but something that really irks me is that we pollute the > vtysh history with our pvestatd invocations. It's possible to set > VTYSH_HISTFILE=/dev/null to avoid writing the commands to history - so > we could do that here for all vtysh invocations? > could also do as a follow-up that does this for all, but can also include in v5 for bgp fabric. fine with me either way > I can prepare a patch for OSPF / Openfabric. > ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (2 preceding siblings ...) 2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-12 14:13 ` [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer ` (4 subsequent siblings) 8 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel Add 'bgp' to the pve-sdn-fabric-protocol enum so it can be selected when creating or updating fabric configurations via the API. Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- src/PVE/Network/SDN/Fabrics.pm | 62 +++++++++++++++++++++++++++++++- src/PVE/Network/SDN/RouteMaps.pm | 15 ++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm index 09273de..29a7cad 100644 --- a/src/PVE/Network/SDN/Fabrics.pm +++ b/src/PVE/Network/SDN/Fabrics.pm @@ -51,7 +51,7 @@ PVE::JSONSchema::register_standard_option( { description => "Type of configuration entry in an SDN Fabric section config", type => 'string', - enum => ['openfabric', 'ospf', 'wireguard'], + enum => ['openfabric', 'ospf', 'wireguard', 'bgp'], }, ); @@ -251,6 +251,21 @@ sub node_properties { description => 'WireGuard network interface', optional => 1, }, + { + 'instance-types' => ['bgp'], + items => { + type => 'string', + format => { + name => { + type => 'string', + format => 'pve-iface', + description => 'Name of the network interface', + }, + }, + }, + description => 'BGP network interface', + optional => 1, + }, ], }, public_key => { @@ -332,6 +347,15 @@ sub node_properties { type => 'array', 'type-property' => 'protocol', oneOf => [ + { + type => 'array', + 'instance-types' => ['bgp'], + items => { + type => 'string', + enum => ['interfaces', 'ip', 'ip6'], + }, + optional => 1, + }, { type => 'array', 'instance-types' => ['openfabric', 'ospf'], @@ -444,12 +468,39 @@ sub fabric_properties { 'The protocol from which to redistribute routes from.', enum => [ 'bgp', + 'connected', + 'kernel', + 'static', + ], + }, + }, + }, + optional => 1, + }, + { + type => 'array', + 'instance-types' => ['bgp'], + items => { + type => 'string', + format => { + source => { + type => 'string', + description => + 'The protocol from which to redistribute routes from.', + enum => [ 'connected', 'kernel', 'ospf', 'static', ], }, + 'route-map' => { + type => 'string', + format => 'pve-sdn-route-map-id', + description => + 'Route map to filter or transform redistributed routes from this source.', + optional => 1, + }, }, }, optional => 1, @@ -473,6 +524,15 @@ sub fabric_properties { }, optional => 1, }, + { + type => 'array', + 'instance-types' => ['bgp'], + items => { + type => 'string', + enum => ['redistribute', 'route_filter', 'route_map_in', 'route_map_out'], + }, + optional => 1, + }, { type => 'array', 'instance-types' => ['ospf'], diff --git a/src/PVE/Network/SDN/RouteMaps.pm b/src/PVE/Network/SDN/RouteMaps.pm index 5560f18..cd16f5e 100644 --- a/src/PVE/Network/SDN/RouteMaps.pm +++ b/src/PVE/Network/SDN/RouteMaps.pm @@ -125,6 +125,21 @@ sub check_references { if $controller->{'route-map-out'} eq $route_map_id; } } + + my $fabrics = PVE::Network::SDN::Fabrics::config()->list_fabrics(); + for my $fabric_id (keys $fabrics->%*) { + my $fabric = $fabrics->{$fabric_id}; + + if ($fabric->{route_map_in}) { + die "route map $route_map_id still referenced by fabric $fabric_id" + if $fabric->{route_map_in} eq $route_map_id; + } + + if ($fabric->{route_map_out}) { + die "route map $route_map_id still referenced by fabric $fabric_id" + if $fabric->{route_map_out} eq $route_map_id; + } + } } sub route_map_properties { -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (3 preceding siblings ...) 2026-05-12 14:13 ` [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer ` (3 subsequent siblings) 8 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- .../bgp_fabric/expected_controller_config | 73 ++++++++++++++++ .../evpn/bgp_fabric/expected_sdn_interfaces | 60 +++++++++++++ src/test/zones/evpn/bgp_fabric/interfaces | 6 ++ src/test/zones/evpn/bgp_fabric/sdn_config | 85 +++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config diff --git a/src/test/zones/evpn/bgp_fabric/expected_controller_config b/src/test/zones/evpn/bgp_fabric/expected_controller_config new file mode 100644 index 0000000..24bf629 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/expected_controller_config @@ -0,0 +1,73 @@ +frr version 10.6.1 +frr defaults datacenter +hostname localhost +log syslog informational +service integrated-vtysh-config +! +vrf vrf_evpn + vni 100 +exit-vrf +! +router bgp 65000 + bgp router-id 10.10.10.1 + no bgp hard-administrative-reset + no bgp default ipv4-unicast + coalesce-time 1000 + no bgp graceful-restart notification + neighbor VTEP peer-group + neighbor VTEP remote-as 65000 + neighbor VTEP bfd + neighbor VTEP update-source dummy_test + neighbor 10.10.10.2 peer-group VTEP + neighbor 10.10.10.3 peer-group VTEP + neighbor test peer-group + neighbor test remote-as external + neighbor test local-as 65100 no-prepend replace-as + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + neighbor test route-map pve_bgp_test_in in + exit-address-family + ! + address-family l2vpn evpn + neighbor VTEP activate + neighbor VTEP route-map MAP_VTEP_IN in + neighbor VTEP route-map MAP_VTEP_OUT out + advertise-all-vni + exit-address-family +exit +! +router bgp 65000 vrf vrf_evpn + bgp router-id 10.10.10.1 + no bgp hard-administrative-reset + no bgp graceful-restart notification +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map MAP_VTEP_IN permit 1 +exit +! +route-map MAP_VTEP_OUT permit 1 +exit +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +route-map pve_bgp permit 65535 +exit +! +route-map pve_bgp_test_in permit 10 + match ip address pve_bgp_test_ips +exit +! +ip protocol bgp route-map pve_bgp +! +line vty +! diff --git a/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces b/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces new file mode 100644 index 0000000..fd16853 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces @@ -0,0 +1,60 @@ +#version:1 + +auto vnet0 +iface vnet0 + address 10.123.123.1/24 + hwaddress BC:24:11:3B:39:34 + bridge_ports vxlan_vnet0 + bridge_stp off + bridge_fd 0 + mtu 1450 + ip-forward on + arp-accept on + vrf vrf_evpn + +auto vrf_evpn +iface vrf_evpn + vrf-table auto + post-up ip route add vrf vrf_evpn unreachable default metric 4278198272 + +auto vrfbr_evpn +iface vrfbr_evpn + bridge-ports vrfvx_evpn + bridge_stp off + bridge_fd 0 + mtu 1450 + vrf vrf_evpn + +auto vrfvx_evpn +iface vrfvx_evpn + vxlan-id 100 + vxlan-local-tunnelip 10.10.10.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 + +auto vxlan_vnet0 +iface vxlan_vnet0 + vxlan-id 123456 + vxlan-local-tunnelip 10.10.10.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 + +auto dummy_test +iface dummy_test inet static + address 10.10.10.1/32 + link-type dummy + ip-forward 1 + +auto ens18 +iface ens18 inet manual + ip-forward 1 + ip6-forward 1 + accept-ra 0 + +auto ens19 +iface ens19 inet manual + ip-forward 1 + ip6-forward 1 + accept-ra 0 diff --git a/src/test/zones/evpn/bgp_fabric/interfaces b/src/test/zones/evpn/bgp_fabric/interfaces new file mode 100644 index 0000000..0887413 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/interfaces @@ -0,0 +1,6 @@ +auto vmbr0 +iface vmbr0 inet static + address 10.10.10.1/32 + bridge-ports eth0 + bridge-stp off + bridge-fd 0 diff --git a/src/test/zones/evpn/bgp_fabric/sdn_config b/src/test/zones/evpn/bgp_fabric/sdn_config new file mode 100644 index 0000000..080f1e9 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/sdn_config @@ -0,0 +1,85 @@ +{ + 'zones' => { + 'ids' => { + 'evpn' => { + 'type' => 'evpn', + 'ipam' => 'pve', + 'mac' => 'BC:24:11:3B:39:34', + 'controller' => 'ctrl', + 'vrf-vxlan' => 100 + } + } + }, + 'vnets' => { + 'ids' => { + 'vnet0' => { + 'zone' => 'evpn', + 'type' => 'vnet', + 'tag' => 123456 + } + } + }, + 'version' => 1, + 'subnets' => { + 'ids' => { + 'evpn-10.123.123.0-24' => { + 'vnet' => 'vnet0', + 'type' => 'subnet', + 'gateway' => '10.123.123.1' + } + } + }, + 'controllers' => { + 'ids' => { + 'ctrl' => { + 'fabric' => 'test', + 'asn' => 65000, + 'type' => 'evpn' + } + } + }, + 'fabrics' => { + 'ids' => { + 'test' => { + 'type' => 'bgp_fabric', + 'id' => 'test', + 'bfd' => 0, + 'ip_prefix' => '10.10.10.0/24', + }, + 'test_localhost' => { + 'asn' => 65100, + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'id' => 'test_localhost', + 'type' => 'bgp_node', + 'ip' => '10.10.10.1', + 'role' => 'internal', + }, + 'test_pathfinder' => { + 'asn' => 65101, + 'id' => 'test_pathfinder', + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'ip' => '10.10.10.2', + 'type' => 'bgp_node', + 'role' => 'internal', + }, + 'test_raider' => { + 'asn' => 65102, + 'ip' => '10.10.10.3', + 'type' => 'bgp_node', + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'id' => 'test_raider', + 'role' => 'internal', + } + } + } + }; + -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (4 preceding siblings ...) 2026-05-12 14:13 ` [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-13 12:38 ` Stefan Hanreich 2026-05-12 14:13 ` [PATCH pve-docs v4 7/7] sdn: add bgp fabric section Hannes Laimer ` (2 subsequent siblings) 8 siblings, 1 reply; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel Register BGP as a fabric protocol in the UI. The fabric editor omits the ASN field since each node must specify its own unique ASN for eBGP unnumbered peering. The interface panel excludes the IP column since BGP unnumbered interfaces have no IP addresses. Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- www/manager6/Makefile | 3 + www/manager6/sdn/FabricsView.js | 12 ++++ www/manager6/sdn/fabrics/FabricEdit.js | 12 +++- www/manager6/sdn/fabrics/NodeEdit.js | 1 + www/manager6/sdn/fabrics/bgp/FabricEdit.js | 67 +++++++++++++++++++ .../sdn/fabrics/bgp/InterfacePanel.js | 15 +++++ www/manager6/sdn/fabrics/bgp/NodeEdit.js | 23 +++++++ 7 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 www/manager6/sdn/fabrics/bgp/FabricEdit.js create mode 100644 www/manager6/sdn/fabrics/bgp/InterfacePanel.js create mode 100644 www/manager6/sdn/fabrics/bgp/NodeEdit.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 1ad77f3d..90d73ba1 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -347,6 +347,9 @@ JSSRC= \ sdn/fabrics/wireguard/InterfacePanel.js \ sdn/fabrics/wireguard/NodeEdit.js \ sdn/fabrics/wireguard/FabricEdit.js \ + sdn/fabrics/bgp/InterfacePanel.js \ + sdn/fabrics/bgp/NodeEdit.js \ + sdn/fabrics/bgp/FabricEdit.js \ storage/ContentView.js \ storage/BackupView.js \ storage/Base.js \ diff --git a/www/manager6/sdn/FabricsView.js b/www/manager6/sdn/FabricsView.js index 0362df01..6901b96f 100644 --- a/www/manager6/sdn/FabricsView.js +++ b/www/manager6/sdn/FabricsView.js @@ -34,6 +34,7 @@ Ext.define('PVE.sdn.Fabric.View', { openfabric: 'OpenFabric', ospf: 'OSPF', wireguard: 'WireGuard', + bgp: 'BGP', }; const displayValue = PROTOCOL_DISPLAY_NAMES[value]; if (rec.data.state === undefined || rec.data.state === null) { @@ -199,6 +200,10 @@ Ext.define('PVE.sdn.Fabric.View', { text: 'WireGuard', handler: 'addWireGuard', }, + { + text: 'BGP', + handler: 'addBgp', + }, ], }, addNodeButton, @@ -278,6 +283,7 @@ Ext.define('PVE.sdn.Fabric.View', { openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit', ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit', wireguard: 'PVE.sdn.Fabric.WireGuard.Fabric.Edit', + bgp: 'PVE.sdn.Fabric.Bgp.Fabric.Edit', }; return FABRIC_PANELS[protocol]; @@ -288,6 +294,7 @@ Ext.define('PVE.sdn.Fabric.View', { openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit', ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit', wireguard: 'PVE.sdn.Fabric.WireGuard.Node.Edit', + bgp: 'PVE.sdn.Fabric.Bgp.Node.Edit', }; return NODE_PANELS[protocol]; @@ -308,6 +315,11 @@ Ext.define('PVE.sdn.Fabric.View', { me.openFabricAddWindow('ospf'); }, + addBgp: function () { + let me = this; + me.openFabricAddWindow('bgp'); + }, + openFabricAddWindow: function (protocol) { let me = this; diff --git a/www/manager6/sdn/fabrics/FabricEdit.js b/www/manager6/sdn/fabrics/FabricEdit.js index 43a402a9..322a62c0 100644 --- a/www/manager6/sdn/fabrics/FabricEdit.js +++ b/www/manager6/sdn/fabrics/FabricEdit.js @@ -37,6 +37,7 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', { ], additionalItems: [], + additionalAdvancedItems: [], additionalTabs: [], initComponent: function () { @@ -101,10 +102,15 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', { if (me.additionalTabs.length > 0) { let items = [...me.items, ...me.additionalItems]; - let iPanel = Ext.create('Proxmox.panel.InputPanel', { + let panelConfig = { title: gettext('Fabric'), items, - }); + }; + if (me.additionalAdvancedItems.length > 0) { + panelConfig.advancedItems = me.additionalAdvancedItems; + } + + let iPanel = Ext.create('Proxmox.panel.InputPanel', panelConfig); me.bodyPadding = 0; @@ -116,7 +122,7 @@ Ext.define('PVE.sdn.Fabric.Fabric.Edit', { }, ]; } else { - me.items.push(...me.additionalItems); + me.items.push(...me.additionalItems, ...me.additionalAdvancedItems); } me.callParent(); diff --git a/www/manager6/sdn/fabrics/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js index 4b3967a1..8004ff1b 100644 --- a/www/manager6/sdn/fabrics/NodeEdit.js +++ b/www/manager6/sdn/fabrics/NodeEdit.js @@ -260,6 +260,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', { openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel', ospf: 'PVE.sdn.Fabric.Ospf.InterfacePanel', wireguard: 'PVE.sdn.Fabric.WireGuard.InterfacePanel', + bgp: 'PVE.sdn.Fabric.Bgp.InterfacePanel', }; return INTERFACE_PANELS[protocol]; diff --git a/www/manager6/sdn/fabrics/bgp/FabricEdit.js b/www/manager6/sdn/fabrics/bgp/FabricEdit.js new file mode 100644 index 00000000..c4a94a52 --- /dev/null +++ b/www/manager6/sdn/fabrics/bgp/FabricEdit.js @@ -0,0 +1,67 @@ +Ext.define('PVE.sdn.Fabric.Bgp.Fabric.Edit', { + extend: 'PVE.sdn.Fabric.Fabric.Edit', + + subject: 'BGP', + onlineHelp: 'pvesdn_bgp_fabric', + + extraRequestParams: { + protocol: 'bgp', + }, + + additionalItems: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('BFD'), + labelWidth: 120, + name: 'bfd', + uncheckedValue: 0, + defaultValue: 0, + }, + { + xtype: 'pveSDNPrefixListSelector', + name: 'route_filter', + fieldLabel: gettext('Route Filter'), + emptyText: gettext('IP Prefixes'), + deleteEmpty: true, + skipEmptyText: true, + }, + ], + + additionalAdvancedItems: [ + { + xtype: 'pveSDNRouteMapSelector', + name: 'route_map_in', + fieldLabel: gettext('Incoming Route Map'), + emptyText: gettext('Route Map'), + deleteEmpty: true, + skipEmptyText: true, + }, + { + xtype: 'pveSDNRouteMapSelector', + name: 'route_map_out', + fieldLabel: gettext('Outgoing Route Map'), + emptyText: gettext('Route Map'), + deleteEmpty: true, + skipEmptyText: true, + }, + ], + + additionalTabs: [ + { + xtype: 'inputpanel', + title: gettext('Route Redistribution'), + items: [ + { + xtype: 'pveSDNRedistributionGrid', + name: 'redistribute', + sources: [ + ['ospf', gettext('OSPF')], + ['connected', gettext('Connected')], + ['static', gettext('Static')], + ['kernel', gettext('Kernel')], + ], + }, + ], + } + ], +}); diff --git a/www/manager6/sdn/fabrics/bgp/InterfacePanel.js b/www/manager6/sdn/fabrics/bgp/InterfacePanel.js new file mode 100644 index 00000000..c7ac7627 --- /dev/null +++ b/www/manager6/sdn/fabrics/bgp/InterfacePanel.js @@ -0,0 +1,15 @@ +Ext.define('PVE.sdn.Fabric.Bgp.InterfacePanel', { + extend: 'PVE.sdn.Fabric.InterfacePanel', + + hasIpv6Support: false, + + // BGP unnumbered interfaces have no IP - override commonColumns to + // exclude the IP column that the base class defines. + initComponent: function () { + let me = this; + + me.commonColumns = me.commonColumns.filter((col) => col.dataIndex !== 'ip'); + + me.callParent(); + }, +}); diff --git a/www/manager6/sdn/fabrics/bgp/NodeEdit.js b/www/manager6/sdn/fabrics/bgp/NodeEdit.js new file mode 100644 index 00000000..1ad586df --- /dev/null +++ b/www/manager6/sdn/fabrics/bgp/NodeEdit.js @@ -0,0 +1,23 @@ +Ext.define('PVE.sdn.Fabric.Bgp.Node.Edit', { + extend: 'PVE.sdn.Fabric.Node.Edit', + protocol: 'bgp', + + extraRequestParams: { + protocol: 'bgp', + role: 'internal', + }, + + includeWireguardInterfaces: true, + + additionalItems: [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('ASN'), + labelWidth: 120, + name: 'asn', + minValue: 1, + maxValue: 4294967295, + allowBlank: false, + }, + ], +}); -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support 2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer @ 2026-05-13 12:38 ` Stefan Hanreich 0 siblings, 0 replies; 15+ messages in thread From: Stefan Hanreich @ 2026-05-13 12:38 UTC (permalink / raw) To: pve-devel On 5/12/26 4:12 PM, Hannes Laimer wrote: [snip] > diff --git a/www/manager6/sdn/fabrics/bgp/FabricEdit.js b/www/manager6/sdn/fabrics/bgp/FabricEdit.js > new file mode 100644 > index 00000000..c4a94a52 > --- /dev/null > +++ b/www/manager6/sdn/fabrics/bgp/FabricEdit.js > @@ -0,0 +1,67 @@ > +Ext.define('PVE.sdn.Fabric.Bgp.Fabric.Edit', { > + extend: 'PVE.sdn.Fabric.Fabric.Edit', > + > + subject: 'BGP', > + onlineHelp: 'pvesdn_bgp_fabric', > + > + extraRequestParams: { > + protocol: 'bgp', > + }, > + > + additionalItems: [ > + { > + xtype: 'proxmoxcheckbox', > + fieldLabel: gettext('BFD'), > + labelWidth: 120, > + name: 'bfd', > + uncheckedValue: 0, > + defaultValue: 0, > + }, > + { > + xtype: 'pveSDNPrefixListSelector', > + name: 'route_filter', > + fieldLabel: gettext('Route Filter'), > + emptyText: gettext('IP Prefixes'), > + deleteEmpty: true, > + skipEmptyText: true, > + }, > + ], > + > + additionalAdvancedItems: [ > + { > + xtype: 'pveSDNRouteMapSelector', > + name: 'route_map_in', > + fieldLabel: gettext('Incoming Route Map'), > + emptyText: gettext('Route Map'), > + deleteEmpty: true, > + skipEmptyText: true, > + }, > + { > + xtype: 'pveSDNRouteMapSelector', > + name: 'route_map_out', > + fieldLabel: gettext('Outgoing Route Map'), > + emptyText: gettext('Route Map'), > + deleteEmpty: true, > + skipEmptyText: true, > + }, > + ], setting labelWidth: 120 to the routemap / routefilter elements would make the width of all input fields uniform. ^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH pve-docs v4 7/7] sdn: add bgp fabric section 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (5 preceding siblings ...) 2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer @ 2026-05-12 14:13 ` Hannes Laimer 2026-05-13 12:39 ` [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Stefan Hanreich 2026-05-13 18:43 ` superseded: " Hannes Laimer 8 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-12 14:13 UTC (permalink / raw) To: pve-devel Signed-off-by: Hannes Laimer <h.laimer@proxmox.com> --- pvesdn.adoc | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/pvesdn.adoc b/pvesdn.adoc index 8f955e8..a15c568 100644 --- a/pvesdn.adoc +++ b/pvesdn.adoc @@ -835,6 +835,102 @@ NOTE: The dummy interface will automatically be configured as `passive`. Every interface which doesn't have an ip-address configured will be treated as a `point-to-point` link. +[[pvesdn_bgp]] +BGP +~~~ + +BGP (Border Gateway Protocol) can be used as an eBGP unnumbered fabric. Each +node has its own Autonomous System Number (ASN) and peers with its neighbors +over physical interfaces without requiring IP addresses on the fabric links. + +Configuration options: + +[[pvesdn_bgp_fabric]] +On the Fabric +^^^^^^^^^^^^^ + +IPv4 Prefix:: IPv4 CIDR network range (e.g., 192.0.2.0/24) used to verify that +all node IPv4 addresses (BGP router-IDs) are contained within this prefix. + +IPv6 Prefix:: IPv6 CIDR network range (e.g., 2001:db8::/64) used to verify that +all node IPv6 addresses in the fabric are contained within this prefix. + +BFD:: Enable Bidirectional Forwarding Detection on all peering sessions in this +fabric. BFD provides fast failure detection for links between nodes. + +Route Filter:: A prefix list applied to the fabric peer-group's inbound +direction. When set, this prefix list replaces the implicit "must be within the +fabric prefix" filter. Useful to widen the accepted range (e.g. accept +additional non-fabric prefixes redistributed via the fabric) or to use a more +expressive match. References a prefix list defined under SDN. + +Incoming Route Map:: A route map chained from the fabric peer-group's implicit +inbound filter via FRR's `call` action. Only prefixes that have already passed +the implicit prefix check (or the Route Filter override) reach this route map. +Useful to transform or further narrow accepted routes (e.g. adjust community or +local-preference), but cannot widen the accepted set. Use Route Filter to +override the implicit filter. References a route map defined under SDN. + +Outgoing Route Map:: A route map applied to the fabric peer-group's outbound +direction. Used to transform or filter what is announced to fabric neighbors. +References a route map defined under SDN. + +Redistribute:: Which other-protocol routes the BGP router on each node should +redistribute into the fabric. Supported sources are `connected`, `kernel`, +`static`, and `ospf`. An optional per-source route map can be attached to shape +the redistributed routes before they are announced. + +NOTE: At least one of IPv4 Prefix or IPv6 Prefix must be configured. + +[[pvesdn_bgp_node]] +On the Node +^^^^^^^^^^^ + +Options that are available on every node that is part of a fabric: + +Node:: Select the node which will be added to the fabric. Only nodes that are +currently in the cluster will be shown. + +ASN:: BGP Autonomous System Number for this node. It is recommended to use +private ASN numbers (64512-65534 for 16-bit, 4200000000-4294967294 for 32-bit). + +IPv4:: A unique IPv4 address for this node, also used as the BGP router-id. +Required when the fabric has an IPv4 prefix configured; each node in the same +fabric must have a different address. + +IPv6:: IPv6 address for this node. If only an IPv6 address is configured +(without an IPv4 address), the BGP router-id is derived from it via a FNV-1a +hash. + +Interfaces:: Specify the interfaces used to establish peering connections with +other BGP nodes. These interfaces run BGP unnumbered (no IP address assignment +needed). When a node IP is configured, a dummy "loopback" interface is +automatically created with that IP. + +NOTE: Unlike OSPF and OpenFabric, BGP unnumbered interfaces do not need IP +addresses. Peering is established using IPv6 link-local addresses +automatically. + +NOTE: In an eBGP unnumbered fabric, directly peering nodes must have different +ASNs for BGP sessions to establish. + +[[pvesdn_bgp_evpn]] +Using BGP Fabrics with EVPN +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When configuring an EVPN controller, a BGP fabric can be selected as the +underlay instead of manually specifying peer addresses. The EVPN overlay +sessions will run as iBGP, using the EVPN controller's ASN for the router +process. The per-node fabric ASN is automatically applied via `local-as` on the +underlay neighbor group. + +This means the EVPN controller ASN and the per-node fabric ASNs should be +different. For example, with three nodes using ASNs 65001, 65002, and 65003 for +the underlay, the EVPN controller could use ASN 65000 for the overlay. + +NOTE: Using a BGP fabric for an EVPN underlay requires each node to have an +IPv4 address, since EVPN uses it as the VTEP address. + [[pvesdn_config_ipam]] IPAM ---- -- 2.47.3 ^ permalink raw reply related [flat|nested] 15+ messages in thread
* Re: [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (6 preceding siblings ...) 2026-05-12 14:13 ` [PATCH pve-docs v4 7/7] sdn: add bgp fabric section Hannes Laimer @ 2026-05-13 12:39 ` Stefan Hanreich 2026-05-13 18:43 ` superseded: " Hannes Laimer 8 siblings, 0 replies; 15+ messages in thread From: Stefan Hanreich @ 2026-05-13 12:39 UTC (permalink / raw) To: pve-devel Doesn't apply on current master anymore - conflicts were only imports iirc. I'll be giving this a spin again this afternoon. Some minor comments on the code - but other than that lgtm code-wise. On 5/12/26 4:12 PM, Hannes Laimer wrote: > This patch series adds BGP as a third fabric protocol alongside OpenFabric and > OSPF. It targets eBGP unnumbered underlays where each node has a unique ASN and > peers over physical interfaces without IP assignment on fabric links. > > ## Dependencies > > This series is based on the route-maps, evpn-rework, wireguard, and > ospf-route-dist series ([3]-[6]). > > ## eBGP underlay > > Each node gets its own ASN (e.g. 65001, 65002, 65003) and peers with its > neighbors using 'remote-as external' on unnumbered interfaces. The fabric > peer-group is named after the fabric ID and uses BFD when enabled. > > ## EVPN overlay > > When the EVPN controller references a BGP fabric, VTEP sessions are iBGP, > consistent with how EVPN operates on OSPF and OpenFabric fabrics. The per-node > ASN is applied via 'local-as' on the underlay neighbor group. > > ## Single BGP process > > FRR allows only one BGP instance per VRF, so the fabric underlay and the EVPN > overlay coexist in one 'router bgp' instance. The fabric generates its own > BgpRouter and merges into the existing one via merge_fabric(), appending > neighbor groups and address families without overwriting EVPN settings. > > ## IPv6-only support > > For nodes with only an IPv6 address, the BGP router-id (which must be a 32-bit > value) is derived from the IPv6 address using FNV-1a hashing. > > ### Testing results for hash collisions > Scattered /64 n=1000 unique=1000 collisions=0 worst=1 > Scattered /64 n=10000 unique=10000 collisions=0 worst=1 > Scattered /64 n=100000 unique=99997 collisions=3 worst=2 > Sequential /64 n=1000 unique=1000 collisions=0 worst=1 > Sequential /64 n=10000 unique=10000 collisions=0 worst=1 > Sequential /64 n=100000 unique=100000 collisions=0 worst=1 > Spaced /64 (step 256) n=1000 unique=1000 collisions=0 worst=1 > Spaced /64 (step 256) n=10000 unique=10000 collisions=0 worst=1 > Spaced /64 (step 256) n=100000 unique=100000 collisions=0 worst=1 > Sparse multi-/48 n=1000 unique=1000 collisions=0 worst=1 > Sparse multi-/48 n=10000 unique=10000 collisions=0 worst=1 > Sparse multi-/48 n=100000 unique=100000 collisions=0 worst=1 > > Only the random assignment in a /64 prefix caused a tiny amount of collisions, > and having 100k routers with randomly assigned IPs is not really typical. So > FNV-1a does seem like a good choice here. (generally I'm open to alternative > approaches for getting router-ids on nodes with no ipv4 ips) > > > Thanks a lot @Stefan for the base of this series! > > > v4: > - split route-map: `pve_bgp` (zebra `ip protocol bgp` filter) for set-src + > catch-all, `pve_bgp_<id>_in` (per-fabric peer-inbound) for filtering > - added `accept-ra 0` and `ip6-forward 1` on BGP fabric interfaces > - dropped v3's patch 6/8 (EVPN underlay prefix check) > - dropped the `router_id` field references. With prefixes always > present, the router-id is always derivable > - rebase onto the latest versions of the > route-maps[3]/wireguard[4]/evpn[5]/ospf-route-dist[6] series > > > v3, thanks @Gabriel and @Stefan for the (mostly off-list) feedback on v2!: > - fixed exit-node routing: moved the set-src route-map from `ip > protocol bgp` to the fabric peer-group's inbound direction, so EVPN > VRF imports aren't dropped by the filter's implicit deny > - renamed the route-map to be per-fabric (pve_bgp_<id>) > - added optional router_id field on BGP nodes (required when the fabric > has no prefix), enabling prefix-less BGP fabrics > - dropped the per-node ASN uniqueness check > - ui: show `router_id` field only when the fabric has no prefix > > > v2, thanks @Gabriel and @Stefan for the (off-list) feedback on v1!: > - switched EVPN overlay from eBGP to iBGP > - rebased onto Stefan's evpn[1]/route-maps[2] series > - made LocalAsSettings fields pub (needed for Rust-side construction) > - added router-id collision validation for IPv6-only nodes > - added docs section > > [1] https://lore.proxmox.com/pve-devel/20260414163315.419384-1-s.hanreich@proxmox.com/ > [2] https://lore.proxmox.com/pve-devel/20260401143957.386809-1-s.hanreich@proxmox.com/ > > [3] https://lore.proxmox.com/pve-devel/20260511090202.60323-1-s.hanreich@proxmox.com/ > [4] https://lore.proxmox.com/pve-devel/20260507124008.417223-1-s.hanreich@proxmox.com/ > [5] https://lore.proxmox.com/pve-devel/20260504162501.425135-1-s.hanreich@proxmox.com/ > [6] https://lore.proxmox.com/pve-devel/20260504163157.429628-1-s.hanreich@proxmox.com/ > > > proxmox-ve-rs: > > Stefan Hanreich (1): > sdn: fabric: add BGP protocol support > > proxmox-frr/src/ser/bgp.rs | 87 ++++- > proxmox-ve-config/src/sdn/fabric/frr.rs | 304 ++++++++++++++++- > proxmox-ve-config/src/sdn/fabric/mod.rs | 169 +++++++++- > .../src/sdn/fabric/section_config/fabric.rs | 22 ++ > .../src/sdn/fabric/section_config/mod.rs | 21 +- > .../src/sdn/fabric/section_config/node.rs | 21 ++ > .../sdn/fabric/section_config/protocol/bgp.rs | 317 ++++++++++++++++++ > .../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 | 36 ++ > .../snapshots/fabric__bgp_default_pve1.snap | 35 ++ > .../snapshots/fabric__bgp_ipv6_only_pve.snap | 37 ++ > .../snapshots/fabric__bgp_ipv6_only_pve1.snap | 36 ++ > .../fabric__bgp_merge_with_evpn_pve.snap | 42 +++ > 16 files changed, 1268 insertions(+), 13 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 > > > proxmox-perl-rs: > > Hannes Laimer (1): > sdn: fabrics: add BGP status endpoints > > Stefan Hanreich (1): > sdn: fabrics: add BGP config generation > > pve-rs/src/bindings/sdn/fabrics.rs | 123 ++++++++++++++++++++++++++++- > pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++- > 2 files changed, 225 insertions(+), 4 deletions(-) > > > pve-network: > > Hannes Laimer (2): > sdn: fabrics: register bgp as a fabric protocol type > test: evpn: add integration test for EVPN over BGP fabric > > src/PVE/Network/SDN/Fabrics.pm | 62 +++++++++++++- > src/PVE/Network/SDN/RouteMaps.pm | 15 ++++ > .../bgp_fabric/expected_controller_config | 73 ++++++++++++++++ > .../evpn/bgp_fabric/expected_sdn_interfaces | 60 +++++++++++++ > src/test/zones/evpn/bgp_fabric/interfaces | 6 ++ > src/test/zones/evpn/bgp_fabric/sdn_config | 85 +++++++++++++++++++ > 6 files changed, 300 insertions(+), 1 deletion(-) > create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config > create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces > create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces > create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config > > > pve-manager: > > Hannes Laimer (1): > ui: sdn: add BGP fabric support > > www/manager6/Makefile | 3 + > www/manager6/sdn/FabricsView.js | 12 ++++ > www/manager6/sdn/fabrics/FabricEdit.js | 12 +++- > www/manager6/sdn/fabrics/NodeEdit.js | 1 + > www/manager6/sdn/fabrics/bgp/FabricEdit.js | 67 +++++++++++++++++++ > .../sdn/fabrics/bgp/InterfacePanel.js | 15 +++++ > www/manager6/sdn/fabrics/bgp/NodeEdit.js | 23 +++++++ > 7 files changed, 130 insertions(+), 3 deletions(-) > create mode 100644 www/manager6/sdn/fabrics/bgp/FabricEdit.js > create mode 100644 www/manager6/sdn/fabrics/bgp/InterfacePanel.js > create mode 100644 www/manager6/sdn/fabrics/bgp/NodeEdit.js > > > pve-docs: > > Hannes Laimer (1): > sdn: add bgp fabric section > > pvesdn.adoc | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 96 insertions(+) > > > Summary over all repositories: > 32 files changed, 2019 insertions(+), 21 deletions(-) > ^ permalink raw reply [flat|nested] 15+ messages in thread
* superseded: [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric 2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer ` (7 preceding siblings ...) 2026-05-13 12:39 ` [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Stefan Hanreich @ 2026-05-13 18:43 ` Hannes Laimer 8 siblings, 0 replies; 15+ messages in thread From: Hannes Laimer @ 2026-05-13 18:43 UTC (permalink / raw) To: pve-devel superseded-by: https://lore.proxmox.com/pve-devel/20260513184213.506775-1-h.laimer@proxmox.com/T/#t On 2026-05-12 16:12, Hannes Laimer wrote: > This patch series adds BGP as a third fabric protocol alongside OpenFabric and > OSPF. It targets eBGP unnumbered underlays where each node has a unique ASN and > peers over physical interfaces without IP assignment on fabric links. > > ## Dependencies > > This series is based on the route-maps, evpn-rework, wireguard, and > ospf-route-dist series ([3]-[6]). > > ## eBGP underlay > > Each node gets its own ASN (e.g. 65001, 65002, 65003) and peers with its > neighbors using 'remote-as external' on unnumbered interfaces. The fabric > peer-group is named after the fabric ID and uses BFD when enabled. > > ## EVPN overlay > > When the EVPN controller references a BGP fabric, VTEP sessions are iBGP, > consistent with how EVPN operates on OSPF and OpenFabric fabrics. The per-node > ASN is applied via 'local-as' on the underlay neighbor group. > > ## Single BGP process > > FRR allows only one BGP instance per VRF, so the fabric underlay and the EVPN > overlay coexist in one 'router bgp' instance. The fabric generates its own > BgpRouter and merges into the existing one via merge_fabric(), appending > neighbor groups and address families without overwriting EVPN settings. > > ## IPv6-only support > > For nodes with only an IPv6 address, the BGP router-id (which must be a 32-bit > value) is derived from the IPv6 address using FNV-1a hashing. > > ### Testing results for hash collisions > Scattered /64 n=1000 unique=1000 collisions=0 worst=1 > Scattered /64 n=10000 unique=10000 collisions=0 worst=1 > Scattered /64 n=100000 unique=99997 collisions=3 worst=2 > Sequential /64 n=1000 unique=1000 collisions=0 worst=1 > Sequential /64 n=10000 unique=10000 collisions=0 worst=1 > Sequential /64 n=100000 unique=100000 collisions=0 worst=1 > Spaced /64 (step 256) n=1000 unique=1000 collisions=0 worst=1 > Spaced /64 (step 256) n=10000 unique=10000 collisions=0 worst=1 > Spaced /64 (step 256) n=100000 unique=100000 collisions=0 worst=1 > Sparse multi-/48 n=1000 unique=1000 collisions=0 worst=1 > Sparse multi-/48 n=10000 unique=10000 collisions=0 worst=1 > Sparse multi-/48 n=100000 unique=100000 collisions=0 worst=1 > > Only the random assignment in a /64 prefix caused a tiny amount of collisions, > and having 100k routers with randomly assigned IPs is not really typical. So > FNV-1a does seem like a good choice here. (generally I'm open to alternative > approaches for getting router-ids on nodes with no ipv4 ips) > > > Thanks a lot @Stefan for the base of this series! > > > v4: > - split route-map: `pve_bgp` (zebra `ip protocol bgp` filter) for set-src + > catch-all, `pve_bgp_<id>_in` (per-fabric peer-inbound) for filtering > - added `accept-ra 0` and `ip6-forward 1` on BGP fabric interfaces > - dropped v3's patch 6/8 (EVPN underlay prefix check) > - dropped the `router_id` field references. With prefixes always > present, the router-id is always derivable > - rebase onto the latest versions of the > route-maps[3]/wireguard[4]/evpn[5]/ospf-route-dist[6] series > > > v3, thanks @Gabriel and @Stefan for the (mostly off-list) feedback on v2!: > - fixed exit-node routing: moved the set-src route-map from `ip > protocol bgp` to the fabric peer-group's inbound direction, so EVPN > VRF imports aren't dropped by the filter's implicit deny > - renamed the route-map to be per-fabric (pve_bgp_<id>) > - added optional router_id field on BGP nodes (required when the fabric > has no prefix), enabling prefix-less BGP fabrics > - dropped the per-node ASN uniqueness check > - ui: show `router_id` field only when the fabric has no prefix > > > v2, thanks @Gabriel and @Stefan for the (off-list) feedback on v1!: > - switched EVPN overlay from eBGP to iBGP > - rebased onto Stefan's evpn[1]/route-maps[2] series > - made LocalAsSettings fields pub (needed for Rust-side construction) > - added router-id collision validation for IPv6-only nodes > - added docs section > > [1] https://lore.proxmox.com/pve-devel/20260414163315.419384-1-s.hanreich@proxmox.com/ > [2] https://lore.proxmox.com/pve-devel/20260401143957.386809-1-s.hanreich@proxmox.com/ > > [3] https://lore.proxmox.com/pve-devel/20260511090202.60323-1-s.hanreich@proxmox.com/ > [4] https://lore.proxmox.com/pve-devel/20260507124008.417223-1-s.hanreich@proxmox.com/ > [5] https://lore.proxmox.com/pve-devel/20260504162501.425135-1-s.hanreich@proxmox.com/ > [6] https://lore.proxmox.com/pve-devel/20260504163157.429628-1-s.hanreich@proxmox.com/ > > > proxmox-ve-rs: > > Stefan Hanreich (1): > sdn: fabric: add BGP protocol support > > proxmox-frr/src/ser/bgp.rs | 87 ++++- > proxmox-ve-config/src/sdn/fabric/frr.rs | 304 ++++++++++++++++- > proxmox-ve-config/src/sdn/fabric/mod.rs | 169 +++++++++- > .../src/sdn/fabric/section_config/fabric.rs | 22 ++ > .../src/sdn/fabric/section_config/mod.rs | 21 +- > .../src/sdn/fabric/section_config/node.rs | 21 ++ > .../sdn/fabric/section_config/protocol/bgp.rs | 317 ++++++++++++++++++ > .../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 | 36 ++ > .../snapshots/fabric__bgp_default_pve1.snap | 35 ++ > .../snapshots/fabric__bgp_ipv6_only_pve.snap | 37 ++ > .../snapshots/fabric__bgp_ipv6_only_pve1.snap | 36 ++ > .../fabric__bgp_merge_with_evpn_pve.snap | 42 +++ > 16 files changed, 1268 insertions(+), 13 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 > > > proxmox-perl-rs: > > Hannes Laimer (1): > sdn: fabrics: add BGP status endpoints > > Stefan Hanreich (1): > sdn: fabrics: add BGP config generation > > pve-rs/src/bindings/sdn/fabrics.rs | 123 ++++++++++++++++++++++++++++- > pve-rs/src/sdn/status.rs | 106 ++++++++++++++++++++++++- > 2 files changed, 225 insertions(+), 4 deletions(-) > > > pve-network: > > Hannes Laimer (2): > sdn: fabrics: register bgp as a fabric protocol type > test: evpn: add integration test for EVPN over BGP fabric > > src/PVE/Network/SDN/Fabrics.pm | 62 +++++++++++++- > src/PVE/Network/SDN/RouteMaps.pm | 15 ++++ > .../bgp_fabric/expected_controller_config | 73 ++++++++++++++++ > .../evpn/bgp_fabric/expected_sdn_interfaces | 60 +++++++++++++ > src/test/zones/evpn/bgp_fabric/interfaces | 6 ++ > src/test/zones/evpn/bgp_fabric/sdn_config | 85 +++++++++++++++++++ > 6 files changed, 300 insertions(+), 1 deletion(-) > create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config > create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces > create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces > create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config > > > pve-manager: > > Hannes Laimer (1): > ui: sdn: add BGP fabric support > > www/manager6/Makefile | 3 + > www/manager6/sdn/FabricsView.js | 12 ++++ > www/manager6/sdn/fabrics/FabricEdit.js | 12 +++- > www/manager6/sdn/fabrics/NodeEdit.js | 1 + > www/manager6/sdn/fabrics/bgp/FabricEdit.js | 67 +++++++++++++++++++ > .../sdn/fabrics/bgp/InterfacePanel.js | 15 +++++ > www/manager6/sdn/fabrics/bgp/NodeEdit.js | 23 +++++++ > 7 files changed, 130 insertions(+), 3 deletions(-) > create mode 100644 www/manager6/sdn/fabrics/bgp/FabricEdit.js > create mode 100644 www/manager6/sdn/fabrics/bgp/InterfacePanel.js > create mode 100644 www/manager6/sdn/fabrics/bgp/NodeEdit.js > > > pve-docs: > > Hannes Laimer (1): > sdn: add bgp fabric section > > pvesdn.adoc | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 96 insertions(+) > > > Summary over all repositories: > 32 files changed, 2019 insertions(+), 21 deletions(-) > ^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-05-13 18:43 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer
2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer
2026-05-13 12:29 ` Stefan Hanreich
2026-05-13 13:00 ` Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-05-13 12:33 ` Stefan Hanreich
2026-05-13 13:02 ` Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer
2026-05-13 12:38 ` Stefan Hanreich
2026-05-12 14:13 ` [PATCH pve-docs v4 7/7] sdn: add bgp fabric section Hannes Laimer
2026-05-13 12:39 ` [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Stefan Hanreich
2026-05-13 18:43 ` superseded: " Hannes Laimer
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.