public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric
@ 2026-03-27 15:10 Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support Hannes Laimer
                   ` (5 more replies)
  0 siblings, 6 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 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.

## 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, the overlay sessions run
as eBGP too. The node's fabric ASN becomes the 'router bgp' process ASN.
Since every node has a different ASN, auto-derived route targets would differ
per node, so the EVPN controller ASN is used via 'autort as' to produce
uniform RTs. This relies on a patch we ship with our frr, upstream
reference: [1]. Two additional flags are enabled:
'bestpath as-path multipath-relax' for ECMP across different ASNs, and
'disable-ebgp-connected-route-check' for loopback-based VTEP peers.

An iBGP overlay using 'local-as' was considered but rejected due to a
limitation in how FRR handles hop limits. See the EVPN commit message for
details.

## Single BGP process

Unlike OSPF and OpenFabric which have their own FRR protocol blocks, BGP
fabric config must coexist with the EVPN BGP config in one 'router bgp'
instance. The fabric generates its own BgpRouter and merges it into the
existing one via merge_fabric(), appending neighbor groups and address
families without overwriting the EVPN settings.


Thanks a lot @Stefan for the base of this series!


[1] https://github.com/FRRouting/frr/issues/17945



proxmox-ve-rs:

Hannes Laimer (1):
  sdn: fabric: add BGP protocol support

 proxmox-frr/src/ser/bgp.rs                    |  66 +++++
 proxmox-frr/src/ser/mod.rs                    |   1 +
 proxmox-ve-config/src/sdn/fabric/frr.rs       | 258 ++++++++++++++++-
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 100 +++++++
 .../src/sdn/fabric/section_config/fabric.rs   |  22 ++
 .../src/sdn/fabric/section_config/mod.rs      |  19 ++
 .../src/sdn/fabric/section_config/node.rs     |  21 ++
 .../sdn/fabric/section_config/protocol/bgp.rs | 266 ++++++++++++++++++
 .../sdn/fabric/section_config/protocol/mod.rs |   1 +
 .../tests/fabric/cfg/bgp_default/fabrics.cfg  |  17 ++
 .../fabric/cfg/bgp_ipv6_only/fabrics.cfg      |  17 ++
 proxmox-ve-config/tests/fabric/main.rs        |  58 ++++
 .../snapshots/fabric__bgp_default_pve.snap    |  28 ++
 .../snapshots/fabric__bgp_default_pve1.snap   |  27 ++
 .../snapshots/fabric__bgp_ipv6_only_pve.snap  |  29 ++
 .../snapshots/fabric__bgp_ipv6_only_pve1.snap |  28 ++
 16 files changed, 955 insertions(+), 3 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap


proxmox-perl-rs:

Hannes Laimer (2):
  sdn: fabrics: add BGP config generation
  sdn: fabrics: add BGP status endpoints

 pve-rs/src/bindings/sdn/fabrics.rs | 100 +++++++++++++++++++++++++++
 pve-rs/src/sdn/status.rs           | 105 ++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 2 deletions(-)


pve-network:

Hannes Laimer (2):
  sdn: fabrics: register bgp as a fabric protocol type
  sdn: evpn: support eBGP EVPN over BGP fabric underlay

 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 40 ++++++++-
 src/PVE/Network/SDN/Fabrics.pm                | 18 +++-
 .../bgp_fabric/expected_controller_config     | 73 ++++++++++++++++
 .../evpn/bgp_fabric/expected_sdn_interfaces   | 56 ++++++++++++
 src/test/zones/evpn/bgp_fabric/interfaces     |  6 ++
 src/test/zones/evpn/bgp_fabric/sdn_config     | 85 +++++++++++++++++++
 6 files changed, 275 insertions(+), 3 deletions(-)
 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/NodeEdit.js          |  1 +
 www/manager6/sdn/fabrics/bgp/FabricEdit.js    | 52 +++++++++++++++++++
 .../sdn/fabrics/bgp/InterfacePanel.js         | 15 ++++++
 www/manager6/sdn/fabrics/bgp/NodeEdit.js      | 32 ++++++++++++
 6 files changed, 115 insertions(+)
 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


Summary over all repositories:
  30 files changed, 1548 insertions(+), 8 deletions(-)

-- 
Generated by murpp 0.9.0




^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-perl-rs 1/2] sdn: fabrics: add BGP config generation Hannes Laimer
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 UTC (permalink / raw)
  To: pve-devel

Add BGP as a fabric protocol for eBGP unnumbered underlays. Each node
has a mandatory, globally unique ASN for interface-based eBGP peering.

Since the EVPN controller shares the same 'router bgp' block, the
fabric merges its neighbor group and address families into an existing
router config rather than replacing it. This is unlike OSPF and
OpenFabric which have their own separate protocol blocks.

Co-authored-by: Hannes Laimer <h.laimer@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 proxmox-frr/src/ser/bgp.rs                    |  66 +++++
 proxmox-frr/src/ser/mod.rs                    |   1 +
 proxmox-ve-config/src/sdn/fabric/frr.rs       | 258 ++++++++++++++++-
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 100 +++++++
 .../src/sdn/fabric/section_config/fabric.rs   |  22 ++
 .../src/sdn/fabric/section_config/mod.rs      |  19 ++
 .../src/sdn/fabric/section_config/node.rs     |  21 ++
 .../sdn/fabric/section_config/protocol/bgp.rs | 266 ++++++++++++++++++
 .../sdn/fabric/section_config/protocol/mod.rs |   1 +
 .../tests/fabric/cfg/bgp_default/fabrics.cfg  |  17 ++
 .../fabric/cfg/bgp_ipv6_only/fabrics.cfg      |  17 ++
 proxmox-ve-config/tests/fabric/main.rs        |  58 ++++
 .../snapshots/fabric__bgp_default_pve.snap    |  28 ++
 .../snapshots/fabric__bgp_default_pve1.snap   |  27 ++
 .../snapshots/fabric__bgp_ipv6_only_pve.snap  |  29 ++
 .../snapshots/fabric__bgp_ipv6_only_pve1.snap |  28 ++
 16 files changed, 955 insertions(+), 3 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap

diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
index 6eb7c57..45b0c2e 100644
--- a/proxmox-frr/src/ser/bgp.rs
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -142,6 +142,72 @@ pub struct AddressFamilies {
     pub l2vpn_evpn: Option<L2vpnEvpnAF>,
 }
 
+impl AddressFamilies {
+    /// Merge another [`AddressFamilies`] into this one.
+    ///
+    /// For each address family: if `self` already has it, extend its neighbors, networks, and
+    /// redistribute lists. If `self` doesn't have it, take it from `other`.
+    pub fn merge(&mut self, other: AddressFamilies) {
+        match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) {
+            (Some(existing), Some(incoming)) => {
+                existing
+                    .common_options
+                    .neighbors
+                    .extend(incoming.common_options.neighbors);
+                existing
+                    .common_options
+                    .import_vrf
+                    .extend(incoming.common_options.import_vrf);
+                existing.networks.extend(incoming.networks);
+                existing.redistribute.extend(incoming.redistribute);
+            }
+            (None, Some(incoming)) => {
+                self.ipv4_unicast = Some(incoming);
+            }
+            _ => {}
+        }
+
+        match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) {
+            (Some(existing), Some(incoming)) => {
+                existing
+                    .common_options
+                    .neighbors
+                    .extend(incoming.common_options.neighbors);
+                existing
+                    .common_options
+                    .import_vrf
+                    .extend(incoming.common_options.import_vrf);
+                existing.networks.extend(incoming.networks);
+                existing.redistribute.extend(incoming.redistribute);
+            }
+            (None, Some(incoming)) => {
+                self.ipv6_unicast = Some(incoming);
+            }
+            _ => {}
+        }
+
+        // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this)
+        if self.l2vpn_evpn.is_none() {
+            self.l2vpn_evpn = other.l2vpn_evpn;
+        }
+    }
+}
+
+impl BgpRouter {
+    /// Merge a fabric-generated [`BgpRouter`] into an existing one.
+    ///
+    /// Appends the fabric's neighbor groups and merges address families. Keeps the existing
+    /// router's ASN, router-id, and other top-level settings.
+    pub fn merge_fabric(&mut self, other: BgpRouter) {
+        self.neighbor_groups.extend(other.neighbor_groups);
+        self.address_families.merge(other.address_families);
+
+        if self.default_ipv4_unicast.is_none() {
+            self.default_ipv4_unicast = other.default_ipv4_unicast;
+        }
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 pub struct Vrf {
     pub vni: Option<u32>,
diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs
index 692fb7f..107837b 100644
--- a/proxmox-frr/src/ser/mod.rs
+++ b/proxmox-frr/src/ser/mod.rs
@@ -113,6 +113,7 @@ impl InterfaceName {
             Err(InterfaceNameError::TooLong)
         }
     }
+
     fn from_string_unchecked(s: String) -> InterfaceName {
         Self(s)
     }
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index f2b7c72..f1e0a46 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -1,7 +1,11 @@
-use std::net::{IpAddr, Ipv4Addr};
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 
 use tracing;
 
+use proxmox_frr::ser::bgp::{
+    AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF,
+    Ipv6UnicastAF, NeighborGroup, NeighborRemoteAs, RedistributeProtocol, Redistribution,
+};
 use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
 use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
 use proxmox_frr::ser::route_map::{
@@ -9,13 +13,16 @@ use proxmox_frr::ser::route_map::{
     RouteMapMatchInner, RouteMapName, RouteMapSet,
 };
 use proxmox_frr::ser::{
-    self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
+    self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, VrfName,
 };
-use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
 use proxmox_sdn_types::net::Net;
 
 use crate::common::valid::Valid;
+
+use crate::sdn::fabric::section_config::protocol::bgp::BgpNode;
 use crate::sdn::fabric::section_config::protocol::{
+    bgp::BgpRedistributionSource,
     openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
     ospf::OspfInterfaceProperties,
 };
@@ -278,6 +285,203 @@ pub fn build_fabric(
                     );
                 }
             }
+            FabricEntry::Bgp(bgp_entry) => {
+                let Ok(node) = bgp_entry.node_section(&current_node) else {
+                    continue;
+                };
+
+                let BgpNode::Internal(properties) = node.properties() else {
+                    continue;
+                };
+
+                let fabric = bgp_entry.fabric_section();
+
+                let local_asn = properties.asn().as_u32();
+
+                let mut bgp_interfaces = Vec::new();
+                for interface in properties.interfaces() {
+                    bgp_interfaces.push(interface.name.as_str().try_into()?)
+                }
+
+                let neighbor_group = NeighborGroup {
+                    name: FrrWord::new(fabric.id().to_string())?,
+                    bfd: fabric.properties().bfd(),
+                    remote_as: NeighborRemoteAs::External,
+                    local_as: Default::default(),
+                    interfaces: bgp_interfaces,
+                    ips: Default::default(),
+                    ebgp_multihop: Default::default(),
+                    update_source: Default::default(),
+                };
+
+                let redistribute: Vec<Redistribution> = fabric
+                    .properties()
+                    .redistribute
+                    .iter()
+                    .map(|redistribution| Redistribution {
+                        protocol: match redistribution.source {
+                            BgpRedistributionSource::Ospf => RedistributeProtocol::Ospf,
+                            BgpRedistributionSource::Connected => RedistributeProtocol::Connected,
+                            BgpRedistributionSource::Isis => RedistributeProtocol::Isis,
+                            BgpRedistributionSource::Kernel => RedistributeProtocol::Kernel,
+                            BgpRedistributionSource::Openfabric => RedistributeProtocol::Openfabric,
+                            BgpRedistributionSource::Ospf6 => RedistributeProtocol::Ospf6,
+                            BgpRedistributionSource::Static => RedistributeProtocol::Static,
+                        },
+                        metric: redistribution.metric,
+                        route_map: redistribution.route_map.clone().map(RouteMapName::new),
+                    })
+                    .collect();
+
+                let af_neighbors = vec![AddressFamilyNeighbor {
+                    name: fabric.id().to_string(),
+                    route_map_in: None,
+                    route_map_out: None,
+                    soft_reconfiguration_inbound: Some(true),
+                }];
+
+                let ipv4_family = node.ip().map(|ipv4| Ipv4UnicastAF {
+                    common_options: CommonAddressFamilyOptions {
+                        import_vrf: Default::default(),
+                        neighbors: af_neighbors.clone(),
+                        custom_frr_config: Default::default(),
+                    },
+                    redistribute: redistribute.clone(),
+                    networks: vec![Ipv4Cidr::from(ipv4)],
+                });
+
+                let ipv6_family = node.ip6().map(|ipv6| Ipv6UnicastAF {
+                    common_options: CommonAddressFamilyOptions {
+                        import_vrf: Default::default(),
+                        neighbors: af_neighbors,
+                        custom_frr_config: Default::default(),
+                    },
+                    networks: vec![Ipv6Cidr::from(ipv6)],
+                    redistribute,
+                });
+
+                let address_families = AddressFamilies {
+                    ipv4_unicast: ipv4_family,
+                    ipv6_unicast: ipv6_family,
+                    ..Default::default()
+                };
+
+                let router_id = match (node.ip(), node.ip6()) {
+                    (Some(ipv4), _) => ipv4,
+                    (None, Some(ipv6)) => router_id_from_ipv6(&ipv6),
+                    (None, None) => {
+                        anyhow::bail!("BGP node must have an IPv4 or IPv6 address")
+                    }
+                };
+
+                let router = BgpRouter {
+                    asn: local_asn,
+                    router_id,
+                    neighbor_groups: vec![neighbor_group],
+                    address_families,
+                    coalesce_time: Default::default(),
+                    default_ipv4_unicast: Some(false),
+                    hard_administrative_reset: Default::default(),
+                    graceful_restart_notification: Default::default(),
+                    disable_ebgp_connected_route_check: Default::default(),
+                    bestpath_as_path_multipath_relax: Default::default(),
+                    custom_frr_config: Default::default(),
+                };
+
+                if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) {
+                    existing.merge_fabric(router);
+                } else {
+                    frr_config.bgp.vrf_router.insert(VrfName::Default, router);
+                }
+
+                // Create access-list and route-map for source address selection,
+                // so the kernel uses the loopback IP as source for fabric-learned routes.
+                if let Some(ipv4_prefix) = fabric.ip_prefix() {
+                    let access_list_name =
+                        AccessListName::new(format!("pve_bgp_{}_ips", fabric_id));
+
+                    let rule = ser::route_map::AccessListRule {
+                        action: AccessAction::Permit,
+                        network: Cidr::from(ipv4_prefix),
+                        is_ipv6: false,
+                        seq: None,
+                    };
+
+                    frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+                    let (routemap_name, routemap_rule) = build_bgp_routemap(
+                        fabric_id,
+                        IpAddr::from(node.ip().expect("node must have an ipv4 address")),
+                        routemap_seq,
+                    );
+
+                    routemap_seq += 10;
+
+                    if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+                        routemap.push(routemap_rule)
+                    } else {
+                        frr_config
+                            .routemaps
+                            .insert(routemap_name.clone(), vec![routemap_rule]);
+                    }
+
+                    if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+                    {
+                        routemap.v4 = Some(routemap_name);
+                    } else {
+                        frr_config.protocol_routemaps.insert(
+                            FrrProtocol::Bgp,
+                            IpProtocolRouteMap {
+                                v4: Some(routemap_name),
+                                v6: None,
+                            },
+                        );
+                    }
+                }
+
+                if let Some(ipv6_prefix) = fabric.ip6_prefix() {
+                    let access_list_name =
+                        AccessListName::new(format!("pve_bgp_{}_ip6s", fabric_id));
+
+                    let rule = ser::route_map::AccessListRule {
+                        action: AccessAction::Permit,
+                        network: Cidr::from(ipv6_prefix),
+                        is_ipv6: true,
+                        seq: None,
+                    };
+
+                    frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+                    let (routemap_name, routemap_rule) = build_bgp_routemap(
+                        fabric_id,
+                        IpAddr::from(node.ip6().expect("node must have an ipv6 address")),
+                        routemap_seq,
+                    );
+
+                    routemap_seq += 10;
+
+                    if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+                        routemap.push(routemap_rule)
+                    } else {
+                        frr_config
+                            .routemaps
+                            .insert(routemap_name.clone(), vec![routemap_rule]);
+                    }
+
+                    if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+                    {
+                        routemap.v6 = Some(routemap_name);
+                    } else {
+                        frr_config.protocol_routemaps.insert(
+                            FrrProtocol::Bgp,
+                            IpProtocolRouteMap {
+                                v4: None,
+                                v6: Some(routemap_name),
+                            },
+                        );
+                    }
+                }
+            }
         }
     }
 
@@ -419,6 +623,39 @@ fn build_openfabric_routemap(
     )
 }
 
+/// Helper that builds a RouteMap for the BGP protocol.
+fn build_bgp_routemap(
+    fabric_id: &FabricId,
+    router_ip: IpAddr,
+    seq: u32,
+) -> (RouteMapName, RouteMapEntry) {
+    let routemap_name = match router_ip {
+        IpAddr::V4(_) => RouteMapName::new("pve_bgp".to_owned()),
+        IpAddr::V6(_) => RouteMapName::new("pve_bgp6".to_owned()),
+    };
+    (
+        routemap_name,
+        RouteMapEntry {
+            seq,
+            action: AccessAction::Permit,
+            matches: vec![match router_ip {
+                IpAddr::V4(_) => RouteMapMatch::V4(RouteMapMatchInner::Address(
+                    AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+                        "pve_bgp_{fabric_id}_ips"
+                    ))),
+                )),
+                IpAddr::V6(_) => RouteMapMatch::V6(RouteMapMatchInner::Address(
+                    AccessListOrPrefixList::AccessList(AccessListName::new(format!(
+                        "pve_bgp_{fabric_id}_ip6s"
+                    ))),
+                )),
+            }],
+            sets: vec![RouteMapSet::Src(router_ip)],
+            custom_frr_config: Vec::new(),
+        },
+    )
+}
+
 /// Helper that builds a RouteMap for the OSPF protocol.
 fn build_ospf_dummy_routemap(
     fabric_id: &FabricId,
@@ -441,3 +678,18 @@ fn build_ospf_dummy_routemap(
 
     Ok((routemap_name, routemap))
 }
+
+/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a.
+///
+/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the
+/// full 16 octets down to 4 bytes. Collision probability follows the birthday
+/// bound: <0.01% at 10k routers, ~0.25% at 50k. Sequential and sparse
+/// loopback allocations both distribute well with the standard FNV constants.
+fn router_id_from_ipv6(addr: &Ipv6Addr) -> Ipv4Addr {
+    let mut hash: u32 = 0x811c9dc5;
+    for &byte in &addr.octets() {
+        hash ^= byte as u32;
+        hash = hash.wrapping_mul(0x01000193);
+    }
+    Ipv4Addr::from(hash)
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 677a309..4ad848a 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -20,6 +20,9 @@ use crate::sdn::fabric::section_config::node::{
     api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
     Node, NodeId, NodeSection,
 };
+use crate::sdn::fabric::section_config::protocol::bgp::{
+    BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater, BgpProperties,
+};
 use crate::sdn::fabric::section_config::protocol::openfabric::{
     OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
     OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater,
@@ -64,6 +67,8 @@ pub enum FabricConfigError {
     // this is technically possible, but we don't allow it
     #[error("duplicate OSPF area")]
     DuplicateOspfArea,
+    #[error("BGP fabric '{0}' has nodes with duplicate ASN {1}")]
+    DuplicateBgpAsn(String, u32),
     #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")]
     OverlappingIp4Prefix(String, String, String, String),
     #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")]
@@ -162,6 +167,33 @@ where
     }
 }
 
+impl Entry<BgpProperties, BgpNode> {
+    /// Get the BGP fabric config.
+    ///
+    /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+    /// so it is guaranteed that a [`FabricSection<BgpProperties>`] is returned.
+    pub fn fabric_section(&self) -> &FabricSection<BgpProperties> {
+        if let Fabric::Bgp(section) = &self.fabric {
+            return section;
+        }
+
+        unreachable!();
+    }
+
+    /// Get the BGP node config for the given node_id.
+    ///
+    /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+    /// so it is guaranteed that a [`NodeSection<BgpNode>`] is returned.
+    /// An error is returned if the node is not found.
+    pub fn node_section(&self, id: &NodeId) -> Result<&NodeSection<BgpNode>, FabricConfigError> {
+        if let Node::Bgp(section) = self.get_node(id)? {
+            return Ok(section);
+        }
+
+        unreachable!();
+    }
+}
+
 impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
     /// Get the OpenFabric fabric config.
     ///
@@ -230,6 +262,7 @@ impl Entry<OspfProperties, OspfNodeProperties> {
 pub enum FabricEntry {
     Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
     Ospf(Entry<OspfProperties, OspfNodeProperties>),
+    Bgp(Entry<BgpProperties, BgpNode>),
 }
 
 impl FabricEntry {
@@ -241,6 +274,7 @@ impl FabricEntry {
                 entry.add_node(node_section)
             }
             (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+            (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section),
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -251,6 +285,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node(id),
             FabricEntry::Ospf(entry) => entry.get_node(id),
+            FabricEntry::Bgp(entry) => entry.get_node(id),
         }
     }
 
@@ -260,6 +295,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
             FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+            FabricEntry::Bgp(entry) => entry.get_node_mut(id),
         }
     }
 
@@ -339,6 +375,46 @@ impl FabricEntry {
 
                 Ok(())
             }
+            (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => {
+                let BgpNode::Internal(ref mut props) = node_section.properties else {
+                    return Err(FabricConfigError::ProtocolMismatch);
+                };
+
+                let NodeDataUpdater::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> {
+                    ip,
+                    ip6,
+                    properties: BgpNodePropertiesUpdater { asn, interfaces },
+                    delete,
+                } = updater;
+
+                if let Some(ip) = ip {
+                    node_section.ip = Some(ip);
+                }
+
+                if let Some(ip) = ip6 {
+                    node_section.ip6 = Some(ip);
+                }
+
+                if let Some(asn) = asn {
+                    props.asn = asn;
+                }
+
+                if let Some(interfaces) = interfaces {
+                    props.interfaces = interfaces;
+                }
+
+                for property in delete {
+                    match property {
+                        NodeDeletableProperties::Ip => node_section.ip = None,
+                        NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+                        NodeDeletableProperties::Protocol(
+                            BgpNodeDeletableProperties::Interfaces,
+                        ) => props.interfaces = Vec::new(),
+                    }
+                }
+
+                Ok(())
+            }
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -348,6 +424,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.nodes.iter(),
             FabricEntry::Ospf(entry) => entry.nodes.iter(),
+            FabricEntry::Bgp(entry) => entry.nodes.iter(),
         }
     }
 
@@ -356,6 +433,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.delete_node(id),
             FabricEntry::Ospf(entry) => entry.delete_node(id),
+            FabricEntry::Bgp(entry) => entry.delete_node(id),
         }
     }
 
@@ -365,6 +443,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.into_pair(),
             FabricEntry::Ospf(entry) => entry.into_pair(),
+            FabricEntry::Bgp(entry) => entry.into_pair(),
         }
     }
 
@@ -373,6 +452,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &entry.fabric,
             FabricEntry::Ospf(entry) => &entry.fabric,
+            FabricEntry::Bgp(entry) => &entry.fabric,
         }
     }
 
@@ -381,6 +461,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &mut entry.fabric,
             FabricEntry::Ospf(entry) => &mut entry.fabric,
+            FabricEntry::Bgp(entry) => &mut entry.fabric,
         }
     }
 }
@@ -392,6 +473,7 @@ impl From<Fabric> for FabricEntry {
                 FabricEntry::Openfabric(Entry::new(fabric_section))
             }
             Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+            Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)),
         }
     }
 }
@@ -513,6 +595,7 @@ impl Validatable for FabricConfig {
     fn validate(&self) -> Result<(), FabricConfigError> {
         let mut node_interfaces = HashSet::new();
         let mut ospf_area = HashSet::new();
+        let mut bgp_asns = HashSet::new();
 
         // Check for overlapping IP prefixes across fabrics
         let fabrics: Vec<_> = self.fabrics.values().map(|f| f.fabric()).collect();
@@ -573,6 +656,23 @@ impl Validatable for FabricConfig {
                             return Err(FabricConfigError::DuplicateInterface);
                         }
                     }
+                    Node::Bgp(node_section) => {
+                        if let BgpNode::Internal(props) = node_section.properties() {
+                            if !props.interfaces().all(|interface| {
+                                node_interfaces.insert((node_id, interface.name().as_str()))
+                            }) {
+                                return Err(FabricConfigError::DuplicateInterface);
+                            }
+
+                            let asn = props.asn().as_u32();
+                            if !bgp_asns.insert(asn) {
+                                return Err(FabricConfigError::DuplicateBgpAsn(
+                                    entry.fabric().id().to_string(),
+                                    asn,
+                                ));
+                            }
+                        }
+                    }
                 }
             }
 
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 38911a6..9f41eae 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -8,6 +8,9 @@ use proxmox_schema::{
 };
 
 use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{
+    BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater,
+};
 use crate::sdn::fabric::section_config::protocol::openfabric::{
     OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
 };
@@ -139,6 +142,10 @@ impl UpdaterType for FabricSection<OspfProperties> {
     type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
 }
 
+impl UpdaterType for FabricSection<BgpProperties> {
+    type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>;
+}
+
 /// Enum containing all types of fabrics.
 ///
 /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
@@ -159,6 +166,7 @@ impl UpdaterType for FabricSection<OspfProperties> {
 pub enum Fabric {
     Openfabric(FabricSection<OpenfabricProperties>),
     Ospf(FabricSection<OspfProperties>),
+    Bgp(FabricSection<BgpProperties>),
 }
 
 impl UpdaterType for Fabric {
@@ -173,6 +181,7 @@ impl Fabric {
         match self {
             Self::Openfabric(fabric_section) => fabric_section.id(),
             Self::Ospf(fabric_section) => fabric_section.id(),
+            Self::Bgp(fabric_section) => fabric_section.id(),
         }
     }
 
@@ -183,6 +192,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
+            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(),
         }
     }
 
@@ -193,6 +203,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
+            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
         }
     }
 
@@ -203,6 +214,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
+            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(),
         }
     }
 
@@ -213,6 +225,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
+            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
         }
     }
 }
@@ -225,6 +238,7 @@ impl Validatable for Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.validate(),
             Fabric::Ospf(fabric_section) => fabric_section.validate(),
+            Fabric::Bgp(fabric_section) => fabric_section.validate(),
         }
     }
 }
@@ -241,12 +255,19 @@ impl From<FabricSection<OspfProperties>> for Fabric {
     }
 }
 
+impl From<FabricSection<BgpProperties>> for Fabric {
+    fn from(section: FabricSection<BgpProperties>) -> Self {
+        Fabric::Bgp(section)
+    }
+}
+
 /// Enum containing all updater types for fabrics
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case", tag = "protocol")]
 pub enum FabricUpdater {
     Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
     Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
+    Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater),
 }
 
 impl Updater for FabricUpdater {
@@ -254,6 +275,7 @@ impl Updater for FabricUpdater {
         match self {
             FabricUpdater::Openfabric(updater) => updater.is_empty(),
             FabricUpdater::Ospf(updater) => updater.is_empty(),
+            FabricUpdater::Bgp(updater) => updater.is_empty(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index d02d4ae..698dac9 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -10,6 +10,7 @@ use crate::sdn::fabric::section_config::{
     fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR},
     node::{Node, NodeSection, NODE_ID_REGEX_STR},
     protocol::{
+        bgp::{BgpNode, BgpProperties},
         openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
         ospf::{OspfNodeProperties, OspfProperties},
     },
@@ -31,8 +32,10 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
         match section {
             Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
+            Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
             Section::OspfNode(node_section) => Self::Node(node_section.into()),
+            Section::BgpNode(node_section) => Self::Node(node_section.into()),
         }
     }
 }
@@ -62,8 +65,10 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION
 pub enum Section {
     OpenfabricFabric(FabricSection<OpenfabricProperties>),
     OspfFabric(FabricSection<OspfProperties>),
+    BgpFabric(FabricSection<BgpProperties>),
     OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
     OspfNode(NodeSection<OspfNodeProperties>),
+    BgpNode(NodeSection<BgpNode>),
 }
 
 impl From<FabricSection<OpenfabricProperties>> for Section {
@@ -78,6 +83,12 @@ impl From<FabricSection<OspfProperties>> for Section {
     }
 }
 
+impl From<FabricSection<BgpProperties>> for Section {
+    fn from(section: FabricSection<BgpProperties>) -> Self {
+        Self::BgpFabric(section)
+    }
+}
+
 impl From<NodeSection<OpenfabricNodeProperties>> for Section {
     fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
         Self::OpenfabricNode(section)
@@ -90,11 +101,18 @@ impl From<NodeSection<OspfNodeProperties>> for Section {
     }
 }
 
+impl From<NodeSection<BgpNode>> for Section {
+    fn from(section: NodeSection<BgpNode>) -> Self {
+        Self::BgpNode(section)
+    }
+}
+
 impl From<Fabric> for Section {
     fn from(fabric: Fabric) -> Self {
         match fabric {
             Fabric::Openfabric(fabric_section) => fabric_section.into(),
             Fabric::Ospf(fabric_section) => fabric_section.into(),
+            Fabric::Bgp(fabric_section) => fabric_section.into(),
         }
     }
 }
@@ -104,6 +122,7 @@ impl From<Node> for Section {
         match node {
             Node::Openfabric(node_section) => node_section.into(),
             Node::Ospf(node_section) => node_section.into(),
+            Node::Bgp(node_section) => node_section.into(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 17d2f0b..8f4564c 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -10,6 +10,7 @@ use proxmox_schema::{
 };
 
 use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties};
 use crate::sdn::fabric::section_config::{
     fabric::{FabricId, FABRIC_ID_REGEX_STR},
     protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
@@ -186,6 +187,7 @@ impl<T: ApiType> ApiType for NodeSection<T> {
 pub enum Node {
     Openfabric(NodeSection<OpenfabricNodeProperties>),
     Ospf(NodeSection<OspfNodeProperties>),
+    Bgp(NodeSection<BgpNode>),
 }
 
 impl Node {
@@ -194,6 +196,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.id(),
             Node::Ospf(node_section) => node_section.id(),
+            Node::Bgp(node_section) => node_section.id(),
         }
     }
 
@@ -202,6 +205,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip(),
             Node::Ospf(node_section) => node_section.ip(),
+            Node::Bgp(node_section) => node_section.ip(),
         }
     }
 
@@ -210,6 +214,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip6(),
             Node::Ospf(node_section) => node_section.ip6(),
+            Node::Bgp(node_section) => node_section.ip6(),
         }
     }
 }
@@ -221,6 +226,7 @@ impl Validatable for Node {
         match self {
             Node::Openfabric(node_section) => node_section.validate(),
             Node::Ospf(node_section) => node_section.validate(),
+            Node::Bgp(node_section) => node_section.validate(),
         }
     }
 }
@@ -237,6 +243,12 @@ impl From<NodeSection<OspfNodeProperties>> for Node {
     }
 }
 
+impl From<NodeSection<BgpNode>> for Node {
+    fn from(value: NodeSection<BgpNode>) -> Self {
+        Self::Bgp(value)
+    }
+}
+
 /// API types for SDN fabric node configurations.
 ///
 /// This module provides specialized types that are used for API interactions when retrieving,
@@ -258,6 +270,7 @@ pub mod api {
     use proxmox_schema::{Updater, UpdaterType};
 
     use crate::sdn::fabric::section_config::protocol::{
+        bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater},
         openfabric::{
             OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
             OpenfabricNodePropertiesUpdater,
@@ -320,6 +333,7 @@ pub mod api {
     pub enum Node {
         Openfabric(NodeData<OpenfabricNodeProperties>),
         Ospf(NodeData<OspfNodeProperties>),
+        Bgp(NodeData<BgpNode>),
     }
 
     impl From<super::Node> for Node {
@@ -327,6 +341,7 @@ pub mod api {
             match value {
                 super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                super::Node::Bgp(node_section) => Self::Bgp(node_section.into()),
             }
         }
     }
@@ -336,6 +351,7 @@ pub mod api {
             match value {
                 Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                Node::Bgp(node_section) => Self::Bgp(node_section.into()),
             }
         }
     }
@@ -349,6 +365,10 @@ pub mod api {
         type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
     }
 
+    impl UpdaterType for NodeData<BgpNodeProperties> {
+        type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>;
+    }
+
     #[derive(Debug, Clone, Serialize, Deserialize)]
     pub struct NodeDataUpdater<T, D> {
         #[serde(skip_serializing_if = "Option::is_none")]
@@ -384,6 +404,7 @@ pub mod api {
             NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
         ),
         Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+        Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>),
     }
 
     #[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
new file mode 100644
index 0000000..652ffdd
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
@@ -0,0 +1,266 @@
+use std::ops::{Deref, DerefMut};
+
+use proxmox_network_types::ip_address::api_types::Ipv4Addr;
+use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::fabric::FabricSection;
+use crate::sdn::fabric::section_config::interface::InterfaceName;
+use crate::sdn::fabric::section_config::node::NodeSection;
+use crate::sdn::fabric::FabricConfigError;
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+#[serde(rename_all = "lowercase")]
+/// Redistribution Sources for BGP fabric
+pub enum BgpRedistributionSource {
+    /// redistribute connected routes
+    Connected,
+    /// redistribute IS-IS routes
+    Isis,
+    /// redistribute kernel routes
+    Kernel,
+    /// redistribute openfabric routes
+    Openfabric,
+    /// redistribute ospfv2 routes
+    Ospf,
+    /// redistribute ospfv3 routes
+    Ospf6,
+    /// redistribute static routes
+    Static,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// A BGP redistribution target
+pub struct BgpRedistribution {
+    /// The source used for redistribution
+    pub(crate) source: BgpRedistributionSource,
+    /// The metric to apply to redistributed routes
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub(crate) metric: Option<u32>,
+    /// Route MAP to use for filtering redistributed routes
+    #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")]
+    pub(crate) route_map: Option<String>,
+}
+
+#[api(
+    type: Integer,
+    minimum: u32::MIN as i64,
+    maximum: u32::MAX as i64,
+)]
+#[derive(Debug, Clone, Serialize, Updater, Hash)]
+/// Autonomous system number as defined by RFC 6793
+pub struct ASN(u32);
+
+impl<'de> Deserialize<'de> for ASN {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::{self, Visitor};
+
+        struct AsnVisitor;
+
+        impl<'de> Visitor<'de> for AsnVisitor {
+            type Value = ASN;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a u32 or string containing a u32")
+            }
+
+            fn visit_i64<E: de::Error>(self, v: i64) -> Result<ASN, E> {
+                u32::try_from(v)
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
+            }
+
+            fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> {
+                u32::try_from(v)
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
+            }
+
+            fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> {
+                v.parse::<u32>()
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("invalid ASN: {v}")))
+            }
+        }
+
+        deserializer.deserialize_any(AsnVisitor)
+    }
+}
+
+impl UpdaterType for ASN {
+    type Updater = Option<ASN>;
+}
+
+impl ASN {
+    pub fn as_u32(&self) -> u32 {
+        self.0
+    }
+}
+
+#[api(
+    properties: {
+        redistribute: {
+            type: Array,
+            optional: true,
+            items: {
+                type: String,
+                description: "A BGP redistribution source",
+                format: &ApiStringFormat::PropertyString(&BgpRedistribution::API_SCHEMA),
+            }
+        }
+    },
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Bgp fabric.
+pub struct BgpProperties {
+    /// enable BFD for this fabric
+    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub(crate) bfd: bool,
+    /// redistribution configuration for this fabric
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    #[updater(serde(skip_serializing_if = "Option::is_none"))]
+    pub(crate) redistribute: Vec<PropertyString<BgpRedistribution>>,
+}
+
+impl BgpProperties {
+    pub fn bfd(&self) -> bool {
+        self.bfd
+    }
+}
+
+impl Validatable for FabricSection<BgpProperties> {
+    type Error = FabricConfigError;
+
+    /// Validate the [`FabricSection<BgpProperties>`].
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum BgpDeletableProperties {}
+
+#[api]
+/// External Bgp Node
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct ExternalBgpNode {
+    peer_ip: Option<Ipv4Addr>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "role")]
+pub enum BgpNode {
+    Internal(BgpNodeProperties),
+    External(ExternalBgpNode),
+}
+
+impl ApiType for BgpNode {
+    const API_SCHEMA: Schema = OneOfSchema::new(
+        "BGP node",
+        &(
+            "role",
+            false,
+            &StringSchema::new("internal or external").schema(),
+        ),
+        &[
+            ("external", &ExternalBgpNode::API_SCHEMA),
+            ("internal", &BgpNodeProperties::API_SCHEMA),
+        ],
+    )
+    .schema();
+}
+
+impl Validatable for NodeSection<BgpNode> {
+    type Error = FabricConfigError;
+
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[api(
+    properties: {
+        interfaces: {
+            type: Array,
+            optional: true,
+            items: {
+                type: String,
+                description: "Properties for an Bgp interface.",
+                format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA),
+            }
+        },
+    }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Bgp node.
+pub struct BgpNodeProperties {
+    /// Autonomous system number for this Node
+    pub(crate) asn: ASN,
+    /// Interfaces for this Node.
+    #[serde(default)]
+    pub(crate) interfaces: Vec<PropertyString<BgpInterfaceProperties>>,
+}
+
+impl BgpNodeProperties {
+    /// Returns the ASN for this node.
+    pub fn asn(&self) -> &ASN {
+        &self.asn
+    }
+
+    /// Returns an iterator over all the interfaces.
+    pub fn interfaces(&self) -> impl Iterator<Item = &BgpInterfaceProperties> {
+        self.interfaces
+            .iter()
+            .map(|property_string| property_string.deref())
+    }
+
+    /// Returns an iterator over all the interfaces (mutable).
+    pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut BgpInterfaceProperties> {
+        self.interfaces
+            .iter_mut()
+            .map(|property_string| property_string.deref_mut())
+    }
+}
+
+impl Validatable for NodeSection<BgpNodeProperties> {
+    type Error = FabricConfigError;
+
+    /// Validate the [`NodeSection<BgpNodeProperties>`].
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum BgpNodeDeletableProperties {
+    Interfaces,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an BGP interface.
+pub struct BgpInterfaceProperties {
+    pub(crate) name: InterfaceName,
+}
+
+impl BgpInterfaceProperties {
+    /// Get the name of the BGP interface.
+    pub fn name(&self) -> &InterfaceName {
+        &self.name
+    }
+
+    /// Set the name of the interface.
+    pub fn set_name(&mut self, name: InterfaceName) {
+        self.name = name
+    }
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index c1ec847..8f918ef 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1,2 +1,3 @@
+pub mod bgp;
 pub mod openfabric;
 pub mod ospf;
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
new file mode 100644
index 0000000..bd434a7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+        bfd 0
+        ip_prefix 10.10.10.0/24
+
+bgp_node: test_pve
+        asn 65001
+        interfaces name=ens18
+        interfaces name=ens19
+        ip 10.10.10.1
+        role internal
+
+bgp_node: test_pve1
+        asn 65002
+        interfaces name=ens19
+        ip 10.10.10.2
+        role internal
+
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
new file mode 100644
index 0000000..f4581fb
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+        bfd 0
+        ip6_prefix fd00:10::/64
+
+bgp_node: test_pve
+        asn 65001
+        interfaces name=ens18
+        interfaces name=ens19
+        ip6 fd00:10::1
+        role internal
+
+bgp_node: test_pve1
+        asn 65002
+        interfaces name=ens19
+        ip6 fd00:10::2
+        role internal
+
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 95b2e62..e1562b1 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -162,3 +162,61 @@ fn openfabric_ipv6_only() {
 
     insta::assert_snapshot!(helper::reference_name!("pve"), output);
 }
+
+#[test]
+fn bgp_default() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+    let mut frr_config = FrrConfig::default();
+
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let mut output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+    frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn bgp_ipv6_only() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+    let mut frr_config = FrrConfig::default();
+
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let mut output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+    frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
new file mode 100644
index 0000000..34aa2cc
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 10.10.10.1
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.1/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
new file mode 100644
index 0000000..a3e6b6d
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
@@ -0,0 +1,27 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 10.10.10.2
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.2/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.2
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
new file mode 100644
index 0000000..f335a4f
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
@@ -0,0 +1,29 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 5.76.46.251
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+  network fd00:10::1/128
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::1
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
new file mode 100644
index 0000000..67628a9
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 6.76.48.142
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+  network fd00:10::2/128
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::2
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH proxmox-perl-rs 1/2] sdn: fabrics: add BGP config generation
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints Hannes Laimer
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 UTC (permalink / raw)
  To: pve-devel

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: Stefan Hanreich <s.hanreich@proxmox.com>
Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 18848c4..8af4f3f 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -31,6 +31,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::protocol::bgp::BgpNode;
     use proxmox_ve_config::sdn::fabric::{FabricConfig, FabricEntry};
 
     use crate::sdn::status::{self, RunningConfig};
@@ -361,6 +362,15 @@ pub mod pve_rs_sdn_fabrics {
                         }
                     }
                 }
+                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);
+                            }
+                        }
+                    }
+                }
             }
         }
 
@@ -453,6 +463,8 @@ pub mod pve_rs_sdn_fabrics {
                 FabricEntry::Openfabric(_) => {
                     daemons.insert("fabricd");
                 }
+                // enabled by the frr package
+                FabricEntry::Bgp(_) => {}
             };
         }
 
@@ -567,6 +579,17 @@ 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")?;
+                        }
+                    }
+                }
             }
         }
 
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support Hannes Laimer
  2026-03-27 15:10 ` [PATCH proxmox-perl-rs 1/2] sdn: fabrics: add BGP config generation Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  2026-03-27 15:10 ` [PATCH pve-network 1/2] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 UTC (permalink / raw)
  To: pve-devel

Add status reporting for BGP fabrics by querying vtysh for BGP
routes, neighbors, and interface state. Neighbors are filtered by
peer-group name matching the fabric ID, and interface status is
derived from the BGP session state.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pve-rs/src/bindings/sdn/fabrics.rs |  77 +++++++++++++++++++++
 pve-rs/src/sdn/status.rs           | 105 ++++++++++++++++++++++++++++-
 2 files changed, 180 insertions(+), 2 deletions(-)

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 8af4f3f..0065f42 100644
--- a/pve-rs/src/bindings/sdn/fabrics.rs
+++ b/pve-rs/src/bindings/sdn/fabrics.rs
@@ -682,6 +682,35 @@ pub mod pve_rs_sdn_fabrics {
 
                 status::get_routes(fabric_id, config, ospf_routes, proxmox_sys::nodename())
             }
+            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())
+            }
         }
     }
 
@@ -740,6 +769,23 @@ pub mod pve_rs_sdn_fabrics {
                 )
                 .map(|v| v.into())
             }
+            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())
+            }
         }
     }
 
@@ -799,6 +845,23 @@ pub mod pve_rs_sdn_fabrics {
                 )
                 .map(|v| v.into())
             }
+            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())
+            }
         }
     }
 
@@ -859,9 +922,23 @@ pub mod pve_rs_sdn_fabrics {
                 .with_context(|| "error parsing ospf routes")?
         };
 
+        let bgp_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip route bgp json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let bgp_routes: proxmox_frr::de::Routes = if bgp_routes_string.is_empty() {
+            proxmox_frr::de::Routes::default()
+        } else {
+            serde_json::from_str(&bgp_routes_string).with_context(|| "error parsing bgp routes")?
+        };
+
         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 e1e3362..1f373d0 100644
--- a/pve-rs/src/sdn/status.rs
+++ b/pve-rs/src/sdn/status.rs
@@ -6,6 +6,7 @@ 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,
 };
@@ -80,12 +81,32 @@ mod openfabric {
     }
 }
 
-/// 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>),
+    Bgp(Vec<bgp::NeighborStatus>),
 }
 
 impl From<Vec<openfabric::NeighborStatus>> for NeighborStatus {
@@ -98,13 +119,19 @@ 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>),
+    Bgp(Vec<bgp::InterfaceStatus>),
 }
 
 impl From<Vec<openfabric::InterfaceStatus>> for InterfaceStatus {
@@ -117,6 +144,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.
 ///
@@ -135,6 +167,8 @@ pub enum Protocol {
     Openfabric,
     /// OSPF
     Ospf,
+    /// BGP
+    Bgp,
 }
 
 /// The status of a fabric.
@@ -173,6 +207,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
@@ -217,6 +253,10 @@ pub fn get_routes(
                 .interfaces()
                 .map(|i| i.name().as_str())
                 .collect(),
+            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());
@@ -408,6 +448,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
@@ -429,6 +525,7 @@ pub fn get_status(
         let (current_protocol, all_routes) = match &node {
             ConfigNode::Openfabric(_) => (Protocol::Openfabric, &routes.openfabric.0),
             ConfigNode::Ospf(_) => (Protocol::Ospf, &routes.ospf.0),
+            ConfigNode::Bgp(_) => (Protocol::Bgp, &routes.bgp.0),
         };
 
         // get interfaces
@@ -443,6 +540,10 @@ pub fn get_status(
                 .interfaces()
                 .map(|i| i.name().as_str())
                 .collect(),
+            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	[flat|nested] 7+ messages in thread

* [PATCH pve-network 1/2] sdn: fabrics: register bgp as a fabric protocol type
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
                   ` (2 preceding siblings ...)
  2026-03-27 15:10 ` [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  2026-03-27 15:10 ` [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay Hannes Laimer
  2026-03-27 15:10 ` [PATCH pve-manager 1/1] ui: sdn: add BGP fabric support Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 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 | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Fabrics.pm b/src/PVE/Network/SDN/Fabrics.pm
index 9be7f021..e576c668 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -46,7 +46,7 @@ PVE::JSONSchema::register_standard_option(
     {
         description => "Type of configuration entry in an SDN Fabric section config",
         type => 'string',
-        enum => ['openfabric', 'ospf'],
+        enum => ['openfabric', 'ospf', 'bgp'],
     },
 );
 
@@ -197,6 +197,22 @@ sub node_properties {
                     description => 'OSPF network interface',
                     optional => 1,
                 },
+                {
+                    type => 'array',
+                    '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,
+                },
             ],
         },
     };
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
                   ` (3 preceding siblings ...)
  2026-03-27 15:10 ` [PATCH pve-network 1/2] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  2026-03-27 15:10 ` [PATCH pve-manager 1/1] ui: sdn: add BGP fabric support Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 UTC (permalink / raw)
  To: pve-devel

When the EVPN controller references a BGP fabric, configure the EVPN
sessions as eBGP. Each node's fabric ASN becomes the router process ASN,
and VTEP peers use 'remote-as external' with 'ebgp-multihop' for
loopback-based peering across the fabric.

EVPN sessions run as eBGP with per-node fabric ASNs rather than using
'local-as' to present a shared overlay ASN for iBGP peering. While iBGP
would be simpler (no autort override needed), peers created as eBGP
retain a hop limit of 1 even after local-as reclassifies them as iBGP,
and 'ebgp-multihop' - used to raise the hop limit for eBGP peers - is
silently ignored for iBGP peers. With other fabric types all nodes share
one ASN, so peers are created as iBGP from the start with hop limit 255.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 40 ++++++++-
 .../bgp_fabric/expected_controller_config     | 73 ++++++++++++++++
 .../evpn/bgp_fabric/expected_sdn_interfaces   | 56 ++++++++++++
 src/test/zones/evpn/bgp_fabric/interfaces     |  6 ++
 src/test/zones/evpn/bgp_fabric/sdn_config     | 85 +++++++++++++++++++
 5 files changed, 258 insertions(+), 2 deletions(-)
 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/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index d2825f57..186b4c39 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -65,6 +65,9 @@ sub generate_frr_config {
     my $bgp_controller = find_bgp_controller($local_node, $controller_cfg);
     my $isis_controller = find_isis_controller($local_node, $controller_cfg);
 
+    my $fabric_is_bgp = 0;
+    my $fabric_asn = undef;
+
     if ($plugin_config->{'fabric'}) {
         my $config = PVE::Network::SDN::Fabrics::config(1);
 
@@ -99,6 +102,12 @@ sub generate_frr_config {
         $ifaceip = $current_node->{ip};
         $routerid = $current_node->{ip};
 
+        # Detect BGP fabric underlay - needs special ASN handling
+        if ($fabric->{protocol} eq 'bgp') {
+            $fabric_is_bgp = 1;
+            $fabric_asn = int($current_node->{asn});
+        }
+
     } elsif ($plugin_config->{'peers'}) {
         @peers = PVE::Tools::split_list($plugin_config->{'peers'});
 
@@ -116,7 +125,14 @@ sub generate_frr_config {
         return;
     }
 
-    if ($bgp_controller) {
+    if ($fabric_is_bgp && $fabric_asn) {
+        # BGP fabric underlay: EVPN sessions are eBGP between per-node ASNs.
+        # The router runs under the fabric node ASN; VTEP peers are external.
+        $ebgp = 1;
+        $asn = $fabric_asn;
+        $autortas = $plugin_config->{asn}
+            if int($plugin_config->{asn}) != $fabric_asn;
+    } elsif ($bgp_controller) {
         $ebgp = 1 if $plugin_config->{'asn'} ne $bgp_controller->{asn};
         $asn = int($bgp_controller->{asn}) if $bgp_controller->{asn};
         $autortas = $plugin_config->{'asn'} if $ebgp;
@@ -138,6 +154,12 @@ sub generate_frr_config {
         $bgp_router->{address_families} = {};
     }
 
+    # eBGP EVPN over BGP fabric needs multipath-relax for ECMP
+    if ($fabric_is_bgp) {
+        $bgp_router->{bestpath_as_path_multipath_relax} = 1;
+        $bgp_router->{disable_ebgp_connected_route_check} = 1;
+    }
+
     # Build VTEP neighbor group
     my @vtep_ips = grep { $_ ne $ifaceip } @peers;
 
@@ -148,6 +170,7 @@ sub generate_frr_config {
         ips => \@vtep_ips,
         interfaces => [],
     };
+
     $neighbor_group->{ebgp_multihop} = 10 if $ebgp && $loopback;
     $neighbor_group->{update_source} = $loopback if $loopback;
 
@@ -201,6 +224,9 @@ sub generate_zone_frr_config {
     my $bgprouter = find_bgp_controller($local_node, $controller_cfg);
     my $isisrouter = find_isis_controller($local_node, $controller_cfg);
 
+    my $fabric_is_bgp = 0;
+    my $fabric_asn = undef;
+
     if ($controller->{fabric}) {
         my $config = PVE::Network::SDN::Fabrics::config(1);
 
@@ -234,6 +260,11 @@ sub generate_zone_frr_config {
         $ifaceip = $current_node->{ip};
         $routerid = $current_node->{ip};
 
+        if ($fabric->{protocol} eq 'bgp') {
+            $fabric_is_bgp = 1;
+            $fabric_asn = int($current_node->{asn});
+        }
+
     } elsif ($controller->{peers}) {
         @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'};
 
@@ -252,7 +283,12 @@ sub generate_zone_frr_config {
         return;
     }
 
-    if ($bgprouter) {
+    if ($fabric_is_bgp && $fabric_asn) {
+        $ebgp = 1;
+        $asn = $fabric_asn;
+        $autortas = $controller->{asn}
+            if int($controller->{asn}) != $fabric_asn;
+    } elsif ($bgprouter) {
         $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn};
         $asn = $bgprouter->{asn} if $bgprouter->{asn};
         $autortas = $controller->{'asn'} if $ebgp;
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 00000000..56d35e02
--- /dev/null
+++ b/src/test/zones/evpn/bgp_fabric/expected_controller_config
@@ -0,0 +1,73 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_evpn
+ vni 100
+exit-vrf
+!
+router bgp 65100
+ 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
+ bgp disable-ebgp-connected-route-check
+ bgp bestpath as-path multipath-relax
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as external
+ neighbor VTEP bfd
+ neighbor VTEP ebgp-multihop 10
+ 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 ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.1/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+  neighbor VTEP activate
+  neighbor VTEP route-map MAP_VTEP_IN in
+  neighbor VTEP route-map MAP_VTEP_OUT out
+  advertise-all-vni
+  autort as 65000
+ exit-address-family
+exit
+!
+router bgp 65100 vrf vrf_evpn
+ bgp router-id 10.10.10.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+ !
+ address-family l2vpn evpn
+  route-target import 65000:100
+  route-target export 65000:100
+ exit-address-family
+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
+!
+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 00000000..fd1429d8
--- /dev/null
+++ b/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces
@@ -0,0 +1,56 @@
+#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
+
+auto ens19
+iface ens19 inet manual
+	ip-forward 1
diff --git a/src/test/zones/evpn/bgp_fabric/interfaces b/src/test/zones/evpn/bgp_fabric/interfaces
new file mode 100644
index 00000000..08874137
--- /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 00000000..080f1e98
--- /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	[flat|nested] 7+ messages in thread

* [PATCH pve-manager 1/1] ui: sdn: add BGP fabric support
  2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
                   ` (4 preceding siblings ...)
  2026-03-27 15:10 ` [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay Hannes Laimer
@ 2026-03-27 15:10 ` Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-03-27 15:10 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/NodeEdit.js          |  1 +
 www/manager6/sdn/fabrics/bgp/FabricEdit.js    | 52 +++++++++++++++++++
 .../sdn/fabrics/bgp/InterfacePanel.js         | 15 ++++++
 www/manager6/sdn/fabrics/bgp/NodeEdit.js      | 32 ++++++++++++
 6 files changed, 115 insertions(+)
 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 da602523..d8a423e0 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -334,6 +334,9 @@ JSSRC= 							\
 	sdn/fabrics/ospf/InterfacePanel.js		\
 	sdn/fabrics/ospf/NodeEdit.js			\
 	sdn/fabrics/ospf/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 093a70f3..6b067247 100644
--- a/www/manager6/sdn/FabricsView.js
+++ b/www/manager6/sdn/FabricsView.js
@@ -33,6 +33,7 @@ Ext.define('PVE.sdn.Fabric.View', {
                     const PROTOCOL_DISPLAY_NAMES = {
                         openfabric: 'OpenFabric',
                         ospf: 'OSPF',
+                        bgp: 'BGP',
                     };
                     const displayValue = PROTOCOL_DISPLAY_NAMES[value];
                     if (rec.data.state === undefined || rec.data.state === null) {
@@ -194,6 +195,10 @@ Ext.define('PVE.sdn.Fabric.View', {
                             text: 'OSPF',
                             handler: 'addOspf',
                         },
+                        {
+                            text: 'BGP',
+                            handler: 'addBgp',
+                        },
                     ],
                 },
                 addNodeButton,
@@ -272,6 +277,7 @@ Ext.define('PVE.sdn.Fabric.View', {
             const FABRIC_PANELS = {
                 openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
                 ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
+                bgp: 'PVE.sdn.Fabric.Bgp.Fabric.Edit',
             };
 
             return FABRIC_PANELS[protocol];
@@ -281,6 +287,7 @@ Ext.define('PVE.sdn.Fabric.View', {
             const NODE_PANELS = {
                 openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
                 ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit',
+                bgp: 'PVE.sdn.Fabric.Bgp.Node.Edit',
             };
 
             return NODE_PANELS[protocol];
@@ -296,6 +303,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/NodeEdit.js b/www/manager6/sdn/fabrics/NodeEdit.js
index 161917cc..2e88b684 100644
--- a/www/manager6/sdn/fabrics/NodeEdit.js
+++ b/www/manager6/sdn/fabrics/NodeEdit.js
@@ -200,6 +200,7 @@ Ext.define('PVE.sdn.Fabric.Node.Edit', {
         const INTERFACE_PANELS = {
             openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
             ospf: 'PVE.sdn.Fabric.Ospf.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..1c37e533
--- /dev/null
+++ b/www/manager6/sdn/fabrics/bgp/FabricEdit.js
@@ -0,0 +1,52 @@
+Ext.define('PVE.sdn.Fabric.Bgp.Fabric.Edit', {
+    extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+    subject: 'BGP',
+
+    viewModel: {
+        data: {
+            showIpv6ForwardingHint: false,
+        },
+    },
+
+    extraRequestParams: {
+        protocol: 'bgp',
+    },
+
+    additionalItems: [
+        {
+            xtype: 'displayfield',
+            value: 'To make IPv6 fabrics work, enable global IPv6 forwarding on all nodes.',
+            bind: {
+                hidden: '{!showIpv6ForwardingHint}',
+            },
+            userCls: 'pmx-hint',
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('IPv6 Prefix'),
+            labelWidth: 120,
+            name: 'ip6_prefix',
+            allowBlank: true,
+            skipEmptyText: true,
+            cbind: {
+                disabled: '{!isCreate}',
+                deleteEmpty: '{!isCreate}',
+            },
+            listeners: {
+                change: function (textbox, value) {
+                    let vm = textbox.up('window').getViewModel();
+                    vm.set('showIpv6ForwardingHint', !!value);
+                },
+            },
+        },
+        {
+            xtype: 'proxmoxcheckbox',
+            fieldLabel: gettext('BFD'),
+            labelWidth: 120,
+            name: 'bfd',
+            uncheckedValue: 0,
+            defaultValue: 0,
+        },
+    ],
+});
diff --git a/www/manager6/sdn/fabrics/bgp/InterfacePanel.js b/www/manager6/sdn/fabrics/bgp/InterfacePanel.js
new file mode 100644
index 00000000..81f3e849
--- /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',
+
+    // 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..4a172872
--- /dev/null
+++ b/www/manager6/sdn/fabrics/bgp/NodeEdit.js
@@ -0,0 +1,32 @@
+Ext.define('PVE.sdn.Fabric.Bgp.Node.Edit', {
+    extend: 'PVE.sdn.Fabric.Node.Edit',
+    protocol: 'bgp',
+
+    extraRequestParams: {
+        protocol: 'bgp',
+        role: 'internal',
+    },
+
+    additionalItems: [
+        {
+            xtype: 'proxmoxtextfield',
+            fieldLabel: gettext('IPv6'),
+            labelWidth: 120,
+            name: 'ip6',
+            allowBlank: true,
+            skipEmptyText: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            fieldLabel: gettext('ASN'),
+            labelWidth: 120,
+            name: 'asn',
+            minValue: 1,
+            maxValue: 4294967295,
+            allowBlank: false,
+        },
+    ],
+});
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-03-27 15:11 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-27 15:10 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} 0/6] sdn: add BGP fabric Hannes Laimer
2026-03-27 15:10 ` [PATCH proxmox-ve-rs 1/1] sdn: fabric: add BGP protocol support Hannes Laimer
2026-03-27 15:10 ` [PATCH proxmox-perl-rs 1/2] sdn: fabrics: add BGP config generation Hannes Laimer
2026-03-27 15:10 ` [PATCH proxmox-perl-rs 2/2] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-network 1/2] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay Hannes Laimer
2026-03-27 15:10 ` [PATCH pve-manager 1/1] ui: sdn: add BGP fabric support Hannes Laimer

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal