all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric
@ 2026-04-15 11:11 Hannes Laimer
  2026-04-15 11:11 ` [PATCH proxmox-ve-rs v2 1/7] sdn: fabric: add BGP protocol support Hannes Laimer
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Hannes Laimer @ 2026-04-15 11:11 UTC (permalink / raw)
  To: pve-devel

This patch series adds BGP as a third fabric protocol alongside OpenFabric
and OSPF. It targets eBGP unnumbered underlays where each node has a unique
ASN and peers over physical interfaces without IP assignment on fabric links.

## Dependencies

This series depends on the evpn-rework and route-maps series.

## eBGP underlay

Each node gets its own ASN (e.g. 65001, 65002, 65003) and peers with its
neighbors using 'remote-as external' on unnumbered interfaces. The fabric
peer-group is named after the fabric ID and uses BFD when enabled.

## EVPN overlay

When the EVPN controller references a BGP fabric, VTEP sessions are iBGP,
consistent with how EVPN operates on OSPF and OpenFabric fabrics. The
per-node ASN is applied via 'local-as' on the underlay neighbor group.

## Single BGP process

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.

## IPv6-only support

For nodes with only an IPv6 address, the BGP router-id (which must be a
32-bit value) is derived from the IPv6 address using FNV-1a hashing.

### Testing results for hash collisions
Scattered /64           n=1000       unique=1000       collisions=0      worst=1
Scattered /64           n=10000      unique=10000      collisions=0      worst=1
Scattered /64           n=100000     unique=99997      collisions=3      worst=2
Sequential /64          n=1000       unique=1000       collisions=0      worst=1
Sequential /64          n=10000      unique=10000      collisions=0      worst=1
Sequential /64          n=100000     unique=100000     collisions=0      worst=1
Spaced /64 (step 256)   n=1000       unique=1000       collisions=0      worst=1
Spaced /64 (step 256)   n=10000      unique=10000      collisions=0      worst=1
Spaced /64 (step 256)   n=100000     unique=100000     collisions=0      worst=1
Sparse multi-/48        n=1000       unique=1000       collisions=0      worst=1
Sparse multi-/48        n=10000      unique=10000      collisions=0      worst=1
Sparse multi-/48        n=100000     unique=100000     collisions=0      worst=1

Only the random assignment in a /64 prefix caused a tiny amount of collisions,
and having 100k routers with randomly assigned IPs is not really typical. So
FNV-1a does seem like a good choice here. (generally I'm open to alternative
approaches for getting router-ids on nodes with no ipv4 ips)


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


v2, thanks @Gabriel and @Stefan for the (off-list) feedback on v1!:
 - switched EVPN overlay from eBGP to iBGP
 - rebased onto Stefan's evpn[1]/route-maps[2] series
 - made LocalAsSettings fields pub (needed for Rust-side construction)
 - added router-id collision validation for IPv6-only nodes
 - added docs section

[1] https://lore.proxmox.com/pve-devel/20260414163315.419384-1-s.hanreich@proxmox.com/
[2] https://lore.proxmox.com/pve-devel/20260401143957.386809-1-s.hanreich@proxmox.com/


proxmox-ve-rs:

Stefan Hanreich (1):
  sdn: fabric: add BGP protocol support

 proxmox-frr/src/ser/bgp.rs                    |  85 +++++-
 proxmox-ve-config/src/sdn/fabric/frr.rs       | 254 +++++++++++++++-
 proxmox-ve-config/src/sdn/fabric/mod.rs       | 111 +++++++
 .../src/sdn/fabric/section_config/fabric.rs   |  22 ++
 .../src/sdn/fabric/section_config/mod.rs      |  19 ++
 .../src/sdn/fabric/section_config/node.rs     |  21 ++
 .../sdn/fabric/section_config/protocol/bgp.rs | 286 ++++++++++++++++++
 .../sdn/fabric/section_config/protocol/mod.rs |   1 +
 .../tests/fabric/cfg/bgp_default/fabrics.cfg  |  17 ++
 .../fabric/cfg/bgp_ipv6_only/fabrics.cfg      |  17 ++
 proxmox-ve-config/tests/fabric/main.rs        | 119 +++++++-
 .../snapshots/fabric__bgp_default_pve.snap    |  28 ++
 .../snapshots/fabric__bgp_default_pve1.snap   |  27 ++
 .../snapshots/fabric__bgp_ipv6_only_pve.snap  |  29 ++
 .../snapshots/fabric__bgp_ipv6_only_pve1.snap |  28 ++
 .../fabric__bgp_merge_with_evpn_pve.snap      |  34 +++
 16 files changed, 1089 insertions(+), 9 deletions(-)
 create mode 100644 proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
 create mode 100644 proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap


proxmox-perl-rs:

Hannes Laimer (1):
  sdn: fabrics: add BGP status endpoints

Stefan Hanreich (1):
  sdn: fabrics: add BGP config generation

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


pve-network:

Hannes Laimer (2):
  sdn: fabrics: register bgp as a fabric protocol type
  test: evpn: add integration test for EVPN over BGP fabric

 src/PVE/Network/SDN/Fabrics.pm                | 18 +++-
 .../bgp_fabric/expected_controller_config     | 65 ++++++++++++++
 .../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, 229 insertions(+), 1 deletion(-)
 create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config
 create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces
 create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces
 create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config


pve-manager:

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

 www/manager6/Makefile                         |  3 ++
 www/manager6/sdn/FabricsView.js               | 12 +++++
 www/manager6/sdn/fabrics/NodeEdit.js          |  1 +
 www/manager6/sdn/fabrics/bgp/FabricEdit.js    | 53 +++++++++++++++++++
 .../sdn/fabrics/bgp/InterfacePanel.js         | 13 +++++
 www/manager6/sdn/fabrics/bgp/NodeEdit.js      | 32 +++++++++++
 6 files changed, 114 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


pve-docs:

Hannes Laimer (1):
  sdn: add bgp fabric section

 pvesdn.adoc | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)


Summary over all repositories:
  30 files changed, 1715 insertions(+), 12 deletions(-)

-- 
Generated by murpp 0.11.0




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

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

From: Stefan Hanreich <s.hanreich@proxmox.com>

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

Unlike OSPF and OpenFabric, BGP does not have its own FRR daemon -
the fabric config needs to coexist with EVPN in a single 'router bgp'
block. To handle this, the fabric merges into an existing router
rather than replacing it, using local-as to present the per-node ASN
to underlay peers when the router already runs under the EVPN ASN.

For IPv6-only nodes, the BGP router-id is derived from the IPv6
address using FNV-1a, since router-id must be a 32-bit value.

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

diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs
index 0bf4a1d..5d454fa 100644
--- a/proxmox-frr/src/ser/bgp.rs
+++ b/proxmox-frr/src/ser/bgp.rs
@@ -43,9 +43,9 @@ pub enum LocalAsFlags {
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase")]
 pub struct LocalAsSettings {
-    asn: u32,
+    pub asn: u32,
     #[serde(default)]
-    mode: Option<LocalAsFlags>,
+    pub mode: Option<LocalAsFlags>,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@@ -159,9 +159,84 @@ pub struct CommonAddressFamilyOptions {
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)]
 pub struct AddressFamilies {
-    ipv4_unicast: Option<Ipv4UnicastAF>,
-    ipv6_unicast: Option<Ipv6UnicastAF>,
-    l2vpn_evpn: Option<L2vpnEvpnAF>,
+    pub ipv4_unicast: Option<Ipv4UnicastAF>,
+    pub ipv6_unicast: Option<Ipv6UnicastAF>,
+    pub l2vpn_evpn: Option<L2vpnEvpnAF>,
+}
+
+impl AddressFamilies {
+    /// Merge another [`AddressFamilies`] into this one.
+    ///
+    /// For each address family: if `self` already has it, extend its neighbors, networks, and
+    /// redistribute lists. If `self` doesn't have it, take it from `other`.
+    pub fn merge(&mut self, other: AddressFamilies) {
+        match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) {
+            (Some(existing), Some(incoming)) => {
+                existing
+                    .common_options
+                    .neighbors
+                    .extend(incoming.common_options.neighbors);
+                existing
+                    .common_options
+                    .import_vrf
+                    .extend(incoming.common_options.import_vrf);
+                existing
+                    .common_options
+                    .custom_frr_config
+                    .extend(incoming.common_options.custom_frr_config);
+                existing.networks.extend(incoming.networks);
+                existing.redistribute.extend(incoming.redistribute);
+            }
+            (None, Some(incoming)) => {
+                self.ipv4_unicast = Some(incoming);
+            }
+            _ => {}
+        }
+
+        match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) {
+            (Some(existing), Some(incoming)) => {
+                existing
+                    .common_options
+                    .neighbors
+                    .extend(incoming.common_options.neighbors);
+                existing
+                    .common_options
+                    .import_vrf
+                    .extend(incoming.common_options.import_vrf);
+                existing
+                    .common_options
+                    .custom_frr_config
+                    .extend(incoming.common_options.custom_frr_config);
+                existing.networks.extend(incoming.networks);
+                existing.redistribute.extend(incoming.redistribute);
+            }
+            (None, Some(incoming)) => {
+                self.ipv6_unicast = Some(incoming);
+            }
+            _ => {}
+        }
+
+        // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this)
+        if self.l2vpn_evpn.is_none() {
+            self.l2vpn_evpn = other.l2vpn_evpn;
+        }
+    }
+}
+
+impl BgpRouter {
+    /// Merge a fabric-generated [`BgpRouter`] into an existing one.
+    ///
+    /// Appends the fabric's neighbor groups and merges address families. Keeps the existing
+    /// router's ASN, router-id, and other top-level settings. The caller is responsible for
+    /// setting `local_as` on the fabric's neighbor group if the ASNs differ.
+    pub fn merge_fabric(&mut self, other: BgpRouter) {
+        self.neighbor_groups.extend(other.neighbor_groups);
+        self.address_families.merge(other.address_families);
+
+        if self.default_ipv4_unicast.is_none() {
+            self.default_ipv4_unicast = other.default_ipv4_unicast;
+        }
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
index b816ef6..373df80 100644
--- a/proxmox-ve-config/src/sdn/fabric/frr.rs
+++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
@@ -2,19 +2,28 @@ use std::net::{IpAddr, Ipv4Addr};
 
 use tracing;
 
+use proxmox_frr::ser::bgp::{
+    AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF,
+    Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs,
+    RedistributeProtocol, Redistribution,
+};
 use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
 use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRouter};
 use proxmox_frr::ser::route_map::{
-    AccessAction, AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
+    AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
 };
+use proxmox_frr::ser::AccessAction;
 use proxmox_frr::ser::{
-    self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap,
+    self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, IpProtocolRouteMap, VrfName,
 };
-use proxmox_network_types::ip_address::Cidr;
+use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
 use proxmox_sdn_types::net::Net;
 
 use crate::common::valid::Valid;
+
+use crate::sdn::fabric::section_config::protocol::bgp::{router_id_from_ipv6, BgpNode};
 use crate::sdn::fabric::section_config::protocol::{
+    bgp::BgpRedistributionSource,
     openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
     ospf::OspfInterfaceProperties,
 };
@@ -277,6 +286,214 @@ pub fn build_fabric(
                     );
                 }
             }
+            FabricEntry::Bgp(bgp_entry) => {
+                let Ok(node) = bgp_entry.node_section(&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 mut router = BgpRouter {
+                    asn: local_asn,
+                    router_id,
+                    neighbor_groups: vec![neighbor_group],
+                    address_families,
+                    coalesce_time: Default::default(),
+                    default_ipv4_unicast: Some(false),
+                    hard_administrative_reset: Default::default(),
+                    graceful_restart_notification: Default::default(),
+                    disable_ebgp_connected_route_check: Default::default(),
+                    bestpath_as_path_multipath_relax: Default::default(),
+                    custom_frr_config: Default::default(),
+                };
+
+                if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) {
+                    // If the existing router uses a different ASN (e.g. the
+                    // EVPN ASN), set local-as on the fabric neighbor group so
+                    // the underlay peers see the correct per-node ASN.
+                    if existing.asn != local_asn {
+                        if let Some(ng) = router.neighbor_groups.first_mut() {
+                            ng.local_as = Some(LocalAsSettings {
+                                asn: local_asn,
+                                mode: Some(LocalAsFlags::ReplaceAs),
+                            });
+                        }
+                    }
+                    existing.merge_fabric(router);
+                } else {
+                    frr_config.bgp.vrf_router.insert(VrfName::Default, router);
+                }
+
+                // Create access-list and route-map for source address selection,
+                // so the kernel uses the loopback IP as source for fabric-learned routes.
+                if let Some(ipv4_prefix) = fabric.ip_prefix() {
+                    let access_list_name =
+                        AccessListName::new(format!("pve_bgp_{}_ips", fabric_id));
+
+                    let rule = ser::route_map::AccessListRule {
+                        action: AccessAction::Permit,
+                        network: Cidr::from(ipv4_prefix),
+                        is_ipv6: false,
+                        seq: None,
+                    };
+
+                    frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+                    let (routemap_name, routemap_rule) = build_bgp_routemap(
+                        fabric_id,
+                        IpAddr::from(node.ip().expect("node must have an ipv4 address")),
+                        routemap_seq,
+                    );
+
+                    routemap_seq += 10;
+
+                    if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+                        routemap.push(routemap_rule)
+                    } else {
+                        frr_config
+                            .routemaps
+                            .insert(routemap_name.clone(), vec![routemap_rule]);
+                    }
+
+                    if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+                    {
+                        routemap.v4 = Some(routemap_name);
+                    } else {
+                        frr_config.protocol_routemaps.insert(
+                            FrrProtocol::Bgp,
+                            IpProtocolRouteMap {
+                                v4: Some(routemap_name),
+                                v6: None,
+                            },
+                        );
+                    }
+                }
+
+                if let Some(ipv6_prefix) = fabric.ip6_prefix() {
+                    let access_list_name =
+                        AccessListName::new(format!("pve_bgp_{}_ip6s", fabric_id));
+
+                    let rule = ser::route_map::AccessListRule {
+                        action: AccessAction::Permit,
+                        network: Cidr::from(ipv6_prefix),
+                        is_ipv6: true,
+                        seq: None,
+                    };
+
+                    frr_config.access_lists.insert(access_list_name, vec![rule]);
+
+                    let (routemap_name, routemap_rule) = build_bgp_routemap(
+                        fabric_id,
+                        IpAddr::from(node.ip6().expect("node must have an ipv6 address")),
+                        routemap_seq,
+                    );
+
+                    routemap_seq += 10;
+
+                    if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
+                        routemap.push(routemap_rule)
+                    } else {
+                        frr_config
+                            .routemaps
+                            .insert(routemap_name.clone(), vec![routemap_rule]);
+                    }
+
+                    if let Some(routemap) = frr_config.protocol_routemaps.get_mut(&FrrProtocol::Bgp)
+                    {
+                        routemap.v6 = Some(routemap_name);
+                    } else {
+                        frr_config.protocol_routemaps.insert(
+                            FrrProtocol::Bgp,
+                            IpProtocolRouteMap {
+                                v4: None,
+                                v6: Some(routemap_name),
+                            },
+                        );
+                    }
+                }
+            }
         }
     }
 
@@ -416,6 +633,37 @@ fn build_openfabric_routemap(
     )
 }
 
+/// Helper that builds a RouteMap for the BGP protocol.
+fn build_bgp_routemap(
+    fabric_id: &FabricId,
+    router_ip: IpAddr,
+    seq: u16,
+) -> (RouteMapName, RouteMapEntry) {
+    let routemap_name = match router_ip {
+        IpAddr::V4(_) => RouteMapName::new("pve_bgp".to_owned()),
+        IpAddr::V6(_) => RouteMapName::new("pve_bgp6".to_owned()),
+    };
+    (
+        routemap_name,
+        RouteMapEntry {
+            seq,
+            action: AccessAction::Permit,
+            matches: vec![match router_ip {
+                IpAddr::V4(_) => RouteMapMatch::IpAddressAccessList(AccessListName::new(format!(
+                    "pve_bgp_{fabric_id}_ips"
+                ))),
+                IpAddr::V6(_) => RouteMapMatch::Ip6AddressAccessList(AccessListName::new(format!(
+                    "pve_bgp_{fabric_id}_ip6s"
+                ))),
+            }],
+            sets: vec![RouteMapSet::Src(router_ip)],
+            custom_frr_config: Vec::new(),
+            call: None,
+            exit_action: None,
+        },
+    )
+}
+
 /// Helper that builds a RouteMap for the OSPF protocol.
 fn build_ospf_dummy_routemap(
     fabric_id: &FabricId,
diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
index 677a309..f4c134b 100644
--- a/proxmox-ve-config/src/sdn/fabric/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
@@ -20,6 +20,10 @@ use crate::sdn::fabric::section_config::node::{
     api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
     Node, NodeId, NodeSection,
 };
+use crate::sdn::fabric::section_config::protocol::bgp::{
+    router_id_from_ipv6, BgpNode, BgpNodeDeletableProperties, BgpNodePropertiesUpdater,
+    BgpProperties,
+};
 use crate::sdn::fabric::section_config::protocol::openfabric::{
     OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
     OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater,
@@ -64,6 +68,10 @@ pub enum FabricConfigError {
     // this is technically possible, but we don't allow it
     #[error("duplicate OSPF area")]
     DuplicateOspfArea,
+    #[error("ASN {0} is already used by another BGP fabric node")]
+    DuplicateBgpAsn(u32),
+    #[error("BGP router-id collision: nodes have different IPv6 addresses but the same derived router-id {0}")]
+    DuplicateBgpRouterId(std::net::Ipv4Addr),
     #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")]
     OverlappingIp4Prefix(String, String, String, String),
     #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")]
@@ -162,6 +170,33 @@ where
     }
 }
 
+impl Entry<BgpProperties, BgpNode> {
+    /// Get the BGP fabric config.
+    ///
+    /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+    /// so it is guaranteed that a [`FabricSection<BgpProperties>`] is returned.
+    pub fn fabric_section(&self) -> &FabricSection<BgpProperties> {
+        if let Fabric::Bgp(section) = &self.fabric {
+            return section;
+        }
+
+        unreachable!();
+    }
+
+    /// Get the BGP node config for the given node_id.
+    ///
+    /// This method is implemented for [`Entry<BgpProperties, BgpNode>`],
+    /// so it is guaranteed that a [`NodeSection<BgpNode>`] is returned.
+    /// An error is returned if the node is not found.
+    pub fn node_section(&self, id: &NodeId) -> Result<&NodeSection<BgpNode>, FabricConfigError> {
+        if let Node::Bgp(section) = self.get_node(id)? {
+            return Ok(section);
+        }
+
+        unreachable!();
+    }
+}
+
 impl Entry<OpenfabricProperties, OpenfabricNodeProperties> {
     /// Get the OpenFabric fabric config.
     ///
@@ -230,6 +265,7 @@ impl Entry<OspfProperties, OspfNodeProperties> {
 pub enum FabricEntry {
     Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
     Ospf(Entry<OspfProperties, OspfNodeProperties>),
+    Bgp(Entry<BgpProperties, BgpNode>),
 }
 
 impl FabricEntry {
@@ -241,6 +277,7 @@ impl FabricEntry {
                 entry.add_node(node_section)
             }
             (FabricEntry::Ospf(entry), Node::Ospf(node_section)) => entry.add_node(node_section),
+            (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section),
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -251,6 +288,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node(id),
             FabricEntry::Ospf(entry) => entry.get_node(id),
+            FabricEntry::Bgp(entry) => entry.get_node(id),
         }
     }
 
@@ -260,6 +298,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
             FabricEntry::Ospf(entry) => entry.get_node_mut(id),
+            FabricEntry::Bgp(entry) => entry.get_node_mut(id),
         }
     }
 
@@ -339,6 +378,46 @@ impl FabricEntry {
 
                 Ok(())
             }
+            (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => {
+                let BgpNode::Internal(ref mut props) = node_section.properties else {
+                    return Err(FabricConfigError::ProtocolMismatch);
+                };
+
+                let NodeDataUpdater::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> {
+                    ip,
+                    ip6,
+                    properties: BgpNodePropertiesUpdater { asn, interfaces },
+                    delete,
+                } = updater;
+
+                if let Some(ip) = ip {
+                    node_section.ip = Some(ip);
+                }
+
+                if let Some(ip) = ip6 {
+                    node_section.ip6 = Some(ip);
+                }
+
+                if let Some(asn) = asn {
+                    props.asn = asn;
+                }
+
+                if let Some(interfaces) = interfaces {
+                    props.interfaces = interfaces;
+                }
+
+                for property in delete {
+                    match property {
+                        NodeDeletableProperties::Ip => node_section.ip = None,
+                        NodeDeletableProperties::Ip6 => node_section.ip6 = None,
+                        NodeDeletableProperties::Protocol(
+                            BgpNodeDeletableProperties::Interfaces,
+                        ) => props.interfaces = Vec::new(),
+                    }
+                }
+
+                Ok(())
+            }
             _ => Err(FabricConfigError::ProtocolMismatch),
         }
     }
@@ -348,6 +427,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.nodes.iter(),
             FabricEntry::Ospf(entry) => entry.nodes.iter(),
+            FabricEntry::Bgp(entry) => entry.nodes.iter(),
         }
     }
 
@@ -356,6 +436,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.delete_node(id),
             FabricEntry::Ospf(entry) => entry.delete_node(id),
+            FabricEntry::Bgp(entry) => entry.delete_node(id),
         }
     }
 
@@ -365,6 +446,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => entry.into_pair(),
             FabricEntry::Ospf(entry) => entry.into_pair(),
+            FabricEntry::Bgp(entry) => entry.into_pair(),
         }
     }
 
@@ -373,6 +455,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &entry.fabric,
             FabricEntry::Ospf(entry) => &entry.fabric,
+            FabricEntry::Bgp(entry) => &entry.fabric,
         }
     }
 
@@ -381,6 +464,7 @@ impl FabricEntry {
         match self {
             FabricEntry::Openfabric(entry) => &mut entry.fabric,
             FabricEntry::Ospf(entry) => &mut entry.fabric,
+            FabricEntry::Bgp(entry) => &mut entry.fabric,
         }
     }
 }
@@ -392,6 +476,7 @@ impl From<Fabric> for FabricEntry {
                 FabricEntry::Openfabric(Entry::new(fabric_section))
             }
             Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
+            Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)),
         }
     }
 }
@@ -513,6 +598,8 @@ impl Validatable for FabricConfig {
     fn validate(&self) -> Result<(), FabricConfigError> {
         let mut node_interfaces = HashSet::new();
         let mut ospf_area = HashSet::new();
+        let mut bgp_asns = HashSet::new();
+        let mut bgp_router_ids = HashSet::new();
 
         // Check for overlapping IP prefixes across fabrics
         let fabrics: Vec<_> = self.fabrics.values().map(|f| f.fabric()).collect();
@@ -573,6 +660,30 @@ impl Validatable for FabricConfig {
                             return Err(FabricConfigError::DuplicateInterface);
                         }
                     }
+                    Node::Bgp(node_section) => {
+                        if let BgpNode::Internal(props) = node_section.properties() {
+                            if !props.interfaces().all(|interface| {
+                                node_interfaces.insert((node_id, interface.name().as_str()))
+                            }) {
+                                return Err(FabricConfigError::DuplicateInterface);
+                            }
+
+                            let asn = props.asn().as_u32();
+                            if !bgp_asns.insert(asn) {
+                                return Err(FabricConfigError::DuplicateBgpAsn(asn));
+                            }
+
+                            // IPv6-only nodes derive router-id via hash, check for collisions
+                            if node_section.ip().is_none() {
+                                if let Some(ipv6) = node_section.ip6() {
+                                    let rid = router_id_from_ipv6(&ipv6);
+                                    if !bgp_router_ids.insert(rid) {
+                                        return Err(FabricConfigError::DuplicateBgpRouterId(rid));
+                                    }
+                                }
+                            }
+                        }
+                    }
                 }
             }
 
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
index 38911a6..9f41eae 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
@@ -8,6 +8,9 @@ use proxmox_schema::{
 };
 
 use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{
+    BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater,
+};
 use crate::sdn::fabric::section_config::protocol::openfabric::{
     OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
 };
@@ -139,6 +142,10 @@ impl UpdaterType for FabricSection<OspfProperties> {
     type Updater = FabricSectionUpdater<OspfPropertiesUpdater, OspfDeletableProperties>;
 }
 
+impl UpdaterType for FabricSection<BgpProperties> {
+    type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>;
+}
+
 /// Enum containing all types of fabrics.
 ///
 /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
@@ -159,6 +166,7 @@ impl UpdaterType for FabricSection<OspfProperties> {
 pub enum Fabric {
     Openfabric(FabricSection<OpenfabricProperties>),
     Ospf(FabricSection<OspfProperties>),
+    Bgp(FabricSection<BgpProperties>),
 }
 
 impl UpdaterType for Fabric {
@@ -173,6 +181,7 @@ impl Fabric {
         match self {
             Self::Openfabric(fabric_section) => fabric_section.id(),
             Self::Ospf(fabric_section) => fabric_section.id(),
+            Self::Bgp(fabric_section) => fabric_section.id(),
         }
     }
 
@@ -183,6 +192,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
+            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(),
         }
     }
 
@@ -193,6 +203,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
+            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
         }
     }
 
@@ -203,6 +214,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
+            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(),
         }
     }
 
@@ -213,6 +225,7 @@ impl Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
             Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
+            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
         }
     }
 }
@@ -225,6 +238,7 @@ impl Validatable for Fabric {
         match self {
             Fabric::Openfabric(fabric_section) => fabric_section.validate(),
             Fabric::Ospf(fabric_section) => fabric_section.validate(),
+            Fabric::Bgp(fabric_section) => fabric_section.validate(),
         }
     }
 }
@@ -241,12 +255,19 @@ impl From<FabricSection<OspfProperties>> for Fabric {
     }
 }
 
+impl From<FabricSection<BgpProperties>> for Fabric {
+    fn from(section: FabricSection<BgpProperties>) -> Self {
+        Fabric::Bgp(section)
+    }
+}
+
 /// Enum containing all updater types for fabrics
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "snake_case", tag = "protocol")]
 pub enum FabricUpdater {
     Openfabric(<FabricSection<OpenfabricProperties> as UpdaterType>::Updater),
     Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
+    Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater),
 }
 
 impl Updater for FabricUpdater {
@@ -254,6 +275,7 @@ impl Updater for FabricUpdater {
         match self {
             FabricUpdater::Openfabric(updater) => updater.is_empty(),
             FabricUpdater::Ospf(updater) => updater.is_empty(),
+            FabricUpdater::Bgp(updater) => updater.is_empty(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
index d02d4ae..698dac9 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
@@ -10,6 +10,7 @@ use crate::sdn::fabric::section_config::{
     fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR},
     node::{Node, NodeSection, NODE_ID_REGEX_STR},
     protocol::{
+        bgp::{BgpNode, BgpProperties},
         openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
         ospf::{OspfNodeProperties, OspfProperties},
     },
@@ -31,8 +32,10 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
         match section {
             Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
+            Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()),
             Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
             Section::OspfNode(node_section) => Self::Node(node_section.into()),
+            Section::BgpNode(node_section) => Self::Node(node_section.into()),
         }
     }
 }
@@ -62,8 +65,10 @@ pub const SECTION_ID_FORMAT: ApiStringFormat = ApiStringFormat::Pattern(&SECTION
 pub enum Section {
     OpenfabricFabric(FabricSection<OpenfabricProperties>),
     OspfFabric(FabricSection<OspfProperties>),
+    BgpFabric(FabricSection<BgpProperties>),
     OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
     OspfNode(NodeSection<OspfNodeProperties>),
+    BgpNode(NodeSection<BgpNode>),
 }
 
 impl From<FabricSection<OpenfabricProperties>> for Section {
@@ -78,6 +83,12 @@ impl From<FabricSection<OspfProperties>> for Section {
     }
 }
 
+impl From<FabricSection<BgpProperties>> for Section {
+    fn from(section: FabricSection<BgpProperties>) -> Self {
+        Self::BgpFabric(section)
+    }
+}
+
 impl From<NodeSection<OpenfabricNodeProperties>> for Section {
     fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
         Self::OpenfabricNode(section)
@@ -90,11 +101,18 @@ impl From<NodeSection<OspfNodeProperties>> for Section {
     }
 }
 
+impl From<NodeSection<BgpNode>> for Section {
+    fn from(section: NodeSection<BgpNode>) -> Self {
+        Self::BgpNode(section)
+    }
+}
+
 impl From<Fabric> for Section {
     fn from(fabric: Fabric) -> Self {
         match fabric {
             Fabric::Openfabric(fabric_section) => fabric_section.into(),
             Fabric::Ospf(fabric_section) => fabric_section.into(),
+            Fabric::Bgp(fabric_section) => fabric_section.into(),
         }
     }
 }
@@ -104,6 +122,7 @@ impl From<Node> for Section {
         match node {
             Node::Openfabric(node_section) => node_section.into(),
             Node::Ospf(node_section) => node_section.into(),
+            Node::Bgp(node_section) => node_section.into(),
         }
     }
 }
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
index 17d2f0b..8f4564c 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
@@ -10,6 +10,7 @@ use proxmox_schema::{
 };
 
 use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties};
 use crate::sdn::fabric::section_config::{
     fabric::{FabricId, FABRIC_ID_REGEX_STR},
     protocol::{openfabric::OpenfabricNodeProperties, ospf::OspfNodeProperties},
@@ -186,6 +187,7 @@ impl<T: ApiType> ApiType for NodeSection<T> {
 pub enum Node {
     Openfabric(NodeSection<OpenfabricNodeProperties>),
     Ospf(NodeSection<OspfNodeProperties>),
+    Bgp(NodeSection<BgpNode>),
 }
 
 impl Node {
@@ -194,6 +196,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.id(),
             Node::Ospf(node_section) => node_section.id(),
+            Node::Bgp(node_section) => node_section.id(),
         }
     }
 
@@ -202,6 +205,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip(),
             Node::Ospf(node_section) => node_section.ip(),
+            Node::Bgp(node_section) => node_section.ip(),
         }
     }
 
@@ -210,6 +214,7 @@ impl Node {
         match self {
             Node::Openfabric(node_section) => node_section.ip6(),
             Node::Ospf(node_section) => node_section.ip6(),
+            Node::Bgp(node_section) => node_section.ip6(),
         }
     }
 }
@@ -221,6 +226,7 @@ impl Validatable for Node {
         match self {
             Node::Openfabric(node_section) => node_section.validate(),
             Node::Ospf(node_section) => node_section.validate(),
+            Node::Bgp(node_section) => node_section.validate(),
         }
     }
 }
@@ -237,6 +243,12 @@ impl From<NodeSection<OspfNodeProperties>> for Node {
     }
 }
 
+impl From<NodeSection<BgpNode>> for Node {
+    fn from(value: NodeSection<BgpNode>) -> Self {
+        Self::Bgp(value)
+    }
+}
+
 /// API types for SDN fabric node configurations.
 ///
 /// This module provides specialized types that are used for API interactions when retrieving,
@@ -258,6 +270,7 @@ pub mod api {
     use proxmox_schema::{Updater, UpdaterType};
 
     use crate::sdn::fabric::section_config::protocol::{
+        bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater},
         openfabric::{
             OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
             OpenfabricNodePropertiesUpdater,
@@ -320,6 +333,7 @@ pub mod api {
     pub enum Node {
         Openfabric(NodeData<OpenfabricNodeProperties>),
         Ospf(NodeData<OspfNodeProperties>),
+        Bgp(NodeData<BgpNode>),
     }
 
     impl From<super::Node> for Node {
@@ -327,6 +341,7 @@ pub mod api {
             match value {
                 super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                super::Node::Bgp(node_section) => Self::Bgp(node_section.into()),
             }
         }
     }
@@ -336,6 +351,7 @@ pub mod api {
             match value {
                 Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
                 Node::Ospf(node_section) => Self::Ospf(node_section.into()),
+                Node::Bgp(node_section) => Self::Bgp(node_section.into()),
             }
         }
     }
@@ -349,6 +365,10 @@ pub mod api {
         type Updater = NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>;
     }
 
+    impl UpdaterType for NodeData<BgpNodeProperties> {
+        type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>;
+    }
+
     #[derive(Debug, Clone, Serialize, Deserialize)]
     pub struct NodeDataUpdater<T, D> {
         #[serde(skip_serializing_if = "Option::is_none")]
@@ -384,6 +404,7 @@ pub mod api {
             NodeDataUpdater<OpenfabricNodePropertiesUpdater, OpenfabricNodeDeletableProperties>,
         ),
         Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
+        Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>),
     }
 
     #[derive(Debug, Clone, Serialize, Deserialize)]
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
new file mode 100644
index 0000000..9b8e5fc
--- /dev/null
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
@@ -0,0 +1,286 @@
+use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr};
+use std::ops::{Deref, DerefMut};
+
+use proxmox_network_types::ip_address::api_types::Ipv4Addr;
+use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType};
+use serde::{Deserialize, Serialize};
+
+use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
+
+use crate::common::valid::Validatable;
+use crate::sdn::fabric::section_config::fabric::FabricSection;
+use crate::sdn::fabric::section_config::interface::InterfaceName;
+use crate::sdn::fabric::section_config::node::NodeSection;
+use crate::sdn::fabric::FabricConfigError;
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+#[serde(rename_all = "lowercase")]
+/// Redistribution Sources for BGP fabric
+pub enum BgpRedistributionSource {
+    /// redistribute connected routes
+    Connected,
+    /// redistribute IS-IS routes
+    Isis,
+    /// redistribute kernel routes
+    Kernel,
+    /// redistribute openfabric routes
+    Openfabric,
+    /// redistribute ospfv2 routes
+    Ospf,
+    /// redistribute ospfv3 routes
+    Ospf6,
+    /// redistribute static routes
+    Static,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// A BGP redistribution target
+pub struct BgpRedistribution {
+    /// The source used for redistribution
+    pub(crate) source: BgpRedistributionSource,
+    /// The metric to apply to redistributed routes
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub(crate) metric: Option<u32>,
+    /// Route MAP to use for filtering redistributed routes
+    #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")]
+    pub(crate) route_map: Option<String>,
+}
+
+#[api(
+    type: Integer,
+    minimum: u32::MIN as i64,
+    maximum: u32::MAX as i64,
+)]
+#[derive(Debug, Clone, Serialize, Updater, Hash)]
+/// Autonomous system number as defined by RFC 6793
+pub struct ASN(u32);
+
+impl<'de> Deserialize<'de> for ASN {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        use serde::de::{self, Visitor};
+
+        struct AsnVisitor;
+
+        impl<'de> Visitor<'de> for AsnVisitor {
+            type Value = ASN;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a u32 or string containing a u32")
+            }
+
+            fn visit_i64<E: de::Error>(self, v: i64) -> Result<ASN, E> {
+                u32::try_from(v)
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
+            }
+
+            fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> {
+                u32::try_from(v)
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
+            }
+
+            fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> {
+                v.parse::<u32>()
+                    .map(ASN)
+                    .map_err(|_| E::custom(format!("invalid ASN: {v}")))
+            }
+        }
+
+        deserializer.deserialize_any(AsnVisitor)
+    }
+}
+
+impl UpdaterType for ASN {
+    type Updater = Option<ASN>;
+}
+
+impl ASN {
+    pub fn as_u32(&self) -> u32 {
+        self.0
+    }
+}
+
+#[api(
+    properties: {
+        redistribute: {
+            type: Array,
+            optional: true,
+            items: {
+                type: String,
+                description: "A BGP redistribution source",
+                format: &ApiStringFormat::PropertyString(&BgpRedistribution::API_SCHEMA),
+            }
+        }
+    },
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Bgp fabric.
+pub struct BgpProperties {
+    /// enable BFD for this fabric
+    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+    pub(crate) bfd: bool,
+    /// redistribution configuration for this fabric
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    #[updater(serde(skip_serializing_if = "Option::is_none"))]
+    pub(crate) redistribute: Vec<PropertyString<BgpRedistribution>>,
+}
+
+impl BgpProperties {
+    pub fn bfd(&self) -> bool {
+        self.bfd
+    }
+}
+
+impl Validatable for FabricSection<BgpProperties> {
+    type Error = FabricConfigError;
+
+    /// Validate the [`FabricSection<BgpProperties>`].
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum BgpDeletableProperties {}
+
+#[api]
+/// External Bgp Node
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+pub struct ExternalBgpNode {
+    peer_ip: Option<Ipv4Addr>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
+#[serde(rename_all = "snake_case", tag = "role")]
+pub enum BgpNode {
+    Internal(BgpNodeProperties),
+    External(ExternalBgpNode),
+}
+
+impl ApiType for BgpNode {
+    const API_SCHEMA: Schema = OneOfSchema::new(
+        "BGP node",
+        &(
+            "role",
+            false,
+            &StringSchema::new("internal or external").schema(),
+        ),
+        &[
+            ("external", &ExternalBgpNode::API_SCHEMA),
+            ("internal", &BgpNodeProperties::API_SCHEMA),
+        ],
+    )
+    .schema();
+}
+
+impl Validatable for NodeSection<BgpNode> {
+    type Error = FabricConfigError;
+
+    fn validate(&self) -> Result<(), Self::Error> {
+        if self.ip().is_none() && self.ip6().is_none() {
+            return Err(FabricConfigError::NodeNoIp(self.id().to_string()));
+        }
+        Ok(())
+    }
+}
+
+#[api(
+    properties: {
+        interfaces: {
+            type: Array,
+            optional: true,
+            items: {
+                type: String,
+                description: "Properties for an Bgp interface.",
+                format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA),
+            }
+        },
+    }
+)]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an Bgp node.
+pub struct BgpNodeProperties {
+    /// Autonomous system number for this Node
+    pub(crate) asn: ASN,
+    /// Interfaces for this Node.
+    #[serde(default)]
+    pub(crate) interfaces: Vec<PropertyString<BgpInterfaceProperties>>,
+}
+
+impl BgpNodeProperties {
+    /// Returns the ASN for this node.
+    pub fn asn(&self) -> &ASN {
+        &self.asn
+    }
+
+    /// Returns an iterator over all the interfaces.
+    pub fn interfaces(&self) -> impl Iterator<Item = &BgpInterfaceProperties> {
+        self.interfaces
+            .iter()
+            .map(|property_string| property_string.deref())
+    }
+
+    /// Returns an iterator over all the interfaces (mutable).
+    pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut BgpInterfaceProperties> {
+        self.interfaces
+            .iter_mut()
+            .map(|property_string| property_string.deref_mut())
+    }
+}
+
+impl Validatable for NodeSection<BgpNodeProperties> {
+    type Error = FabricConfigError;
+
+    /// Validate the [`NodeSection<BgpNodeProperties>`].
+    fn validate(&self) -> Result<(), Self::Error> {
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case", untagged)]
+pub enum BgpNodeDeletableProperties {
+    Interfaces,
+}
+
+#[api]
+#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
+/// Properties for an BGP interface.
+pub struct BgpInterfaceProperties {
+    pub(crate) name: InterfaceName,
+}
+
+impl BgpInterfaceProperties {
+    /// Get the name of the BGP interface.
+    pub fn name(&self) -> &InterfaceName {
+        &self.name
+    }
+
+    /// Set the name of the interface.
+    pub fn set_name(&mut self, name: InterfaceName) {
+        self.name = name
+    }
+}
+
+/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a.
+///
+/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the
+/// full 16 octets down to 4 bytes. Typical loopback allocations (sequential
+/// within a prefix, sparse across /48s) produce zero collisions up to 100k
+/// nodes in testing -- well below the random birthday bound (~1% at 10k)
+/// because structured addresses spread well under FNV-1a.
+pub fn router_id_from_ipv6(addr: &Ipv6Addr) -> StdIpv4Addr {
+    let mut hash: u32 = 0x811c9dc5;
+    for &byte in &addr.octets() {
+        hash ^= byte as u32;
+        hash = hash.wrapping_mul(0x01000193);
+    }
+    StdIpv4Addr::from(hash)
+}
diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
index c1ec847..8f918ef 100644
--- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
+++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
@@ -1,2 +1,3 @@
+pub mod bgp;
 pub mod openfabric;
 pub mod ospf;
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
new file mode 100644
index 0000000..bd434a7
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+        bfd 0
+        ip_prefix 10.10.10.0/24
+
+bgp_node: test_pve
+        asn 65001
+        interfaces name=ens18
+        interfaces name=ens19
+        ip 10.10.10.1
+        role internal
+
+bgp_node: test_pve1
+        asn 65002
+        interfaces name=ens19
+        ip 10.10.10.2
+        role internal
+
diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
new file mode 100644
index 0000000..f4581fb
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
@@ -0,0 +1,17 @@
+bgp_fabric: test
+        bfd 0
+        ip6_prefix fd00:10::/64
+
+bgp_node: test_pve
+        asn 65001
+        interfaces name=ens18
+        interfaces name=ens19
+        ip6 fd00:10::1
+        role internal
+
+bgp_node: test_pve1
+        asn 65002
+        interfaces name=ens19
+        ip6 fd00:10::2
+        role internal
+
diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
index 95b2e62..49c5fcc 100644
--- a/proxmox-ve-config/tests/fabric/main.rs
+++ b/proxmox-ve-config/tests/fabric/main.rs
@@ -1,7 +1,9 @@
 #![cfg(feature = "frr")]
+use std::net::Ipv4Addr;
 use std::str::FromStr;
 
-use proxmox_frr::ser::{serializer::dump, FrrConfig};
+use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF};
+use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName};
 use proxmox_ve_config::sdn::fabric::{
     frr::build_fabric, section_config::node::NodeId, FabricConfig,
 };
@@ -162,3 +164,118 @@ fn openfabric_ipv6_only() {
 
     insta::assert_snapshot!(helper::reference_name!("pve"), output);
 }
+
+#[test]
+fn bgp_default() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+    let mut frr_config = FrrConfig::default();
+
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let mut output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+    frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+#[test]
+fn bgp_ipv6_only() {
+    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
+    let mut frr_config = FrrConfig::default();
+
+    build_fabric(
+        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
+        config.clone(),
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let mut output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+
+    frr_config = FrrConfig::default();
+    build_fabric(
+        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
+}
+
+/// Test that build_fabric merges into an existing EVPN router and sets local-as
+/// when the ASNs differ.
+#[test]
+fn bgp_merge_with_evpn() {
+    let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg")
+        .expect("cannot find config file");
+    let config = FabricConfig::parse_section_config(&raw).unwrap();
+
+    // Pre-populate with an EVPN-like router using a different ASN
+    let mut frr_config = FrrConfig::default();
+    let evpn_router = BgpRouter {
+        asn: 65000,
+        router_id: Ipv4Addr::new(10, 10, 10, 1),
+        coalesce_time: Some(1000),
+        default_ipv4_unicast: Some(false),
+        hard_administrative_reset: None,
+        graceful_restart_notification: None,
+        disable_ebgp_connected_route_check: None,
+        bestpath_as_path_multipath_relax: None,
+        neighbor_groups: Vec::new(),
+        address_families: AddressFamilies {
+            ipv4_unicast: None,
+            ipv6_unicast: None,
+            l2vpn_evpn: Some(L2vpnEvpnAF {
+                common_options: CommonAddressFamilyOptions {
+                    import_vrf: Vec::new(),
+                    neighbors: Vec::new(),
+                    custom_frr_config: Vec::new(),
+                },
+                advertise_all_vni: Some(true),
+                advertise_default_gw: None,
+                default_originate: Vec::new(),
+                advertise_ipv4_unicast: None,
+                advertise_ipv6_unicast: None,
+                autort_as: None,
+                route_targets: None,
+            }),
+        },
+        custom_frr_config: Vec::new(),
+    };
+    frr_config
+        .bgp
+        .vrf_router
+        .insert(VrfName::Default, evpn_router);
+
+    build_fabric(
+        NodeId::from_str("pve").expect("invalid nodeid"),
+        config,
+        &mut frr_config,
+    )
+    .unwrap();
+
+    let output = dump(&frr_config).expect("error dumping stuff");
+
+    insta::assert_snapshot!(helper::reference_name!("pve"), output);
+}
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
new file mode 100644
index 0000000..34aa2cc
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 10.10.10.1
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.1/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
new file mode 100644
index 0000000..a3e6b6d
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
@@ -0,0 +1,27 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 10.10.10.2
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.2/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.2
+exit
+!
+ip protocol bgp route-map pve_bgp
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
new file mode 100644
index 0000000..f335a4f
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
@@ -0,0 +1,29 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65001
+ bgp router-id 5.76.46.251
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+  network fd00:10::1/128
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::1
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
new file mode 100644
index 0000000..67628a9
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
@@ -0,0 +1,28 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65002
+ bgp router-id 6.76.48.142
+ no bgp default ipv4-unicast
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv6 unicast
+  network fd00:10::2/128
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+exit
+!
+ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
+!
+route-map pve_bgp6 permit 100
+ match ipv6 address pve_bgp_test_ip6s
+ set src fd00:10::2
+exit
+!
+!
+ipv6 protocol bgp route-map pve_bgp6
diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
new file mode 100644
index 0000000..ef57cd6
--- /dev/null
+++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
@@ -0,0 +1,34 @@
+---
+source: proxmox-ve-config/tests/fabric/main.rs
+expression: output
+---
+!
+router bgp 65000
+ bgp router-id 10.10.10.1
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor test local-as 65001 no-prepend replace-as
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.1/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+  advertise-all-vni
+ exit-address-family
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+ip protocol bgp route-map pve_bgp
-- 
2.47.3





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

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

From: Stefan Hanreich <s.hanreich@proxmox.com>

Add BGP support to the fabric config generation pipeline. This
includes interface name mapping and network interface output for
BGP unnumbered links.

Co-authored-by: Hannes Laimer <h.laimer@proxmox.com>
Signed-off-by: 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..15dfee8 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");
                 }
+                // bgpd is enabled by default in /etc/frr/daemons
+                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] 8+ messages in thread

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

Expose BGP fabric status through the existing fabric status API.
Routes are fetched for both IPv4 and IPv6, and neighbor/interface
state is derived from BGP session info.

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

diff --git a/pve-rs/src/bindings/sdn/fabrics.rs b/pve-rs/src/bindings/sdn/fabrics.rs
index 15dfee8..aa4324a 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,37 @@ pub mod pve_rs_sdn_fabrics {
                 .with_context(|| "error parsing ospf routes")?
         };
 
+        let bgp_ipv4_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ip route bgp json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let bgp_ipv6_routes_string = String::from_utf8(
+            Command::new("sh")
+                .args(["-c", "vtysh -c 'show ipv6 route bgp json'"])
+                .output()?
+                .stdout,
+        )?;
+
+        let mut bgp_routes: proxmox_frr::de::Routes = if bgp_ipv4_routes_string.is_empty() {
+            proxmox_frr::de::Routes::default()
+        } else {
+            serde_json::from_str(&bgp_ipv4_routes_string)
+                .with_context(|| "error parsing bgp ipv4 routes")?
+        };
+        if !bgp_ipv6_routes_string.is_empty() {
+            let bgp_ipv6_routes: proxmox_frr::de::Routes =
+                serde_json::from_str(&bgp_ipv6_routes_string)
+                    .with_context(|| "error parsing bgp ipv6 routes")?;
+            bgp_routes.0.extend(bgp_ipv6_routes.0);
+        }
+
         let route_status = status::RoutesParsed {
             openfabric: openfabric_routes,
             ospf: ospf_routes,
+            bgp: bgp_routes,
         };
 
         status::get_status(config, route_status, proxmox_sys::nodename())
diff --git a/pve-rs/src/sdn/status.rs b/pve-rs/src/sdn/status.rs
index 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] 8+ messages in thread

* [PATCH pve-network v2 4/7] sdn: fabrics: register bgp as a fabric protocol type
  2026-04-15 11:11 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric Hannes Laimer
                   ` (2 preceding siblings ...)
  2026-04-15 11:11 ` [PATCH proxmox-perl-rs v2 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer
@ 2026-04-15 11:11 ` Hannes Laimer
  2026-04-15 11:11 ` [PATCH pve-network v2 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Hannes Laimer @ 2026-04-15 11:11 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 1ecedbde..f9a9a92e 100644
--- a/src/PVE/Network/SDN/Fabrics.pm
+++ b/src/PVE/Network/SDN/Fabrics.pm
@@ -49,7 +49,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'],
     },
 );
 
@@ -200,6 +200,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] 8+ messages in thread

* [PATCH pve-network v2 5/7] test: evpn: add integration test for EVPN over BGP fabric
  2026-04-15 11:11 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric Hannes Laimer
                   ` (3 preceding siblings ...)
  2026-04-15 11:11 ` [PATCH pve-network v2 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
@ 2026-04-15 11:11 ` Hannes Laimer
  2026-04-15 11:11 ` [PATCH pve-manager v2 6/7] ui: sdn: add BGP fabric support Hannes Laimer
  2026-04-15 11:11 ` [PATCH pve-docs v2 7/7] sdn: add bgp fabric section Hannes Laimer
  6 siblings, 0 replies; 8+ messages in thread
From: Hannes Laimer @ 2026-04-15 11:11 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 .../bgp_fabric/expected_controller_config     | 65 ++++++++++++++
 .../evpn/bgp_fabric/expected_sdn_interfaces   | 56 ++++++++++++
 src/test/zones/evpn/bgp_fabric/interfaces     |  6 ++
 src/test/zones/evpn/bgp_fabric/sdn_config     | 85 +++++++++++++++++++
 4 files changed, 212 insertions(+)
 create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config
 create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces
 create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces
 create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config

diff --git a/src/test/zones/evpn/bgp_fabric/expected_controller_config b/src/test/zones/evpn/bgp_fabric/expected_controller_config
new file mode 100644
index 00000000..440155e7
--- /dev/null
+++ b/src/test/zones/evpn/bgp_fabric/expected_controller_config
@@ -0,0 +1,65 @@
+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 65000
+ bgp router-id 10.10.10.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor VTEP update-source dummy_test
+ neighbor 10.10.10.2 peer-group VTEP
+ neighbor 10.10.10.3 peer-group VTEP
+ neighbor test peer-group
+ neighbor test remote-as external
+ neighbor test local-as 65100 no-prepend replace-as
+ neighbor ens18 interface peer-group test
+ neighbor ens19 interface peer-group test
+ !
+ address-family ipv4 unicast
+  network 10.10.10.1/32
+  neighbor test activate
+  neighbor test soft-reconfiguration inbound
+ exit-address-family
+ !
+ address-family l2vpn evpn
+  neighbor VTEP activate
+  neighbor VTEP route-map MAP_VTEP_IN in
+  neighbor VTEP route-map MAP_VTEP_OUT out
+  advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_evpn
+ bgp router-id 10.10.10.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+access-list pve_bgp_test_ips permit 10.10.10.0/24
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+route-map pve_bgp permit 100
+ match ip address pve_bgp_test_ips
+ set src 10.10.10.1
+exit
+!
+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] 8+ messages in thread

* [PATCH pve-manager v2 6/7] ui: sdn: add BGP fabric support
  2026-04-15 11:11 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric Hannes Laimer
                   ` (4 preceding siblings ...)
  2026-04-15 11:11 ` [PATCH pve-network v2 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
@ 2026-04-15 11:11 ` Hannes Laimer
  2026-04-15 11:11 ` [PATCH pve-docs v2 7/7] sdn: add bgp fabric section Hannes Laimer
  6 siblings, 0 replies; 8+ messages in thread
From: Hannes Laimer @ 2026-04-15 11:11 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    | 53 +++++++++++++++++++
 .../sdn/fabrics/bgp/InterfacePanel.js         | 13 +++++
 www/manager6/sdn/fabrics/bgp/NodeEdit.js      | 32 +++++++++++
 6 files changed, 114 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 b506849d..b40b373c 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -335,6 +335,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..ab8a75c4
--- /dev/null
+++ b/www/manager6/sdn/fabrics/bgp/FabricEdit.js
@@ -0,0 +1,53 @@
+Ext.define('PVE.sdn.Fabric.Bgp.Fabric.Edit', {
+    extend: 'PVE.sdn.Fabric.Fabric.Edit',
+
+    subject: 'BGP',
+    onlineHelp: 'pvesdn_bgp_fabric',
+
+    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..d9f46de0
--- /dev/null
+++ b/www/manager6/sdn/fabrics/bgp/InterfacePanel.js
@@ -0,0 +1,13 @@
+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] 8+ messages in thread

* [PATCH pve-docs v2 7/7] sdn: add bgp fabric section
  2026-04-15 11:11 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 0/7] sdn: add BGP fabric Hannes Laimer
                   ` (5 preceding siblings ...)
  2026-04-15 11:11 ` [PATCH pve-manager v2 6/7] ui: sdn: add BGP fabric support Hannes Laimer
@ 2026-04-15 11:11 ` Hannes Laimer
  6 siblings, 0 replies; 8+ messages in thread
From: Hannes Laimer @ 2026-04-15 11:11 UTC (permalink / raw)
  To: pve-devel

---
 pvesdn.adoc | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index 8f955e8..c993c6d 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -835,6 +835,72 @@ NOTE: The dummy interface will automatically be configured as `passive`. Every
 interface which doesn't have an ip-address configured will be treated as a
 `point-to-point` link.
 
+[[pvesdn_bgp]]
+BGP
+~~~
+
+BGP (Border Gateway Protocol) can be used as an eBGP unnumbered underlay
+fabric. Each node gets its own unique Autonomous System Number (ASN) and
+peers with its neighbors over physical interfaces without requiring IP
+addresses on the fabric links.
+
+Configuration options:
+
+[[pvesdn_bgp_fabric]]
+On the Fabric
+^^^^^^^^^^^^^
+
+IPv4 Prefix:: IPv4 CIDR network range (e.g., 192.0.2.0/24) used to verify that
+all router-IDs in the fabric are contained within this prefix.
+
+IPv6 Prefix:: IPv6 CIDR network range (e.g., 2001:db8::/64) used to verify that
+all node IPv6 addresses in the fabric are contained within this prefix.
+
+BFD:: Enable Bidirectional Forwarding Detection on all peering sessions in this
+fabric. BFD provides fast failure detection for links between nodes.
+
+[[pvesdn_bgp_node]]
+On the Node
+^^^^^^^^^^^
+
+Options that are available on every node that is part of a fabric:
+
+Node:: Select the node which will be added to the fabric. Only nodes that
+are currently in the cluster will be shown.
+
+ASN:: A unique BGP Autonomous System Number for this node. Each node in the
+fabric must have a different ASN. It is recommended to use private ASN numbers
+(64512-65534 for 16-bit, 4200000000-4294967294 for 32-bit).
+
+IPv4:: A unique Router-ID used to identify this router within the BGP network.
+Each node in the same fabric must have a different Router-ID.
+
+IPv6:: An optional IPv6 address for dual-stack fabrics. If only an IPv6 address
+is configured (without an IPv4 address), the BGP router-id will be
+automatically derived from it.
+
+Interfaces:: Specify the interfaces used to establish peering connections with
+other BGP nodes. These interfaces run BGP unnumbered (no IP address assignment
+needed). A dummy "loopback" interface with the router-id is automatically
+created.
+
+NOTE: Unlike OSPF and OpenFabric, BGP unnumbered interfaces do not need IP
+addresses. Peering is established using IPv6 link-local addresses automatically.
+
+[[pvesdn_bgp_evpn]]
+Using BGP Fabrics with EVPN
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When configuring an EVPN controller, a BGP fabric can be selected as the
+underlay instead of manually specifying peer addresses. The EVPN overlay
+sessions will run as iBGP, using the EVPN controller's ASN for the router
+process. The per-node fabric ASN is automatically applied via `local-as` on
+the underlay neighbor group.
+
+This means the EVPN controller ASN and the per-node fabric ASNs should be
+different. For example, with three nodes using ASNs 65001, 65002, and 65003
+for the underlay, the EVPN controller could use ASN 65000 for the overlay.
+
 [[pvesdn_config_ipam]]
 IPAM
 ----
-- 
2.47.3





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

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

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

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal