From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 9BA161FF137 for ; Tue, 03 Feb 2026 17:05:33 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id D8A0826E18; Tue, 3 Feb 2026 17:04:06 +0100 (CET) From: Gabriel Goller To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox-ve-rs 8/9] frr: add bgp support with templates and serialization Date: Tue, 3 Feb 2026 17:01:15 +0100 Message-ID: <20260203160246.353351-9-g.goller@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260203160246.353351-1-g.goller@proxmox.com> References: <20260203160246.353351-1-g.goller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770134496778 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.003 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 6T7BAWTYUNTYCASTKZS52AGCQA73THOB X-Message-ID-Hash: 6T7BAWTYUNTYCASTKZS52AGCQA73THOB X-MailFrom: g.goller@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Implements bgp routing configuration (rust types and templates) including routers, neighbor groups, address families (IPv4/IPv6 unicast, L2VPN EVPN), VRFs, route redistribution, and prefix lists. Co-authored-by: Stefan Hanreich Signed-off-by: Gabriel Goller --- .../templates/bgp_router.jinja | 118 +++++++++++ proxmox-frr-templates/templates/bgpd.jinja | 35 ++++ .../templates/frr.conf.jinja | 3 + .../templates/ip_routes.jinja | 8 + .../templates/prefix_lists.jinja | 6 + proxmox-frr/src/ser/bgp.rs | 184 ++++++++++++++++++ proxmox-frr/src/ser/mod.rs | 34 +++- proxmox-frr/src/ser/serializer.rs | 12 ++ 8 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 proxmox-frr-templates/templates/bgp_router.jinja create mode 100644 proxmox-frr-templates/templates/bgpd.jinja create mode 100644 proxmox-frr-templates/templates/ip_routes.jinja create mode 100644 proxmox-frr-templates/templates/prefix_lists.jinja create mode 100644 proxmox-frr/src/ser/bgp.rs diff --git a/proxmox-frr-templates/templates/bgp_router.jinja b/proxmox-frr-templates/templates/bgp_router.jinja new file mode 100644 index 000000000000..6f48d6ca17a4 --- /dev/null +++ b/proxmox-frr-templates/templates/bgp_router.jinja @@ -0,0 +1,118 @@ +{% macro address_family_common(common_address_family) -%} +{% for vrf in common_address_family.import_vrf %} + import vrf {{ vrf }} +{% endfor %} +{% for neighbor in common_address_family.neighbors %} + neighbor {{ neighbor.name }} activate + {% if neighbor.soft_reconfiguration_inbound %} + neighbor {{ neighbor.name }} soft-reconfiguration inbound + {% endif %} + {% if neighbor.route_map_in %} + neighbor {{ neighbor.name }} route-map {{ neighbor.route_map_in }} in + {% endif %} + {% if neighbor.route_map_out %} + neighbor {{ neighbor.name }} route-map {{ neighbor.route_map_out }} out + {% endif %} +{% endfor -%} +{% for line in common_address_family.custom_frr_config %} +{{ line }} +{% endfor -%} +{% endmacro -%} +{% macro bgp_router(router_config) %} + bgp router-id {{ router_config.router_id }} + no bgp hard-administrative-reset +{% if router_config.default_ipv4_unicast == false %} + no bgp default ipv4-unicast +{% endif %} +{% if router_config.coalesce_time %} + coalesce-time {{ router_config.coalesce_time }} +{% endif %} + no bgp graceful-restart notification +{% if router_config.disable_ebgp_connected_route_check %} + bgp disable-ebgp-connected-route-check +{% endif %} +{% if router_config.bestpath_as_path_multipath_relax %} + bgp bestpath as-path multipath-relax +{% endif %} +{% for neighbor_group in router_config.neighbor_groups %} + neighbor {{ neighbor_group.name }} peer-group + neighbor {{ neighbor_group.name }} remote-as {{ neighbor_group.remote_as }} +{% if neighbor_group.bfd %} + neighbor {{ neighbor_group.name }} bfd +{% endif %} +{% if neighbor_group.ebgp_multihop %} + neighbor {{ neighbor_group.name }} ebgp-multihop {{ neighbor_group.ebgp_multihop }} +{% endif %} +{% if neighbor_group.update_source %} + neighbor {{ neighbor_group.name }} update-source {{ neighbor_group.update_source }} +{% endif %} +{% for ip in neighbor_group.ips %} + neighbor {{ ip }} peer-group {{ neighbor_group.name }} +{% endfor %} +{% for interface in neighbor_group.interfaces %} + neighbor {{ interface }} interface peer-group {{ neighbor_group.name }} +{% endfor %} +{% endfor %} +{% for line in router_config.custom_frr_config %} +{{ line }} +{% endfor %} +{% if router_config.address_families.ipv4_unicast %} + ! + address-family ipv4 unicast +{% for network in router_config.address_families.ipv4_unicast.networks %} + network {{ network }} +{% endfor %} +{{ address_family_common(router_config.address_families.ipv4_unicast) -}} +{% for redistribute in router_config.address_families.ipv4_unicast.redistribute %} + redistribute {{ redistribute.protocol }}{{ (" metric " ~ redistribute.metric) if redistribute.metric }}{{ (" route-map " ~ redistribute.route_map) if redistribute.route_map }} +{% endfor %} + exit-address-family +{% endif %} +{% if router_config.address_families.ipv6_unicast %} + ! + address-family ipv6 unicast +{% for network in router_config.address_families.ipv6_unicast.networks %} + network {{ network }} +{% endfor %} +{{ address_family_common(router_config.address_families.ipv6_unicast) -}} +{% for redistribute in router_config.address_families.ipv6_unicast.redistribute %} + redistribute {{ redistribute.protocol }}{{ (" metric " ~ redistribute.metric) if redistribute.metric }}{{ (" route-map " ~ redistribute.route_map) if redistribute.route_map }} +{% endfor %} + exit-address-family +{% endif %} +{% if router_config.address_families.l2vpn_evpn %} + ! + address-family l2vpn evpn +{{ address_family_common(router_config.address_families.l2vpn_evpn) -}} +{% if router_config.address_families.l2vpn_evpn.advertise_all_vni %} + advertise-all-vni +{% endif %} +{% if router_config.address_families.l2vpn_evpn.advertise_default_gw %} + advertise-default-gw +{% endif %} +{% if router_config.address_families.l2vpn_evpn.autort_as %} + autort as {{ router_config.address_families.l2vpn_evpn.autort_as }} +{% endif %} +{% for default_originate in router_config.address_families.l2vpn_evpn.default_originate %} + default-originate {{ default_originate }} +{% endfor %} +{% if router_config.address_families.l2vpn_evpn.advertise_ipv4_unicast %} + advertise ipv4 unicast +{% endif %} +{% if router_config.address_families.l2vpn_evpn.advertise_ipv6_unicast %} + advertise ipv6 unicast +{% endif %} +{% if router_config.address_families.l2vpn_evpn.route_targets %} +{% for import in router_config.address_families.l2vpn_evpn.route_targets.import %} + route-target import {{ import }} +{% endfor %} +{% for export in router_config.address_families.l2vpn_evpn.route_targets.export %} + route-target export {{ export }} +{% endfor %} +{% for both in router_config.address_families.l2vpn_evpn.route_targets.both %} + route-target both {{ both }} +{% endfor %} +{% endif %} + exit-address-family +{% endif %} +{% endmacro -%} diff --git a/proxmox-frr-templates/templates/bgpd.jinja b/proxmox-frr-templates/templates/bgpd.jinja new file mode 100644 index 000000000000..cdd0a50abf8c --- /dev/null +++ b/proxmox-frr-templates/templates/bgpd.jinja @@ -0,0 +1,35 @@ +{% from "bgp_router.jinja" import bgp_router %} +{% for vrf_name, vrf in bgp.vrfs|items %} +! +vrf {{ vrf_name }} +{% if vrf.vni %} + vni {{ vrf.vni }} +{% for ip_route in vrf.ip_routes %} +{% if ip_route.vrf %} + {{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} {{ ip_route.vrf }} +{% else %} + {{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} +{% endif %} +{% endfor %} +{% endif %} +{% for line in vrf.custom_frr_config %} +{{ line }} +{% endfor %} +exit-vrf +{% endfor %} +{% for vrf_name, router_config in bgp.vrf_router|items %} +! +{% if vrf_name == "default" %} +router bgp {{ router_config.asn }} +{% else %} +router bgp {{ router_config.asn }} vrf {{ vrf_name }} +{% endif %} +{{ bgp_router(router_config) -}} +exit +{% endfor %} +{% for view_id, router_config in bgp.view_router|items %} +! +router bgp {{ router_config.asn }} view {{ view_id }} +{{ bgp_router(router_config) -}} +exit +{% endfor %} diff --git a/proxmox-frr-templates/templates/frr.conf.jinja b/proxmox-frr-templates/templates/frr.conf.jinja index 6d60ad2a4c4c..f9ca85890710 100644 --- a/proxmox-frr-templates/templates/frr.conf.jinja +++ b/proxmox-frr-templates/templates/frr.conf.jinja @@ -1,8 +1,11 @@ +{% include "bgpd.jinja" %} {% include "fabricd.jinja" %} {% include "isisd.jinja" %} {% include "ospfd.jinja" %} {% include "access_lists.jinja" %} +{% include "prefix_lists.jinja" %} {% include "route_maps.jinja" %} +{% include "ip_routes.jinja" %} {% include "protocol_routemaps.jinja" %} {% for line in custom_frr_config %} {{ line }} diff --git a/proxmox-frr-templates/templates/ip_routes.jinja b/proxmox-frr-templates/templates/ip_routes.jinja new file mode 100644 index 000000000000..3e33a709e821 --- /dev/null +++ b/proxmox-frr-templates/templates/ip_routes.jinja @@ -0,0 +1,8 @@ +{% for ip_route in ip_routes %} +! +{% if ip_route.vrf %} +{{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} {{ ip_route.vrf }} +{% else %} +{{ "ipv6" if ip_route.is_ipv6 else "ip" }} route {{ ip_route.prefix }} {{ ip_route.via }} +{% endif %} +{% endfor %} diff --git a/proxmox-frr-templates/templates/prefix_lists.jinja b/proxmox-frr-templates/templates/prefix_lists.jinja new file mode 100644 index 000000000000..f431958af354 --- /dev/null +++ b/proxmox-frr-templates/templates/prefix_lists.jinja @@ -0,0 +1,6 @@ +{% for name, prefix_list in prefix_lists | items %} +! +{% for rule in prefix_list %} +{{ "ipv6" if rule.is_ipv6 else "ip" }} prefix-list {{ name }} {{ ("seq " ~ rule.seq ~ " ") if rule.seq }}{{ rule.action }} {{ rule.network }}{{ (" le " ~ rule.le) if rule.le }}{{ (" ge " ~ rule.ge) if rule.ge }} +{% endfor %} +{% endfor %} diff --git a/proxmox-frr/src/ser/bgp.rs b/proxmox-frr/src/ser/bgp.rs new file mode 100644 index 000000000000..3ac014686339 --- /dev/null +++ b/proxmox-frr/src/ser/bgp.rs @@ -0,0 +1,184 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use bon::Builder; +use proxmox_network_types::ip_address::{Ipv4Cidr, Ipv6Cidr}; +use serde::{Deserialize, Serialize}; + +use crate::ser::route_map::RouteMapName; +use crate::ser::{FrrWord, InterfaceName, IpRoute}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct BgpRouterName { + asn: u32, + vrf: Option, +} + +impl BgpRouterName { + pub fn new(asn: u32, vrf: Option) -> Self { + Self { asn, vrf } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum NeighborRemoteAs { + Internal, + External, + #[serde(untagged)] + Asn(#[serde(deserialize_with = "proxmox_serde::perl::deserialize_u32")] u32), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct NeighborGroup { + pub name: FrrWord, + #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub bfd: bool, + pub local_as: Option, + pub remote_as: NeighborRemoteAs, + #[serde(default)] + pub ips: Vec, + #[serde(default)] + pub interfaces: Vec, + pub ebgp_multihop: Option, + pub update_source: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct Ipv4UnicastAF { + #[serde(flatten)] + pub common_options: CommonAddressFamilyOptions, + #[serde(default)] + pub networks: Vec, + #[serde(default)] + pub redistribute: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct Ipv6UnicastAF { + #[serde(flatten)] + pub common_options: CommonAddressFamilyOptions, + #[serde(default)] + pub networks: Vec, + #[serde(default)] + pub redistribute: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct L2vpnEvpnAF { + #[serde(flatten)] + pub common_options: CommonAddressFamilyOptions, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub advertise_all_vni: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub advertise_default_gw: Option, + #[serde(default)] + pub default_originate: Vec, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub advertise_ipv4_unicast: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub advertise_ipv6_unicast: Option, + pub autort_as: Option, + pub route_targets: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum DefaultOriginate { + Ipv4, + Ipv6, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RedistributeProtocol { + Connected, + Static, + Ospf, + Kernel, + Isis, + Ospf6, + Openfabric, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct Redistribution { + pub protocol: RedistributeProtocol, + pub metric: Option, + pub route_map: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct RouteTargets { + #[serde(default)] + import: Vec, + #[serde(default)] + export: Vec, + #[serde(default)] + both: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct AddressFamilyNeighbor { + pub name: String, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "proxmox_serde::perl::deserialize_bool" + )] + pub soft_reconfiguration_inbound: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub route_map_in: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub route_map_out: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct CommonAddressFamilyOptions { + #[serde(default)] + pub import_vrf: Vec, + #[serde(default)] + pub neighbors: Vec, + #[serde(default)] + pub custom_frr_config: Vec, +} + +#[derive( + Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize, Default, +)] +pub struct AddressFamilies { + #[serde(skip_serializing_if = "Option::is_none")] + ipv4_unicast: Option, + #[serde(skip_serializing_if = "Option::is_none")] + ipv6_unicast: Option, + #[serde(skip_serializing_if = "Option::is_none")] + l2vpn_evpn: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct Vrf { + pub vni: Option, + #[serde(default)] + pub ip_routes: Vec, + #[serde(default)] + pub custom_frr_config: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Builder, Deserialize)] +pub struct BgpRouter { + pub asn: u32, + pub router_id: Ipv4Addr, + #[serde(default)] + pub coalesce_time: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub default_ipv4_unicast: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub disable_ebgp_connected_route_check: Option, + #[serde(default, deserialize_with = "proxmox_serde::perl::deserialize_bool")] + pub bestpath_as_path_multipath_relax: Option, + #[serde(default)] + pub neighbor_groups: Vec, + #[serde(default)] + pub address_families: AddressFamilies, + #[serde(default)] + pub custom_frr_config: Vec, +} diff --git a/proxmox-frr/src/ser/mod.rs b/proxmox-frr/src/ser/mod.rs index 3baa0a318fb0..f3578ef1323a 100644 --- a/proxmox-frr/src/ser/mod.rs +++ b/proxmox-frr/src/ser/mod.rs @@ -1,3 +1,4 @@ +pub mod bgp; pub mod isis; pub mod openfabric; pub mod ospf; @@ -8,7 +9,9 @@ use std::collections::BTreeMap; use std::net::IpAddr; use std::str::FromStr; -use crate::ser::route_map::{AccessListName, AccessListRule, RouteMapEntry, RouteMapName}; +use crate::ser::route_map::{ + AccessListName, AccessListRule, PrefixListName, PrefixListRule, RouteMapEntry, RouteMapName, +}; use bon::Builder; use proxmox_network_types::ip_address::Cidr; @@ -159,6 +162,14 @@ pub struct IpProtocolRouteMap { pub v6: Option, } +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub enum VrfName { + #[serde(rename = "default")] + Default, + #[serde(untagged)] + Custom(String), +} + /// Main FRR config. /// /// Contains the two main frr building blocks: routers and interfaces. It also holds other @@ -174,8 +185,14 @@ pub struct FrrConfig { pub ospf: OspfFrrConfig, #[builder(default)] #[serde(default)] + pub bgp: BgpFrrConfig, + #[builder(default)] + #[serde(default)] pub isis: IsisFrrConfig, + #[builder(default)] + #[serde(default)] + pub ip_routes: Vec, #[builder(default)] #[serde(default)] pub protocol_routemaps: BTreeMap, @@ -185,6 +202,10 @@ pub struct FrrConfig { #[builder(default)] #[serde(default)] pub access_lists: BTreeMap>, + #[builder(default)] + #[serde(default)] + pub prefix_lists: BTreeMap>, + #[builder(default)] #[serde(default)] pub custom_frr_config: Vec, @@ -213,3 +234,14 @@ pub struct OspfFrrConfig { #[serde(default)] pub interfaces: BTreeMap>, } + +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct BgpFrrConfig { + #[serde(default)] + pub vrf_router: BTreeMap, + #[serde(default)] + pub view_router: BTreeMap, + + #[serde(default)] + pub vrfs: BTreeMap, +} diff --git a/proxmox-frr/src/ser/serializer.rs b/proxmox-frr/src/ser/serializer.rs index 646b81ab6044..12e4190744eb 100644 --- a/proxmox-frr/src/ser/serializer.rs +++ b/proxmox-frr/src/ser/serializer.rs @@ -22,21 +22,33 @@ fn create_env<'a>() -> Environment<'a> { "fabricd.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/fabricd.jinja").to_owned(), )), + "bgpd.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/bgpd.jinja").to_owned(), + )), "isisd.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/isisd.jinja").to_owned(), )), "ospfd.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/ospfd.jinja").to_owned(), )), + "bgp_router.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/bgp_router.jinja").to_owned(), + )), "interface.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/interface.jinja").to_owned(), )), "access_lists.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/access_lists.jinja").to_owned(), )), + "prefix_lists.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/prefix_lists.jinja").to_owned(), + )), "route_maps.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/route_maps.jinja").to_owned(), )), + "ip_routes.jinja" => Ok(Some( + include_str!("/usr/share/proxmox-frr/templates/ip_routes.jinja").to_owned(), + )), "protocol_routemaps.jinja" => Ok(Some( include_str!("/usr/share/proxmox-frr/templates/protocol_routemaps.jinja") .to_owned(), -- 2.47.3