public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support
Date: Wed, 13 May 2026 14:29:29 +0200	[thread overview]
Message-ID: <ebd653ed-cfb2-4a9d-b131-7027f7e2b481@proxmox.com> (raw)
In-Reply-To: <20260512141305.199664-2-h.laimer@proxmox.com>

On 5/12/26 4:12 PM, Hannes Laimer wrote:
> +impl AddressFamilies {
> +    /// Merge another [`AddressFamilies`] into this one.
> +    ///
> +    /// For each address family: if `self` already has it, extend its neighbors, networks, and
> +    /// redistribute lists. If `self` doesn't have it, take it from `other`.
> +    pub fn merge(&mut self, other: AddressFamilies) {

maybe extend is the better name, as it mirrors existing conventions from
std::Vec?

> +        match (self.ipv4_unicast.as_mut(), other.ipv4_unicast) {
> +            (Some(existing), Some(incoming)) => {
> +                existing
> +                    .common_options
> +                    .neighbors
> +                    .extend(incoming.common_options.neighbors);
> +                existing
> +                    .common_options
> +                    .import_vrf
> +                    .extend(incoming.common_options.import_vrf);
> +                existing
> +                    .common_options
> +                    .custom_frr_config
> +                    .extend(incoming.common_options.custom_frr_config);
> +                existing.networks.extend(incoming.networks);
> +                existing.redistribute.extend(incoming.redistribute);
> +            }
> +            (None, Some(incoming)) => {
> +                self.ipv4_unicast = Some(incoming);
> +            }
> +            _ => {}
> +        }
> +
> +        match (self.ipv6_unicast.as_mut(), other.ipv6_unicast) {
> +            (Some(existing), Some(incoming)) => {
> +                existing
> +                    .common_options
> +                    .neighbors
> +                    .extend(incoming.common_options.neighbors);
> +                existing
> +                    .common_options
> +                    .import_vrf
> +                    .extend(incoming.common_options.import_vrf);
> +                existing
> +                    .common_options
> +                    .custom_frr_config
> +                    .extend(incoming.common_options.custom_frr_config);
> +                existing.networks.extend(incoming.networks);
> +                existing.redistribute.extend(incoming.redistribute);
> +            }
> +            (None, Some(incoming)) => {
> +                self.ipv6_unicast = Some(incoming);
> +            }
> +            _ => {}
> +        }
> +
> +        // l2vpn_evpn: only take from other if self doesn't have it (fabric never sets this)
> +        if self.l2vpn_evpn.is_none() {
> +            self.l2vpn_evpn = other.l2vpn_evpn;
> +        }
> +    }
> +}
> +
> +impl BgpRouter {
> +    /// Merge a fabric-generated [`BgpRouter`] into an existing one.
> +    ///
> +    /// Appends the fabric's neighbor groups and merges address families. Keeps the existing
> +    /// router's ASN, router-id, and other top-level settings. The caller is responsible for
> +    /// setting `local_as` on the fabric's neighbor group if the ASNs differ.
> +    pub fn merge_fabric(&mut self, other: BgpRouter) {
> +        self.neighbor_groups.extend(other.neighbor_groups);
> +        self.address_families.merge(other.address_families);
> +
> +        if self.default_ipv4_unicast.is_none() {
> +            self.default_ipv4_unicast = other.default_ipv4_unicast;
> +        }
> +    }
>  }
>  
>  #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
> diff --git a/proxmox-ve-config/src/sdn/fabric/frr.rs b/proxmox-ve-config/src/sdn/fabric/frr.rs
> index 40e346f..d70d5aa 100644
> --- a/proxmox-ve-config/src/sdn/fabric/frr.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/frr.rs
> @@ -2,15 +2,25 @@ use std::net::{IpAddr, Ipv4Addr};
>  
>  use tracing;
>  
> +use proxmox_frr::ser::bgp::{
> +    AddressFamilies, AddressFamilyNeighbor, BgpRouter, CommonAddressFamilyOptions, Ipv4UnicastAF,
> +    Ipv6UnicastAF, LocalAsFlags, LocalAsSettings, NeighborGroup, NeighborRemoteAs,
> +    RedistributeProtocol, Redistribution,
> +};
>  use proxmox_frr::ser::openfabric::{OpenfabricInterface, OpenfabricRouter, OpenfabricRouterName};
>  use proxmox_frr::ser::ospf::{self, OspfInterface, OspfRedistribution, OspfRouter};
> -use proxmox_frr::ser::route_map::{AccessListName, RouteMapEntry, RouteMapMatch, RouteMapSet};
> -use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName};
> -use proxmox_network_types::ip_address::Cidr;
> +use proxmox_frr::ser::route_map::{
> +    AccessListName, RouteMapEntry, RouteMapMatch, RouteMapName, RouteMapSet,
> +};
> +use proxmox_frr::ser::{self, FrrConfig, FrrProtocol, FrrWord, Interface, InterfaceName, VrfName};
> +use proxmox_network_types::ip_address::{Cidr, Ipv4Cidr, Ipv6Cidr};
>  use proxmox_sdn_types::net::Net;
>  
>  use crate::common::valid::Valid;
> +
> +use crate::sdn::fabric::section_config::protocol::bgp::{bgp_router_id, BgpNode};
>  use crate::sdn::fabric::section_config::protocol::{
> +    bgp::BgpRedistributionSource,
>      openfabric::{OpenfabricInterfaceProperties, OpenfabricProperties},
>      ospf::OspfInterfaceProperties,
>  };
> @@ -289,6 +299,294 @@ pub fn build_fabric(
>                  protocol_routemap.v4 = Some(routemap_name);
>              }
>              FabricEntry::WireGuard(_) => {} // not a frr fabric
> +            FabricEntry::Bgp(bgp_entry) => {
> +                let Ok(node) = bgp_entry.node_section(&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();

makes me wonder if implementing AsRef<u32> would simplify handling ASN
throughout the code?

> +
> +                let mut bgp_interfaces = Vec::new();
> +                for interface in properties.interfaces() {
> +                    bgp_interfaces.push(interface.name.as_str().try_into()?)
> +                }
> +
> +                let neighbor_group = NeighborGroup {
> +                    name: FrrWord::new(fabric.id().to_string())?,
> +                    bfd: fabric.properties().bfd(),
> +                    remote_as: NeighborRemoteAs::External,
> +                    local_as: Default::default(),
> +                    interfaces: bgp_interfaces,
> +                    ips: Default::default(),
> +                    ebgp_multihop: Default::default(),
> +                    update_source: Default::default(),
> +                };
> +
> +                let redistribute: Vec<Redistribution> = fabric
> +                    .properties()
> +                    .redistribute
> +                    .iter()
> +                    .map(|redistribution| Redistribution {
> +                        protocol: match redistribution.source {
> +                            BgpRedistributionSource::Ospf => RedistributeProtocol::Ospf,
> +                            BgpRedistributionSource::Connected => RedistributeProtocol::Connected,
> +                            BgpRedistributionSource::Isis => RedistributeProtocol::Isis,
> +                            BgpRedistributionSource::Kernel => RedistributeProtocol::Kernel,
> +                            BgpRedistributionSource::Openfabric => RedistributeProtocol::Openfabric,
> +                            BgpRedistributionSource::Ospf6 => RedistributeProtocol::Ospf6,
> +                            BgpRedistributionSource::Static => RedistributeProtocol::Static,
> +                        },
> +                        metric: redistribution.metric,
> +                        route_map: redistribution.route_map.clone().map(RouteMapName::from),
> +                    })
> +                    .collect();
> +
> +                let mut address_families = AddressFamilies::default();
> +
> +                if let Some(ip) = node.ip() {
> +                    // Build the prefix matcher (route_filter prefix-list, or
> +                    // an auto access-list from ip_prefix). Reused for both the
> +                    // pve_bgp set-src clause and the per-peer inbound filter
> +                    // below, so they stay in sync.
> +                    let inbound_matchers: Vec<RouteMapMatch> =
> +                        if let Some(prefix_list_id) = &fabric.properties().route_filter {
> +                            vec![RouteMapMatch::IpAddressPrefixList(
> +                                prefix_list_id.clone().into(),
> +                            )]
> +                        } else if let Some(cidr) = fabric.ip_prefix() {
> +                            let access_list_name =
> +                                AccessListName::new(format!("pve_bgp_{fabric_id}_ips"));
> +
> +                            let rule = ser::route_map::AccessListRule {
> +                                action: ser::route_map::AccessAction::Permit,
> +                                network: Cidr::from(cidr),
> +                                is_ipv6: false,
> +                                seq: None,
> +                            };
> +
> +                            frr_config
> +                                .access_lists
> +                                .insert(access_list_name.clone(), vec![rule]);
> +
> +                            vec![RouteMapMatch::IpAddressAccessList(access_list_name)]
> +                        } else {
> +                            Vec::new()
> +                        };
> +
> +                    // Per-peer inbound filter: permit prefixes matching the fabric's filter,
> +                    // implicit deny everything else. Stops a misbehaving fabric peer from leaking
> +                    // prefixes outside its declared range into BGP at all. If the user configured
> +                    // a custom route_map_in, it is chained via FRR's `call` action so it only sees
> +                    // prefixes that already passed the fabric-prefix filter.
> +                    let auto_in_routemap = if !inbound_matchers.is_empty() {
> +                        let name =
> +                            ser::route_map::RouteMapName::new(format!("pve_bgp_{fabric_id}_in"));
> +                        let in_routemap = frr_config.routemaps.entry(name.clone()).or_default();
> +                        in_routemap.push(RouteMapEntry {
> +                            seq: 10,
> +                            action: ser::route_map::AccessAction::Permit,
> +                            matches: inbound_matchers.clone(),
> +                            sets: Vec::new(),
> +                            custom_frr_config: Vec::new(),
> +                            call: fabric
> +                                .properties()
> +                                .route_map_in
> +                                .clone()
> +                                .map(RouteMapName::from),
> +                            exit_action: None,
> +                        });
> +                        Some(name)
> +                    } else {
> +                        None
> +                    };
> +
> +                    address_families.ipv4_unicast = Some(Ipv4UnicastAF {
> +                        common_options: CommonAddressFamilyOptions {
> +                            import_vrf: Default::default(),
> +                            neighbors: vec![AddressFamilyNeighbor {
> +                                name: fabric.id().to_string(),
> +                                route_map_in: auto_in_routemap,
> +                                route_map_out: fabric
> +                                    .properties()
> +                                    .route_map_out
> +                                    .clone()
> +                                    .map(RouteMapName::from),
> +                                soft_reconfiguration_inbound: Some(true),
> +                            }],
> +                            custom_frr_config: Default::default(),
> +                        },
> +                        redistribute: redistribute.clone(),
> +                        networks: vec![Ipv4Cidr::from(ip)],
> +                    });
> +
> +                    let routemap_name = ser::route_map::RouteMapName::new("pve_bgp".to_owned());
> +                    let routemap = frr_config
> +                        .routemaps
> +                        .entry(routemap_name.clone())
> +                        .or_default();
> +
> +                    let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq);
> +                    routemap_seq += 10;
> +                    routemap_entry.matches = inbound_matchers;
> +
> +                    routemap.push(routemap_entry);
> +
> +                    let protocol_routemap = frr_config
> +                        .protocol_routemaps
> +                        .entry(FrrProtocol::Bgp)
> +                        .or_default();
> +
> +                    protocol_routemap.v4 = Some(routemap_name);
> +                }
> +
> +                if let Some(ip) = node.ip6() {
> +                    let inbound_matchers: Vec<RouteMapMatch> =
> +                        if let Some(prefix_list_id) = &fabric.properties().route_filter {
> +                            vec![RouteMapMatch::Ip6AddressPrefixList(
> +                                prefix_list_id.clone().into(),
> +                            )]
> +                        } else if let Some(cidr) = fabric.ip6_prefix() {
> +                            let access_list_name =
> +                                AccessListName::new(format!("pve_bgp_{fabric_id}_ip6s"));
> +
> +                            let rule = ser::route_map::AccessListRule {
> +                                action: ser::route_map::AccessAction::Permit,
> +                                network: Cidr::from(cidr),
> +                                is_ipv6: true,
> +                                seq: None,
> +                            };
> +
> +                            frr_config
> +                                .access_lists
> +                                .insert(access_list_name.clone(), vec![rule]);
> +
> +                            vec![RouteMapMatch::Ip6AddressAccessList(access_list_name)]
> +                        } else {
> +                            Vec::new()
> +                        };
> +
> +                    let auto_in_routemap = if !inbound_matchers.is_empty() {
> +                        let name =
> +                            ser::route_map::RouteMapName::new(format!("pve_bgp6_{fabric_id}_in"));
> +                        let in_routemap = frr_config.routemaps.entry(name.clone()).or_default();
> +                        in_routemap.push(RouteMapEntry {
> +                            seq: 10,
> +                            action: ser::route_map::AccessAction::Permit,
> +                            matches: inbound_matchers.clone(),
> +                            sets: Vec::new(),
> +                            custom_frr_config: Vec::new(),
> +                            call: fabric
> +                                .properties()
> +                                .route_map_in
> +                                .clone()
> +                                .map(RouteMapName::from),
> +                            exit_action: None,
> +                        });
> +                        Some(name)
> +                    } else {
> +                        None
> +                    };
> +
> +                    address_families.ipv6_unicast = Some(Ipv6UnicastAF {
> +                        common_options: CommonAddressFamilyOptions {
> +                            import_vrf: Default::default(),
> +                            neighbors: vec![AddressFamilyNeighbor {
> +                                name: fabric.id().to_string(),
> +                                route_map_in: auto_in_routemap,
> +                                route_map_out: fabric
> +                                    .properties()
> +                                    .route_map_out
> +                                    .clone()
> +                                    .map(RouteMapName::from),
> +                                soft_reconfiguration_inbound: Some(true),
> +                            }],
> +                            custom_frr_config: Default::default(),
> +                        },
> +                        networks: vec![Ipv6Cidr::from(ip)],
> +                        redistribute,
> +                    });
> +
> +                    let routemap_name = ser::route_map::RouteMapName::new("pve_bgp6".to_owned());
> +                    let routemap = frr_config
> +                        .routemaps
> +                        .entry(routemap_name.clone())
> +                        .or_default();
> +
> +                    let mut routemap_entry = build_source_routemap(ip.into(), routemap_seq);
> +                    routemap_seq += 10;
> +                    routemap_entry.matches = inbound_matchers;
> +
> +                    routemap.push(routemap_entry);
> +
> +                    let protocol_routemap = frr_config
> +                        .protocol_routemaps
> +                        .entry(FrrProtocol::Bgp)
> +                        .or_default();
> +
> +                    protocol_routemap.v6 = Some(routemap_name);
> +                };
> +
> +                let router_id = bgp_router_id(&node)
> +                    .ok_or_else(|| anyhow::anyhow!("BGP node must have ip or ip6 set"))?;
> +
> +                let mut router = BgpRouter {
> +                    asn: local_asn,
> +                    router_id,
> +                    neighbor_groups: vec![neighbor_group],
> +                    address_families,
> +                    coalesce_time: Default::default(),
> +                    default_ipv4_unicast: Some(false),
> +                    hard_administrative_reset: Default::default(),
> +                    graceful_restart_notification: Default::default(),
> +                    disable_ebgp_connected_route_check: Default::default(),
> +                    bestpath_as_path_multipath_relax: Default::default(),
> +                    custom_frr_config: Default::default(),
> +                };
> +
> +                if let Some(existing) = frr_config.bgp.vrf_router.get_mut(&VrfName::Default) {
> +                    // If the existing router uses a different ASN (e.g. the
> +                    // EVPN ASN), set local-as on the fabric neighbor group so
> +                    // the underlay peers see the correct per-node ASN.
> +                    if existing.asn != local_asn {
> +                        if let Some(ng) = router.neighbor_groups.first_mut() {
> +                            ng.local_as = Some(LocalAsSettings {
> +                                asn: local_asn,
> +                                mode: Some(LocalAsFlags::ReplaceAs),
> +                            });
> +                        }
> +                    }
> +                    existing.merge_fabric(router);
> +                } else {
> +                    frr_config.bgp.vrf_router.insert(VrfName::Default, router);
> +                }
> +            }
> +        }
> +    }
> +
> +    // Append a trailing permit-all to the BGP route-maps so non-fabric BGP
> +    // routes (e.g. EVPN-imported VRF routes) reach the kernel unchanged.
> +    // Without this, the implicit deny at the end of the route-map would drop
> +    // them.
> +    for routemap_name in [
> +        ser::route_map::RouteMapName::new("pve_bgp".to_owned()),
> +        ser::route_map::RouteMapName::new("pve_bgp6".to_owned()),
> +    ] {
> +        if let Some(routemap) = frr_config.routemaps.get_mut(&routemap_name) {
> +            routemap.push(RouteMapEntry {
> +                seq: 65535,
> +                action: ser::route_map::AccessAction::Permit,
> +                matches: Vec::new(),
> +                sets: Vec::new(),
> +                custom_frr_config: Vec::new(),
> +                call: None,
> +                exit_action: None,
> +            });
>          }
>      }
>  
> diff --git a/proxmox-ve-config/src/sdn/fabric/mod.rs b/proxmox-ve-config/src/sdn/fabric/mod.rs
> index 5c20ec8..b21d335 100644
> --- a/proxmox-ve-config/src/sdn/fabric/mod.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/mod.rs
> @@ -2,7 +2,7 @@
>  pub mod frr;
>  pub mod section_config;
>  
> -use std::collections::{BTreeMap, HashSet};
> +use std::collections::{BTreeMap, HashMap, HashSet};
>  use std::marker::PhantomData;
>  use std::ops::Deref;
>  
> @@ -21,6 +21,10 @@ use crate::sdn::fabric::section_config::node::{
>      api::{NodeDataUpdater, NodeDeletableProperties, NodeUpdater},
>      Node, NodeId, NodeSection,
>  };
> +use crate::sdn::fabric::section_config::protocol::bgp::{
> +    bgp_router_id, BgpDeletableProperties, BgpNode, BgpNodeDeletableProperties,
> +    BgpNodePropertiesUpdater, BgpProperties, BgpPropertiesUpdater,
> +};
>  use crate::sdn::fabric::section_config::protocol::openfabric::{
>      OpenfabricDeletableProperties, OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
>      OpenfabricNodePropertiesUpdater, OpenfabricProperties, OpenfabricPropertiesUpdater,
> @@ -69,6 +73,8 @@ pub enum FabricConfigError {
>      // this is technically possible, but we don't allow it
>      #[error("duplicate OSPF area")]
>      DuplicateOspfArea,
> +    #[error("BGP router-id collision: multiple nodes resolve to the same router-id {0}")]
> +    DuplicateBgpRouterId(std::net::Ipv4Addr),
>      #[error("IP prefix {0} in fabric '{1}' overlaps with IPv4 prefix {2} in fabric '{3}'")]
>      OverlappingIp4Prefix(String, String, String, String),
>      #[error("IPv6 prefix {0} in fabric '{1}' overlaps with IPv6 prefix {2} in fabric '{3}'")]
> @@ -201,6 +207,7 @@ macro_rules! impl_entry {
>  impl_entry!(Openfabric, OpenfabricProperties, OpenfabricNodeProperties);
>  impl_entry!(Ospf, OspfProperties, OspfNodeProperties);
>  impl_entry!(WireGuard, WireGuardProperties, WireGuardNode);
> +impl_entry!(Bgp, BgpProperties, BgpNode);
>  
>  /// All possible entries in a [`FabricConfig`].
>  ///
> @@ -211,6 +218,7 @@ pub enum FabricEntry {
>      Openfabric(Entry<OpenfabricProperties, OpenfabricNodeProperties>),
>      Ospf(Entry<OspfProperties, OspfNodeProperties>),
>      WireGuard(Entry<WireGuardProperties, WireGuardNode>),
> +    Bgp(Entry<BgpProperties, BgpNode>),
>  }
>  
>  impl FabricEntry {
> @@ -225,6 +233,7 @@ impl FabricEntry {
>              (FabricEntry::WireGuard(entry), Node::WireGuard(node_section)) => {
>                  entry.add_node(node_section)
>              }
> +            (FabricEntry::Bgp(entry), Node::Bgp(node_section)) => entry.add_node(node_section),
>              _ => Err(FabricConfigError::ProtocolMismatch),
>          }
>      }
> @@ -236,6 +245,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => entry.get_node(id),
>              FabricEntry::Ospf(entry) => entry.get_node(id),
>              FabricEntry::WireGuard(entry) => entry.get_node(id),
> +            FabricEntry::Bgp(entry) => entry.get_node(id),
>          }
>      }
>  
> @@ -246,6 +256,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => entry.get_node_mut(id),
>              FabricEntry::Ospf(entry) => entry.get_node_mut(id),
>              FabricEntry::WireGuard(entry) => entry.get_node_mut(id),
> +            FabricEntry::Bgp(entry) => entry.get_node_mut(id),
>          }
>      }
>  
> @@ -392,6 +403,8 @@ impl FabricEntry {
>                                  _ => continue,
>                              }
>                          }
> +
> +                        Ok(())
>                      }
>                      (
>                          WireGuardNode::External(external_wire_guard_node),
> @@ -422,8 +435,48 @@ impl FabricEntry {
>                                  _ => continue,
>                              }
>                          }
> +
> +                        Ok(())
> +                    }
> +                    _ => Err(FabricConfigError::ProtocolMismatch),
> +                }
> +            }
> +            (Node::Bgp(node_section), NodeUpdater::Bgp(updater)) => {
> +                let BgpNode::Internal(ref mut props) = node_section.properties else {
> +                    return Err(FabricConfigError::ProtocolMismatch);
> +                };
> +
> +                let NodeDataUpdater::<BgpNodePropertiesUpdater, BgpNodeDeletableProperties> {
> +                    ip,
> +                    ip6,
> +                    properties: BgpNodePropertiesUpdater { asn, interfaces },
> +                    delete,
> +                } = updater;
> +
> +                if let Some(ip) = ip {
> +                    node_section.ip = Some(ip);
> +                }
> +
> +                if let Some(ip) = ip6 {
> +                    node_section.ip6 = Some(ip);
> +                }
> +
> +                if let Some(asn) = asn {
> +                    props.asn = asn;
> +                }
> +
> +                if let Some(interfaces) = interfaces {
> +                    props.interfaces = interfaces;
> +                }
> +
> +                for property in delete {
> +                    match property {
> +                        NodeDeletableProperties::Ip => node_section.ip = None,
> +                        NodeDeletableProperties::Ip6 => node_section.ip6 = None,
> +                        NodeDeletableProperties::Protocol(
> +                            BgpNodeDeletableProperties::Interfaces,
> +                        ) => props.interfaces = Vec::new(),
>                      }
> -                    _ => return Err(FabricConfigError::ProtocolMismatch),
>                  }
>  
>                  Ok(())
> @@ -438,6 +491,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => entry.nodes.iter(),
>              FabricEntry::Ospf(entry) => entry.nodes.iter(),
>              FabricEntry::WireGuard(entry) => entry.nodes.iter(),
> +            FabricEntry::Bgp(entry) => entry.nodes.iter(),
>          }
>      }
>  
> @@ -447,6 +501,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => entry.delete_node(id),
>              FabricEntry::Ospf(entry) => entry.delete_node(id),
>              FabricEntry::WireGuard(entry) => entry.delete_node(id),
> +            FabricEntry::Bgp(entry) => entry.delete_node(id),
>          }
>      }
>  
> @@ -457,6 +512,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => entry.into_pair(),
>              FabricEntry::Ospf(entry) => entry.into_pair(),
>              FabricEntry::WireGuard(entry) => entry.into_pair(),
> +            FabricEntry::Bgp(entry) => entry.into_pair(),
>          }
>      }
>  
> @@ -466,6 +522,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => &entry.fabric,
>              FabricEntry::Ospf(entry) => &entry.fabric,
>              FabricEntry::WireGuard(entry) => &entry.fabric,
> +            FabricEntry::Bgp(entry) => &entry.fabric,
>          }
>      }
>  
> @@ -475,6 +532,7 @@ impl FabricEntry {
>              FabricEntry::Openfabric(entry) => &mut entry.fabric,
>              FabricEntry::Ospf(entry) => &mut entry.fabric,
>              FabricEntry::WireGuard(entry) => &mut entry.fabric,
> +            FabricEntry::Bgp(entry) => &mut entry.fabric,
>          }
>      }
>  }
> @@ -487,6 +545,7 @@ impl From<Fabric> for FabricEntry {
>              }
>              Fabric::Ospf(fabric_section) => FabricEntry::Ospf(Entry::new(fabric_section)),
>              Fabric::WireGuard(fabric_section) => FabricEntry::WireGuard(Entry::new(fabric_section)),
> +            Fabric::Bgp(fabric_section) => FabricEntry::Bgp(Entry::new(fabric_section)),
>          }
>      }
>  }
> @@ -500,6 +559,8 @@ impl Validatable for FabricEntry {
>      /// - Node IP addresses are within their respective fabric IP prefix ranges
>      /// - IP addresses are unique across all nodes in the fabric
>      /// - Each node passes its own validation checks
> +    /// - For BGP fabrics, derived router-ids are unique across nodes (catches
> +    ///   FNV-1a hash collisions for IPv6-only nodes)
>      fn validate(&self) -> Result<(), FabricConfigError> {
>          let fabric = self.fabric();
>  
> @@ -607,6 +668,27 @@ impl Validatable for FabricEntry {
>              }
>          }
>  
> +        // Per-node IPs are unique by the checks above. Router-ids can still
> +        // collide when at least one node falls back to FNV-1a on its IPv6
> +        // address (the hash is 32 bits wide, so two distinct IPv6 addresses
> +        // can map to the same router-id).
> +        if let FabricEntry::Bgp(bgp_entry) = self {
> +            let mut seen_router_ids: HashMap<std::net::Ipv4Addr, &NodeId> = HashMap::new();
> +            for (node_id, node) in &bgp_entry.nodes {
> +                let Node::Bgp(node_section) = node else {
> +                    continue;
> +                };
> +                if !matches!(node_section.properties(), BgpNode::Internal(_)) {
> +                    continue;
> +                }
> +                if let Some(router_id) = bgp_router_id(node_section) {
> +                    if seen_router_ids.insert(router_id, node_id).is_some() {
> +                        return Err(FabricConfigError::DuplicateBgpRouterId(router_id));
> +                    }
> +                }
> +            }
> +        }
> +
>          fabric.validate()
>      }
>  }
> @@ -712,6 +794,15 @@ impl Validatable for FabricConfig {
>                              }
>                          }
>                      }
> +                    Node::Bgp(node_section) => {
> +                        if let BgpNode::Internal(props) = node_section.properties() {
> +                            if !props.interfaces().all(|interface| {
> +                                node_interfaces.insert((node_id, interface.name().as_str()))
> +                            }) {
> +                                return Err(FabricConfigError::DuplicateInterface);
> +                            }
> +                        }
> +                    }
>                  }
>              }
>  
> @@ -939,6 +1030,80 @@ impl FabricConfig {
>  
>                  Ok(())
>              }
> +            (Fabric::Bgp(fabric_section), FabricUpdater::Bgp(updater)) => {
> +                let FabricSectionUpdater::<BgpPropertiesUpdater, BgpDeletableProperties> {
> +                    ip_prefix,
> +                    ip6_prefix,
> +                    properties:
> +                        BgpPropertiesUpdater {
> +                            bfd,
> +                            redistribute,
> +                            route_map_in,
> +                            route_map_out,
> +                            route_filter,
> +                        },
> +                    delete,
> +                } = updater;
> +
> +                if let Some(prefix) = ip_prefix {
> +                    fabric_section.ip_prefix = Some(prefix);
> +                }
> +
> +                if let Some(prefix) = ip6_prefix {
> +                    fabric_section.ip6_prefix = Some(prefix);
> +                }
> +
> +                if let Some(bfd) = bfd {
> +                    fabric_section.properties.bfd = bfd;
> +                }
> +
> +                if let Some(redistribute) = redistribute {
> +                    fabric_section.properties.redistribute = redistribute;
> +                }
> +
> +                if let Some(route_map_in) = route_map_in {
> +                    fabric_section.properties.route_map_in = Some(route_map_in);
> +                }
> +
> +                if let Some(route_map_out) = route_map_out {
> +                    fabric_section.properties.route_map_out = Some(route_map_out);
> +                }
> +
> +                if let Some(route_filter) = route_filter {
> +                    fabric_section.properties.route_filter = Some(route_filter);
> +                }
> +
> +                for property in delete {
> +                    match property {
> +                        FabricDeletableProperties::IpPrefix => {
> +                            fabric_section.ip_prefix = None;
> +                        }
> +                        FabricDeletableProperties::Ip6Prefix => {
> +                            fabric_section.ip6_prefix = None;
> +                        }
> +                        FabricDeletableProperties::Protocol(
> +                            BgpDeletableProperties::Redistribute,
> +                        ) => {
> +                            fabric_section.properties.redistribute = Vec::new();
> +                        }
> +                        FabricDeletableProperties::Protocol(
> +                            BgpDeletableProperties::RouteFilter,
> +                        ) => {
> +                            fabric_section.properties.route_filter = None;
> +                        }
> +                        FabricDeletableProperties::Protocol(BgpDeletableProperties::RouteMapIn) => {
> +                            fabric_section.properties.route_map_in = None;
> +                        }
> +                        FabricDeletableProperties::Protocol(
> +                            BgpDeletableProperties::RouteMapOut,
> +                        ) => {
> +                            fabric_section.properties.route_map_out = None;
> +                        }
> +                    }
> +                }
> +
> +                Ok(())
> +            }
>              _ => Err(FabricConfigError::ProtocolMismatch),
>          }
>      }
> diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
> index e92074c..efa186a 100644
> --- a/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/section_config/fabric.rs
> @@ -8,6 +8,9 @@ use proxmox_schema::{
>  };
>  
>  use crate::common::valid::Validatable;
> +use crate::sdn::fabric::section_config::protocol::bgp::{
> +    BgpDeletableProperties, BgpProperties, BgpPropertiesUpdater,
> +};
>  use crate::sdn::fabric::section_config::protocol::openfabric::{
>      OpenfabricDeletableProperties, OpenfabricProperties, OpenfabricPropertiesUpdater,
>  };
> @@ -147,6 +150,10 @@ impl UpdaterType for FabricSection<WireGuardProperties> {
>      type Updater = FabricSectionUpdater<WireGuardPropertiesUpdater, WireGuardDeletableProperties>;
>  }
>  
> +impl UpdaterType for FabricSection<BgpProperties> {
> +    type Updater = FabricSectionUpdater<BgpPropertiesUpdater, BgpDeletableProperties>;
> +}
> +
>  /// Enum containing all types of fabrics.
>  ///
>  /// It utilizes [`FabricSection<T>`] to define all possible types of fabrics. For parsing the
> @@ -169,6 +176,7 @@ pub enum Fabric {
>      Ospf(FabricSection<OspfProperties>),
>      #[serde(rename = "wireguard")]
>      WireGuard(FabricSection<WireGuardProperties>),
> +    Bgp(FabricSection<BgpProperties>),
>  }
>  
>  impl UpdaterType for Fabric {
> @@ -184,6 +192,7 @@ impl Fabric {
>              Self::Openfabric(fabric_section) => fabric_section.id(),
>              Self::Ospf(fabric_section) => fabric_section.id(),
>              Self::WireGuard(fabric_section) => fabric_section.id(),
> +            Self::Bgp(fabric_section) => fabric_section.id(),
>          }
>      }
>  
> @@ -195,6 +204,7 @@ impl Fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix(),
>              Fabric::Ospf(fabric_section) => fabric_section.ip_prefix(),
>              Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix(),
> +            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix(),
>          }
>      }
>  
> @@ -206,6 +216,7 @@ impl Fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
>              Fabric::Ospf(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
>              Fabric::WireGuard(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
> +            Fabric::Bgp(fabric_section) => fabric_section.ip_prefix = Some(ipv4_cidr),
>          }
>      }
>  
> @@ -217,6 +228,7 @@ impl Fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix(),
>              Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix(),
>              Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix(),
> +            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix(),
>          }
>      }
>  
> @@ -228,6 +240,7 @@ impl Fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
>              Fabric::Ospf(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
>              Fabric::WireGuard(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
> +            Fabric::Bgp(fabric_section) => fabric_section.ip6_prefix = Some(ipv6_cidr),
>          }
>      }
>  }
> @@ -241,6 +254,7 @@ impl Validatable for Fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.validate(),
>              Fabric::Ospf(fabric_section) => fabric_section.validate(),
>              Fabric::WireGuard(_fabric_section) => Ok(()),
> +            Fabric::Bgp(fabric_section) => fabric_section.validate(),
>          }
>      }
>  }
> @@ -263,6 +277,12 @@ impl From<FabricSection<WireGuardProperties>> for Fabric {
>      }
>  }
>  
> +impl From<FabricSection<BgpProperties>> for Fabric {
> +    fn from(section: FabricSection<BgpProperties>) -> Self {
> +        Fabric::Bgp(section)
> +    }
> +}
> +
>  /// Enum containing all updater types for fabrics
>  #[derive(Debug, Clone, Serialize, Deserialize)]
>  #[serde(rename_all = "snake_case", tag = "protocol")]
> @@ -271,6 +291,7 @@ pub enum FabricUpdater {
>      Ospf(<FabricSection<OspfProperties> as UpdaterType>::Updater),
>      #[serde(rename = "wireguard")]
>      WireGuard(<FabricSection<WireGuardProperties> as UpdaterType>::Updater),
> +    Bgp(<FabricSection<BgpProperties> as UpdaterType>::Updater),
>  }
>  
>  impl Updater for FabricUpdater {
> @@ -279,6 +300,7 @@ impl Updater for FabricUpdater {
>              FabricUpdater::Openfabric(updater) => updater.is_empty(),
>              FabricUpdater::Ospf(updater) => updater.is_empty(),
>              FabricUpdater::WireGuard(updater) => updater.is_empty(),
> +            FabricUpdater::Bgp(updater) => updater.is_empty(),
>          }
>      }
>  }
> diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
> index f47a522..f85c547 100644
> --- a/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/section_config/mod.rs
> @@ -11,6 +11,7 @@ use crate::sdn::fabric::section_config::{
>      fabric::{Fabric, FabricSection, FABRIC_ID_REGEX_STR},
>      node::{Node, NodeSection, NODE_ID_REGEX_STR},
>      protocol::{
> +        bgp::{BgpNode, BgpProperties},
>          openfabric::{OpenfabricNodeProperties, OpenfabricProperties},
>          ospf::{OspfNodeProperties, OspfProperties},
>          wireguard::WireGuardNode,
> @@ -34,9 +35,11 @@ impl From<Section> for FabricOrNode<Fabric, Node> {
>              Section::OpenfabricFabric(fabric_section) => Self::Fabric(fabric_section.into()),
>              Section::OspfFabric(fabric_section) => Self::Fabric(fabric_section.into()),
>              Section::WireGuardFabric(fabric_section) => Self::Fabric(fabric_section.into()),
> +            Section::BgpFabric(fabric_section) => Self::Fabric(fabric_section.into()),
> +            Section::WireGuardNode(node_section) => Self::Node(node_section.into()),
>              Section::OpenfabricNode(node_section) => Self::Node(node_section.into()),
>              Section::OspfNode(node_section) => Self::Node(node_section.into()),
> -            Section::WireGuardNode(node_section) => Self::Node(node_section.into()),
> +            Section::BgpNode(node_section) => Self::Node(node_section.into()),
>          }
>      }
>  }
> @@ -68,10 +71,12 @@ pub enum Section {
>      OspfFabric(FabricSection<OspfProperties>),
>      #[serde(rename = "wireguard_fabric")]
>      WireGuardFabric(FabricSection<WireGuardProperties>),
> +    BgpFabric(FabricSection<BgpProperties>),
>      OpenfabricNode(NodeSection<OpenfabricNodeProperties>),
>      OspfNode(NodeSection<OspfNodeProperties>),
>      #[serde(rename = "wireguard_node")]
>      WireGuardNode(NodeSection<WireGuardNode>),
> +    BgpNode(NodeSection<BgpNode>),
>  }
>  
>  impl From<FabricSection<OpenfabricProperties>> for Section {
> @@ -92,6 +97,12 @@ impl From<FabricSection<WireGuardProperties>> for Section {
>      }
>  }
>  
> +impl From<FabricSection<BgpProperties>> for Section {
> +    fn from(section: FabricSection<BgpProperties>) -> Self {
> +        Self::BgpFabric(section)
> +    }
> +}
> +
>  impl From<NodeSection<OpenfabricNodeProperties>> for Section {
>      fn from(section: NodeSection<OpenfabricNodeProperties>) -> Self {
>          Self::OpenfabricNode(section)
> @@ -110,12 +121,19 @@ impl From<NodeSection<WireGuardNode>> for Section {
>      }
>  }
>  
> +impl From<NodeSection<BgpNode>> for Section {
> +    fn from(section: NodeSection<BgpNode>) -> Self {
> +        Self::BgpNode(section)
> +    }
> +}
> +
>  impl From<Fabric> for Section {
>      fn from(fabric: Fabric) -> Self {
>          match fabric {
>              Fabric::Openfabric(fabric_section) => fabric_section.into(),
>              Fabric::Ospf(fabric_section) => fabric_section.into(),
>              Fabric::WireGuard(fabric_section) => fabric_section.into(),
> +            Fabric::Bgp(fabric_section) => fabric_section.into(),
>          }
>      }
>  }
> @@ -126,6 +144,7 @@ impl From<Node> for Section {
>              Node::Openfabric(node_section) => node_section.into(),
>              Node::Ospf(node_section) => node_section.into(),
>              Node::WireGuard(node_section) => node_section.into(),
> +            Node::Bgp(node_section) => node_section.into(),
>          }
>      }
>  }
> diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
> index f2300ac..af15898 100644
> --- a/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/section_config/node.rs
> @@ -10,6 +10,7 @@ use proxmox_schema::{
>  };
>  
>  use crate::common::valid::Validatable;
> +use crate::sdn::fabric::section_config::protocol::bgp::{BgpNode, BgpNodeProperties};
>  use crate::sdn::fabric::section_config::protocol::wireguard::WireGuardNode;
>  use crate::sdn::fabric::section_config::{
>      fabric::{FabricId, FABRIC_ID_REGEX_STR},
> @@ -191,6 +192,7 @@ pub enum Node {
>      Ospf(NodeSection<OspfNodeProperties>),
>      #[serde(rename = "wireguard")]
>      WireGuard(NodeSection<WireGuardNode>),
> +    Bgp(NodeSection<BgpNode>),
>  }
>  
>  impl Node {
> @@ -200,6 +202,7 @@ impl Node {
>              Node::Openfabric(node_section) => node_section.id(),
>              Node::Ospf(node_section) => node_section.id(),
>              Node::WireGuard(node_section) => node_section.id(),
> +            Node::Bgp(node_section) => node_section.id(),
>          }
>      }
>  
> @@ -209,6 +212,7 @@ impl Node {
>              Node::Openfabric(node_section) => node_section.ip(),
>              Node::Ospf(node_section) => node_section.ip(),
>              Node::WireGuard(node_section) => node_section.ip(),
> +            Node::Bgp(node_section) => node_section.ip(),
>          }
>      }
>  
> @@ -218,6 +222,7 @@ impl Node {
>              Node::Openfabric(node_section) => node_section.ip6(),
>              Node::Ospf(node_section) => node_section.ip6(),
>              Node::WireGuard(node_section) => node_section.ip6(),
> +            Node::Bgp(node_section) => node_section.ip6(),
>          }
>      }
>  }
> @@ -230,6 +235,7 @@ impl Validatable for Node {
>              Node::Openfabric(node_section) => node_section.validate(),
>              Node::Ospf(node_section) => node_section.validate(),
>              Node::WireGuard(node_section) => node_section.validate(),
> +            Node::Bgp(node_section) => node_section.validate(),
>          }
>      }
>  }
> @@ -252,6 +258,12 @@ impl From<NodeSection<WireGuardNode>> for Node {
>      }
>  }
>  
> +impl From<NodeSection<BgpNode>> for Node {
> +    fn from(value: NodeSection<BgpNode>) -> Self {
> +        Self::Bgp(value)
> +    }
> +}
> +
>  /// API types for SDN fabric node configurations.
>  ///
>  /// This module provides specialized types that are used for API interactions when retrieving,
> @@ -273,6 +285,7 @@ pub mod api {
>      use proxmox_schema::{Updater, UpdaterType};
>  
>      use crate::sdn::fabric::section_config::protocol::{
> +        bgp::{BgpNodeDeletableProperties, BgpNodePropertiesUpdater},
>          openfabric::{
>              OpenfabricNodeDeletableProperties, OpenfabricNodeProperties,
>              OpenfabricNodePropertiesUpdater,
> @@ -338,6 +351,7 @@ pub mod api {
>          Ospf(NodeData<OspfNodeProperties>),
>          #[serde(rename = "wireguard")]
>          WireGuard(NodeData<WireGuardNode>),
> +        Bgp(NodeData<BgpNode>),
>      }
>  
>      impl From<super::Node> for Node {
> @@ -346,6 +360,7 @@ pub mod api {
>                  super::Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
>                  super::Node::Ospf(node_section) => Self::Ospf(node_section.into()),
>                  super::Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
> +                super::Node::Bgp(node_section) => Self::Bgp(node_section.into()),
>              }
>          }
>      }
> @@ -356,6 +371,7 @@ pub mod api {
>                  Node::Openfabric(node_section) => Self::Openfabric(node_section.into()),
>                  Node::Ospf(node_section) => Self::Ospf(node_section.into()),
>                  Node::WireGuard(node_section) => Self::WireGuard(node_section.into()),
> +                Node::Bgp(node_section) => Self::Bgp(node_section.into()),
>              }
>          }
>      }
> @@ -373,6 +389,10 @@ pub mod api {
>          type Updater = NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>;
>      }
>  
> +    impl UpdaterType for NodeData<BgpNodeProperties> {
> +        type Updater = NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>;
> +    }
> +
>      #[derive(Debug, Clone, Serialize, Deserialize)]
>      pub struct NodeDataUpdater<T, D> {
>          #[serde(skip_serializing_if = "Option::is_none")]
> @@ -410,6 +430,7 @@ pub mod api {
>          Ospf(NodeDataUpdater<OspfNodePropertiesUpdater, OspfNodeDeletableProperties>),
>          #[serde(rename = "wireguard")]
>          WireGuard(NodeDataUpdater<WireGuardNodeUpdater, WireGuardNodeDeletableProperties>),
> +        Bgp(NodeDataUpdater<BgpNodePropertiesUpdater, BgpNodeDeletableProperties>),
>      }
>  
>      #[derive(Debug, Clone, Serialize, Deserialize)]
> diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
> new file mode 100644
> index 0000000..f6f55e2
> --- /dev/null
> +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/bgp.rs
> @@ -0,0 +1,317 @@
> +use std::net::{Ipv4Addr as StdIpv4Addr, Ipv6Addr};
> +use std::ops::{Deref, DerefMut};
> +
> +use proxmox_network_types::ip_address::api_types::Ipv4Addr;
> +use proxmox_schema::{ApiType, OneOfSchema, Schema, StringSchema, UpdaterType};
> +use serde::{Deserialize, Serialize};
> +
> +use proxmox_schema::{api, property_string::PropertyString, ApiStringFormat, Updater};
> +
> +use crate::common::valid::Validatable;
> +use crate::sdn::fabric::section_config::fabric::FabricSection;
> +use crate::sdn::fabric::section_config::interface::InterfaceName;
> +use crate::sdn::fabric::section_config::node::NodeSection;
> +use crate::sdn::fabric::FabricConfigError;
> +
> +use crate::sdn::prefix_list::PrefixListId;
> +use crate::sdn::route_map::RouteMapId;
> +
> +#[api]
> +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
> +#[serde(rename_all = "lowercase")]
> +/// Redistribution Sources for BGP fabric
> +pub enum BgpRedistributionSource {
> +    /// redistribute connected routes
> +    Connected,
> +    /// redistribute IS-IS routes
> +    Isis,
> +    /// redistribute kernel routes
> +    Kernel,
> +    /// redistribute openfabric routes
> +    Openfabric,
> +    /// redistribute ospfv2 routes
> +    Ospf,
> +    /// redistribute ospfv3 routes
> +    Ospf6,
> +    /// redistribute static routes
> +    Static,
> +}
> +
> +#[api]
> +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
> +/// A BGP redistribution target
> +pub struct BgpRedistribution {
> +    /// The source used for redistribution
> +    pub(crate) source: BgpRedistributionSource,
> +    /// The metric to apply to redistributed routes
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub(crate) metric: Option<u32>,
> +    /// Route MAP to use for filtering redistributed routes
> +    #[serde(rename = "route-map", skip_serializing_if = "Option::is_none")]
> +    pub(crate) route_map: Option<RouteMapId>,
> +}
> +
> +#[api(
> +    type: Integer,
> +    minimum: u32::MIN as i64,
> +    maximum: u32::MAX as i64,
> +)]
> +#[derive(Debug, Clone, Serialize, Updater, Hash)]
> +/// Autonomous system number as defined by RFC 6793
> +pub struct ASN(u32);

potentially something for later, but I think we use ASN in several
places - so we might want to add this to sdn-types instead and reuse it
across the ve-rs crates?

> +impl<'de> Deserialize<'de> for ASN {
> +    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
> +    where
> +        D: serde::Deserializer<'de>,
> +    {
> +        use serde::de::{self, Visitor};
> +
> +        struct AsnVisitor;
> +
> +        impl<'de> Visitor<'de> for AsnVisitor {
> +            type Value = ASN;
> +
> +            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
> +                formatter.write_str("a u32 or string containing a u32")
> +            }
> +
> +            fn visit_i64<E: de::Error>(self, v: i64) -> Result<ASN, E> {
> +                u32::try_from(v)
> +                    .map(ASN)
> +                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
> +            }
> +
> +            fn visit_u64<E: de::Error>(self, v: u64) -> Result<ASN, E> {
> +                u32::try_from(v)
> +                    .map(ASN)
> +                    .map_err(|_| E::custom(format!("ASN out of range: {v}")))
> +            }
> +
> +            fn visit_str<E: de::Error>(self, v: &str) -> Result<ASN, E> {
> +                v.parse::<u32>()
> +                    .map(ASN)
> +                    .map_err(|_| E::custom(format!("invalid ASN: {v}")))
> +            }
> +        }
> +
> +        deserializer.deserialize_any(AsnVisitor)
> +    }
> +}

Is there a reason why proxmox_serde::perl::deserialize_u32 doesn't work?
It should work the same afaict.

> +impl UpdaterType for ASN {
> +    type Updater = Option<ASN>;
> +}
> +
> +impl ASN {
> +    pub fn as_u32(&self) -> u32 {
> +        self.0
> +    }
> +}
> +
> +#[api(
> +    properties: {
> +        redistribute: {
> +            type: Array,
> +            optional: true,
> +            items: {
> +                type: String,
> +                description: "A BGP redistribution source",
> +                format: &ApiStringFormat::PropertyString(&BgpRedistribution::API_SCHEMA),
> +            }
> +        }
> +    },
> +)]
> +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
> +/// Properties for an Bgp fabric.
> +pub struct BgpProperties {
> +    /// enable BFD for this fabric
> +    #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")]
> +    pub(crate) bfd: bool,
> +    /// redistribution configuration for this fabric
> +    #[serde(default, skip_serializing_if = "Vec::is_empty")]
> +    #[updater(serde(skip_serializing_if = "Option::is_none"))]
> +    pub(crate) redistribute: Vec<PropertyString<BgpRedistribution>>,
> +
> +    /// Route map to apply for incoming routes
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub(crate) route_map_in: Option<RouteMapId>,
> +
> +    /// Route map to apply for outgoing routes
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub(crate) route_map_out: Option<RouteMapId>,
> +
> +    /// By default only routes from the configured IP prefix are imported
> +    /// into the local routing table. This setting can be used to override the
> +    /// allowed IPs and import additional routes besides the configured IP
> +    /// prefix.
> +    #[serde(skip_serializing_if = "Option::is_none")]
> +    pub(crate) route_filter: Option<PrefixListId>,
> +}
> +
> +impl BgpProperties {
> +    pub fn bfd(&self) -> bool {
> +        self.bfd
> +    }
> +}
> +
> +impl Validatable for FabricSection<BgpProperties> {
> +    type Error = FabricConfigError;
> +
> +    /// Validate the [`FabricSection<BgpProperties>`].
> +    fn validate(&self) -> Result<(), Self::Error> {
> +        if self.ip_prefix().is_none() && self.ip6_prefix().is_none() {
> +            return Err(FabricConfigError::FabricNoIpPrefix(self.id().to_string()));
> +        }
> +
> +        Ok(())
> +    }
> +}
> +
> +#[derive(Debug, Clone, Serialize, Deserialize)]
> +#[serde(rename_all = "snake_case")]
> +pub enum BgpDeletableProperties {
> +    Redistribute,
> +    RouteFilter,
> +    RouteMapIn,
> +    RouteMapOut,
> +}
> +
> +#[api]
> +/// External Bgp Node
> +#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
> +pub struct ExternalBgpNode {
> +    peer_ip: Option<Ipv4Addr>,
> +}
> +
> +#[derive(Debug, Clone, Serialize, Deserialize, Hash)]
> +#[serde(rename_all = "snake_case", tag = "role")]
> +pub enum BgpNode {
> +    Internal(BgpNodeProperties),
> +    External(ExternalBgpNode),
> +}
> +
> +impl ApiType for BgpNode {
> +    const API_SCHEMA: Schema = OneOfSchema::new(
> +        "BGP node",
> +        &(
> +            "role",
> +            false,
> +            &StringSchema::new("internal or external").schema(),
> +        ),
> +        &[
> +            ("external", &ExternalBgpNode::API_SCHEMA),
> +            ("internal", &BgpNodeProperties::API_SCHEMA),
> +        ],
> +    )
> +    .schema();
> +}
> +
> +impl Validatable for NodeSection<BgpNode> {
> +    type Error = FabricConfigError;
> +
> +    fn validate(&self) -> Result<(), Self::Error> {
> +        Ok(())
> +    }
> +}
> +
> +#[api(
> +    properties: {
> +        interfaces: {
> +            type: Array,
> +            optional: true,
> +            items: {
> +                type: String,
> +                description: "Properties for an Bgp interface.",
> +                format: &ApiStringFormat::PropertyString(&BgpInterfaceProperties::API_SCHEMA),
> +            }
> +        },
> +    }
> +)]
> +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
> +/// Properties for an Bgp node.
> +pub struct BgpNodeProperties {
> +    /// Autonomous system number for this Node
> +    pub(crate) asn: ASN,
> +    /// Interfaces for this Node.
> +    #[serde(default)]
> +    pub(crate) interfaces: Vec<PropertyString<BgpInterfaceProperties>>,
> +}
> +
> +impl BgpNodeProperties {
> +    /// Returns the ASN for this node.
> +    pub fn asn(&self) -> &ASN {
> +        &self.asn
> +    }
> +
> +    /// Returns an iterator over all the interfaces.
> +    pub fn interfaces(&self) -> impl Iterator<Item = &BgpInterfaceProperties> {
> +        self.interfaces
> +            .iter()
> +            .map(|property_string| property_string.deref())
> +    }
> +
> +    /// Returns an iterator over all the interfaces (mutable).
> +    pub fn interfaces_mut(&mut self) -> impl Iterator<Item = &mut BgpInterfaceProperties> {
> +        self.interfaces
> +            .iter_mut()
> +            .map(|property_string| property_string.deref_mut())
> +    }
> +}
> +
> +impl Validatable for NodeSection<BgpNodeProperties> {
> +    type Error = FabricConfigError;
> +
> +    /// Validate the [`NodeSection<BgpNodeProperties>`].
> +    fn validate(&self) -> Result<(), Self::Error> {
> +        Ok(())
> +    }
> +}
> +
> +#[derive(Debug, Clone, Serialize, Deserialize)]
> +#[serde(rename_all = "snake_case", untagged)]
> +pub enum BgpNodeDeletableProperties {
> +    Interfaces,
> +}
> +
> +#[api]
> +#[derive(Debug, Clone, Serialize, Deserialize, Updater, Hash)]
> +/// Properties for an BGP interface.
> +pub struct BgpInterfaceProperties {
> +    pub(crate) name: InterfaceName,
> +}
> +
> +impl BgpInterfaceProperties {
> +    /// Get the name of the BGP interface.
> +    pub fn name(&self) -> &InterfaceName {
> +        &self.name
> +    }
> +
> +    /// Set the name of the interface.
> +    pub fn set_name(&mut self, name: InterfaceName) {
> +        self.name = name
> +    }
> +}
> +
> +/// Derive a deterministic BGP router-id from an IPv6 address using FNV-1a.
> +///
> +/// BGP router-id must be a 32-bit value. For IPv6-only nodes, we hash the
> +/// full 16 octets down to 4 bytes. Typical loopback allocations (sequential
> +/// within a prefix, sparse across /48s) produce zero collisions up to 100k
> +/// nodes in testing -- well below the random birthday bound (~1% at 10k)
> +/// because structured addresses spread well under FNV-1a.
> +pub fn router_id_from_ipv6(addr: &Ipv6Addr) -> StdIpv4Addr {
> +    let mut hash: u32 = 0x811c9dc5;
> +    for &byte in &addr.octets() {
> +        hash ^= byte as u32;
> +        hash = hash.wrapping_mul(0x01000193);
> +    }
> +    StdIpv4Addr::from(hash)
> +}
> +
> +/// Resolves the BGP router-id for a node: the IPv4 address if set,
> +/// otherwise an FNV-1a hash of the IPv6 address.
> +pub fn bgp_router_id(node: &NodeSection<BgpNode>) -> Option<StdIpv4Addr> {
> +    node.ip()
> +        .or_else(|| node.ip6().map(|ipv6| router_id_from_ipv6(&ipv6)))
> +}
> diff --git a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
> index fd77426..c7adf0f 100644
> --- a/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
> +++ b/proxmox-ve-config/src/sdn/fabric/section_config/protocol/mod.rs
> @@ -1,3 +1,4 @@
> +pub mod bgp;
>  pub mod openfabric;
>  pub mod ospf;
>  pub mod wireguard;
> diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
> new file mode 100644
> index 0000000..bd434a7
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_default/fabrics.cfg
> @@ -0,0 +1,17 @@
> +bgp_fabric: test
> +        bfd 0
> +        ip_prefix 10.10.10.0/24
> +
> +bgp_node: test_pve
> +        asn 65001
> +        interfaces name=ens18
> +        interfaces name=ens19
> +        ip 10.10.10.1
> +        role internal
> +
> +bgp_node: test_pve1
> +        asn 65002
> +        interfaces name=ens19
> +        ip 10.10.10.2
> +        role internal
> +
> diff --git a/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
> new file mode 100644
> index 0000000..f4581fb
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/cfg/bgp_ipv6_only/fabrics.cfg
> @@ -0,0 +1,17 @@
> +bgp_fabric: test
> +        bfd 0
> +        ip6_prefix fd00:10::/64
> +
> +bgp_node: test_pve
> +        asn 65001
> +        interfaces name=ens18
> +        interfaces name=ens19
> +        ip6 fd00:10::1
> +        role internal
> +
> +bgp_node: test_pve1
> +        asn 65002
> +        interfaces name=ens19
> +        ip6 fd00:10::2
> +        role internal
> +
> diff --git a/proxmox-ve-config/tests/fabric/main.rs b/proxmox-ve-config/tests/fabric/main.rs
> index 95b2e62..49c5fcc 100644
> --- a/proxmox-ve-config/tests/fabric/main.rs
> +++ b/proxmox-ve-config/tests/fabric/main.rs
> @@ -1,7 +1,9 @@
>  #![cfg(feature = "frr")]
> +use std::net::Ipv4Addr;
>  use std::str::FromStr;
>  
> -use proxmox_frr::ser::{serializer::dump, FrrConfig};
> +use proxmox_frr::ser::bgp::{AddressFamilies, BgpRouter, CommonAddressFamilyOptions, L2vpnEvpnAF};
> +use proxmox_frr::ser::{serializer::dump, FrrConfig, VrfName};
>  use proxmox_ve_config::sdn::fabric::{
>      frr::build_fabric, section_config::node::NodeId, FabricConfig,
>  };
> @@ -162,3 +164,118 @@ fn openfabric_ipv6_only() {
>  
>      insta::assert_snapshot!(helper::reference_name!("pve"), output);
>  }
> +
> +#[test]
> +fn bgp_default() {
> +    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
> +    let mut frr_config = FrrConfig::default();
> +
> +    build_fabric(
> +        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
> +        config.clone(),
> +        &mut frr_config,
> +    )
> +    .unwrap();
> +
> +    let mut output = dump(&frr_config).expect("error dumping stuff");
> +
> +    insta::assert_snapshot!(helper::reference_name!("pve"), output);
> +
> +    frr_config = FrrConfig::default();
> +    build_fabric(
> +        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
> +        config,
> +        &mut frr_config,
> +    )
> +    .unwrap();
> +
> +    output = dump(&frr_config).expect("error dumping stuff");
> +
> +    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
> +}
> +
> +#[test]
> +fn bgp_ipv6_only() {
> +    let config = FabricConfig::parse_section_config(helper::get_fabrics_config!()).unwrap();
> +    let mut frr_config = FrrConfig::default();
> +
> +    build_fabric(
> +        NodeId::from_string("pve".to_owned()).expect("invalid nodeid"),
> +        config.clone(),
> +        &mut frr_config,
> +    )
> +    .unwrap();
> +
> +    let mut output = dump(&frr_config).expect("error dumping stuff");
> +
> +    insta::assert_snapshot!(helper::reference_name!("pve"), output);
> +
> +    frr_config = FrrConfig::default();
> +    build_fabric(
> +        NodeId::from_string("pve1".to_owned()).expect("invalid nodeid"),
> +        config,
> +        &mut frr_config,
> +    )
> +    .unwrap();
> +
> +    output = dump(&frr_config).expect("error dumping stuff");
> +
> +    insta::assert_snapshot!(helper::reference_name!("pve1"), output);
> +}
> +
> +/// Test that build_fabric merges into an existing EVPN router and sets local-as
> +/// when the ASNs differ.
> +#[test]
> +fn bgp_merge_with_evpn() {
> +    let raw = std::fs::read_to_string("tests/fabric/cfg/bgp_default/fabrics.cfg")
> +        .expect("cannot find config file");
> +    let config = FabricConfig::parse_section_config(&raw).unwrap();
> +
> +    // Pre-populate with an EVPN-like router using a different ASN
> +    let mut frr_config = FrrConfig::default();
> +    let evpn_router = BgpRouter {
> +        asn: 65000,
> +        router_id: Ipv4Addr::new(10, 10, 10, 1),
> +        coalesce_time: Some(1000),
> +        default_ipv4_unicast: Some(false),
> +        hard_administrative_reset: None,
> +        graceful_restart_notification: None,
> +        disable_ebgp_connected_route_check: None,
> +        bestpath_as_path_multipath_relax: None,
> +        neighbor_groups: Vec::new(),
> +        address_families: AddressFamilies {
> +            ipv4_unicast: None,
> +            ipv6_unicast: None,
> +            l2vpn_evpn: Some(L2vpnEvpnAF {
> +                common_options: CommonAddressFamilyOptions {
> +                    import_vrf: Vec::new(),
> +                    neighbors: Vec::new(),
> +                    custom_frr_config: Vec::new(),
> +                },
> +                advertise_all_vni: Some(true),
> +                advertise_default_gw: None,
> +                default_originate: Vec::new(),
> +                advertise_ipv4_unicast: None,
> +                advertise_ipv6_unicast: None,
> +                autort_as: None,
> +                route_targets: None,
> +            }),
> +        },
> +        custom_frr_config: Vec::new(),
> +    };
> +    frr_config
> +        .bgp
> +        .vrf_router
> +        .insert(VrfName::Default, evpn_router);
> +
> +    build_fabric(
> +        NodeId::from_str("pve").expect("invalid nodeid"),
> +        config,
> +        &mut frr_config,
> +    )
> +    .unwrap();
> +
> +    let output = dump(&frr_config).expect("error dumping stuff");
> +
> +    insta::assert_snapshot!(helper::reference_name!("pve"), output);
> +}
> diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
> new file mode 100644
> index 0000000..0db0034
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve.snap
> @@ -0,0 +1,36 @@
> +---
> +source: proxmox-ve-config/tests/fabric/main.rs
> +expression: output
> +---
> +!
> +router bgp 65001
> + bgp router-id 10.10.10.1
> + no bgp default ipv4-unicast
> + neighbor test peer-group
> + neighbor test remote-as external
> + neighbor ens18 interface peer-group test
> + neighbor ens19 interface peer-group test
> + !
> + address-family ipv4 unicast
> +  network 10.10.10.1/32
> +  neighbor test activate
> +  neighbor test soft-reconfiguration inbound
> +  neighbor test route-map pve_bgp_test_in in
> + exit-address-family
> +exit
> +!
> +access-list pve_bgp_test_ips permit 10.10.10.0/24
> +!
> +route-map pve_bgp permit 100
> + match ip address pve_bgp_test_ips
> + set src 10.10.10.1
> +exit
> +!
> +route-map pve_bgp permit 65535
> +exit
> +!
> +route-map pve_bgp_test_in permit 10
> + match ip address pve_bgp_test_ips
> +exit
> +!
> +ip protocol bgp route-map pve_bgp
> diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
> new file mode 100644
> index 0000000..d7ed018
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_default_pve1.snap
> @@ -0,0 +1,35 @@
> +---
> +source: proxmox-ve-config/tests/fabric/main.rs
> +expression: output
> +---
> +!
> +router bgp 65002
> + bgp router-id 10.10.10.2
> + no bgp default ipv4-unicast
> + neighbor test peer-group
> + neighbor test remote-as external
> + neighbor ens19 interface peer-group test
> + !
> + address-family ipv4 unicast
> +  network 10.10.10.2/32
> +  neighbor test activate
> +  neighbor test soft-reconfiguration inbound
> +  neighbor test route-map pve_bgp_test_in in
> + exit-address-family
> +exit
> +!
> +access-list pve_bgp_test_ips permit 10.10.10.0/24
> +!
> +route-map pve_bgp permit 100
> + match ip address pve_bgp_test_ips
> + set src 10.10.10.2
> +exit
> +!
> +route-map pve_bgp permit 65535
> +exit
> +!
> +route-map pve_bgp_test_in permit 10
> + match ip address pve_bgp_test_ips
> +exit
> +!
> +ip protocol bgp route-map pve_bgp
> diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
> new file mode 100644
> index 0000000..8dbb36b
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve.snap
> @@ -0,0 +1,37 @@
> +---
> +source: proxmox-ve-config/tests/fabric/main.rs
> +expression: output
> +---
> +!
> +router bgp 65001
> + bgp router-id 5.76.46.251
> + no bgp default ipv4-unicast
> + neighbor test peer-group
> + neighbor test remote-as external
> + neighbor ens18 interface peer-group test
> + neighbor ens19 interface peer-group test
> + !
> + address-family ipv6 unicast
> +  network fd00:10::1/128
> +  neighbor test activate
> +  neighbor test soft-reconfiguration inbound
> +  neighbor test route-map pve_bgp6_test_in in
> + exit-address-family
> +exit
> +!
> +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
> +!
> +route-map pve_bgp6 permit 100
> + match ipv6 address pve_bgp_test_ip6s
> + set src fd00:10::1
> +exit
> +!
> +route-map pve_bgp6 permit 65535
> +exit
> +!
> +route-map pve_bgp6_test_in permit 10
> + match ipv6 address pve_bgp_test_ip6s
> +exit
> +!
> +!
> +ipv6 protocol bgp route-map pve_bgp6
> diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
> new file mode 100644
> index 0000000..a091148
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_ipv6_only_pve1.snap
> @@ -0,0 +1,36 @@
> +---
> +source: proxmox-ve-config/tests/fabric/main.rs
> +expression: output
> +---
> +!
> +router bgp 65002
> + bgp router-id 6.76.48.142
> + no bgp default ipv4-unicast
> + neighbor test peer-group
> + neighbor test remote-as external
> + neighbor ens19 interface peer-group test
> + !
> + address-family ipv6 unicast
> +  network fd00:10::2/128
> +  neighbor test activate
> +  neighbor test soft-reconfiguration inbound
> +  neighbor test route-map pve_bgp6_test_in in
> + exit-address-family
> +exit
> +!
> +ipv6 access-list pve_bgp_test_ip6s permit fd00:10::/64
> +!
> +route-map pve_bgp6 permit 100
> + match ipv6 address pve_bgp_test_ip6s
> + set src fd00:10::2
> +exit
> +!
> +route-map pve_bgp6 permit 65535
> +exit
> +!
> +route-map pve_bgp6_test_in permit 10
> + match ipv6 address pve_bgp_test_ip6s
> +exit
> +!
> +!
> +ipv6 protocol bgp route-map pve_bgp6
> diff --git a/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
> new file mode 100644
> index 0000000..226337f
> --- /dev/null
> +++ b/proxmox-ve-config/tests/fabric/snapshots/fabric__bgp_merge_with_evpn_pve.snap
> @@ -0,0 +1,42 @@
> +---
> +source: proxmox-ve-config/tests/fabric/main.rs
> +expression: output
> +---
> +!
> +router bgp 65000
> + bgp router-id 10.10.10.1
> + no bgp default ipv4-unicast
> + coalesce-time 1000
> + neighbor test peer-group
> + neighbor test remote-as external
> + neighbor test local-as 65001 no-prepend replace-as
> + neighbor ens18 interface peer-group test
> + neighbor ens19 interface peer-group test
> + !
> + address-family ipv4 unicast
> +  network 10.10.10.1/32
> +  neighbor test activate
> +  neighbor test soft-reconfiguration inbound
> +  neighbor test route-map pve_bgp_test_in in
> + exit-address-family
> + !
> + address-family l2vpn evpn
> +  advertise-all-vni
> + exit-address-family
> +exit
> +!
> +access-list pve_bgp_test_ips permit 10.10.10.0/24
> +!
> +route-map pve_bgp permit 100
> + match ip address pve_bgp_test_ips
> + set src 10.10.10.1
> +exit
> +!
> +route-map pve_bgp permit 65535
> +exit
> +!
> +route-map pve_bgp_test_in permit 10
> + match ip address pve_bgp_test_ips
> +exit
> +!
> +ip protocol bgp route-map pve_bgp





  reply	other threads:[~2026-05-13 12:29 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-12 14:12 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Hannes Laimer
2026-05-12 14:12 ` [PATCH proxmox-ve-rs v4 1/7] sdn: fabric: add BGP protocol support Hannes Laimer
2026-05-13 12:29   ` Stefan Hanreich [this message]
2026-05-13 13:00     ` Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 2/7] sdn: fabrics: add BGP config generation Hannes Laimer
2026-05-12 14:13 ` [PATCH proxmox-perl-rs v4 3/7] sdn: fabrics: add BGP status endpoints Hannes Laimer
2026-05-13 12:33   ` Stefan Hanreich
2026-05-13 13:02     ` Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 4/7] sdn: fabrics: register bgp as a fabric protocol type Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-network v4 5/7] test: evpn: add integration test for EVPN over BGP fabric Hannes Laimer
2026-05-12 14:13 ` [PATCH pve-manager v4 6/7] ui: sdn: add BGP fabric support Hannes Laimer
2026-05-13 12:38   ` Stefan Hanreich
2026-05-12 14:13 ` [PATCH pve-docs v4 7/7] sdn: add bgp fabric section Hannes Laimer
2026-05-13 12:39 ` [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v4 0/7] sdn: add BGP fabric Stefan Hanreich
2026-05-13 18:43 ` superseded: " Hannes Laimer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=ebd653ed-cfb2-4a9d-b131-7027f7e2b481@proxmox.com \
    --to=s.hanreich@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal