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 5167B1FF140 for ; Fri, 27 Mar 2026 16:10:33 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7CE0E116F3; Fri, 27 Mar 2026 16:10:51 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 2/2] sdn: evpn: support eBGP EVPN over BGP fabric underlay Date: Fri, 27 Mar 2026 16:10:30 +0100 Message-ID: <20260327151031.149360-6-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260327151031.149360-1-h.laimer@proxmox.com> References: <20260327151031.149360-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1774624193220 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.081 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: ICUCTNUKH5MHEUMVPTTRZGMPI6L4LD7X X-Message-ID-Hash: ICUCTNUKH5MHEUMVPTTRZGMPI6L4LD7X X-MailFrom: h.laimer@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: When the EVPN controller references a BGP fabric, configure the EVPN sessions as eBGP. Each node's fabric ASN becomes the router process ASN, and VTEP peers use 'remote-as external' with 'ebgp-multihop' for loopback-based peering across the fabric. EVPN sessions run as eBGP with per-node fabric ASNs rather than using 'local-as' to present a shared overlay ASN for iBGP peering. While iBGP would be simpler (no autort override needed), peers created as eBGP retain a hop limit of 1 even after local-as reclassifies them as iBGP, and 'ebgp-multihop' - used to raise the hop limit for eBGP peers - is silently ignored for iBGP peers. With other fabric types all nodes share one ASN, so peers are created as iBGP from the start with hop limit 255. Signed-off-by: Hannes Laimer --- src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 40 ++++++++- .../bgp_fabric/expected_controller_config | 73 ++++++++++++++++ .../evpn/bgp_fabric/expected_sdn_interfaces | 56 ++++++++++++ src/test/zones/evpn/bgp_fabric/interfaces | 6 ++ src/test/zones/evpn/bgp_fabric/sdn_config | 85 +++++++++++++++++++ 5 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 src/test/zones/evpn/bgp_fabric/expected_controller_config create mode 100644 src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces create mode 100644 src/test/zones/evpn/bgp_fabric/interfaces create mode 100644 src/test/zones/evpn/bgp_fabric/sdn_config diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm index d2825f57..186b4c39 100644 --- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm +++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm @@ -65,6 +65,9 @@ sub generate_frr_config { my $bgp_controller = find_bgp_controller($local_node, $controller_cfg); my $isis_controller = find_isis_controller($local_node, $controller_cfg); + my $fabric_is_bgp = 0; + my $fabric_asn = undef; + if ($plugin_config->{'fabric'}) { my $config = PVE::Network::SDN::Fabrics::config(1); @@ -99,6 +102,12 @@ sub generate_frr_config { $ifaceip = $current_node->{ip}; $routerid = $current_node->{ip}; + # Detect BGP fabric underlay - needs special ASN handling + if ($fabric->{protocol} eq 'bgp') { + $fabric_is_bgp = 1; + $fabric_asn = int($current_node->{asn}); + } + } elsif ($plugin_config->{'peers'}) { @peers = PVE::Tools::split_list($plugin_config->{'peers'}); @@ -116,7 +125,14 @@ sub generate_frr_config { return; } - if ($bgp_controller) { + if ($fabric_is_bgp && $fabric_asn) { + # BGP fabric underlay: EVPN sessions are eBGP between per-node ASNs. + # The router runs under the fabric node ASN; VTEP peers are external. + $ebgp = 1; + $asn = $fabric_asn; + $autortas = $plugin_config->{asn} + if int($plugin_config->{asn}) != $fabric_asn; + } elsif ($bgp_controller) { $ebgp = 1 if $plugin_config->{'asn'} ne $bgp_controller->{asn}; $asn = int($bgp_controller->{asn}) if $bgp_controller->{asn}; $autortas = $plugin_config->{'asn'} if $ebgp; @@ -138,6 +154,12 @@ sub generate_frr_config { $bgp_router->{address_families} = {}; } + # eBGP EVPN over BGP fabric needs multipath-relax for ECMP + if ($fabric_is_bgp) { + $bgp_router->{bestpath_as_path_multipath_relax} = 1; + $bgp_router->{disable_ebgp_connected_route_check} = 1; + } + # Build VTEP neighbor group my @vtep_ips = grep { $_ ne $ifaceip } @peers; @@ -148,6 +170,7 @@ sub generate_frr_config { ips => \@vtep_ips, interfaces => [], }; + $neighbor_group->{ebgp_multihop} = 10 if $ebgp && $loopback; $neighbor_group->{update_source} = $loopback if $loopback; @@ -201,6 +224,9 @@ sub generate_zone_frr_config { my $bgprouter = find_bgp_controller($local_node, $controller_cfg); my $isisrouter = find_isis_controller($local_node, $controller_cfg); + my $fabric_is_bgp = 0; + my $fabric_asn = undef; + if ($controller->{fabric}) { my $config = PVE::Network::SDN::Fabrics::config(1); @@ -234,6 +260,11 @@ sub generate_zone_frr_config { $ifaceip = $current_node->{ip}; $routerid = $current_node->{ip}; + if ($fabric->{protocol} eq 'bgp') { + $fabric_is_bgp = 1; + $fabric_asn = int($current_node->{asn}); + } + } elsif ($controller->{peers}) { @peers = PVE::Tools::split_list($controller->{'peers'}) if $controller->{'peers'}; @@ -252,7 +283,12 @@ sub generate_zone_frr_config { return; } - if ($bgprouter) { + if ($fabric_is_bgp && $fabric_asn) { + $ebgp = 1; + $asn = $fabric_asn; + $autortas = $controller->{asn} + if int($controller->{asn}) != $fabric_asn; + } elsif ($bgprouter) { $ebgp = 1 if $controller->{'asn'} ne $bgprouter->{asn}; $asn = $bgprouter->{asn} if $bgprouter->{asn}; $autortas = $controller->{'asn'} if $ebgp; diff --git a/src/test/zones/evpn/bgp_fabric/expected_controller_config b/src/test/zones/evpn/bgp_fabric/expected_controller_config new file mode 100644 index 00000000..56d35e02 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/expected_controller_config @@ -0,0 +1,73 @@ +frr version 10.4.1 +frr defaults datacenter +hostname localhost +log syslog informational +service integrated-vtysh-config +! +vrf vrf_evpn + vni 100 +exit-vrf +! +router bgp 65100 + bgp router-id 10.10.10.1 + no bgp hard-administrative-reset + no bgp default ipv4-unicast + coalesce-time 1000 + no bgp graceful-restart notification + bgp disable-ebgp-connected-route-check + bgp bestpath as-path multipath-relax + neighbor VTEP peer-group + neighbor VTEP remote-as external + neighbor VTEP bfd + neighbor VTEP ebgp-multihop 10 + neighbor VTEP update-source dummy_test + neighbor 10.10.10.2 peer-group VTEP + neighbor 10.10.10.3 peer-group VTEP + neighbor test peer-group + neighbor test remote-as external + neighbor ens18 interface peer-group test + neighbor ens19 interface peer-group test + ! + address-family ipv4 unicast + network 10.10.10.1/32 + neighbor test activate + neighbor test soft-reconfiguration inbound + exit-address-family + ! + address-family l2vpn evpn + neighbor VTEP activate + neighbor VTEP route-map MAP_VTEP_IN in + neighbor VTEP route-map MAP_VTEP_OUT out + advertise-all-vni + autort as 65000 + exit-address-family +exit +! +router bgp 65100 vrf vrf_evpn + bgp router-id 10.10.10.1 + no bgp hard-administrative-reset + no bgp graceful-restart notification + ! + address-family l2vpn evpn + route-target import 65000:100 + route-target export 65000:100 + exit-address-family +exit +! +access-list pve_bgp_test_ips permit 10.10.10.0/24 +! +route-map MAP_VTEP_IN permit 1 +exit +! +route-map MAP_VTEP_OUT permit 1 +exit +! +route-map pve_bgp permit 100 + match ip address pve_bgp_test_ips + set src 10.10.10.1 +exit +! +ip protocol bgp route-map pve_bgp +! +line vty +! diff --git a/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces b/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces new file mode 100644 index 00000000..fd1429d8 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/expected_sdn_interfaces @@ -0,0 +1,56 @@ +#version:1 + +auto vnet0 +iface vnet0 + address 10.123.123.1/24 + hwaddress BC:24:11:3B:39:34 + bridge_ports vxlan_vnet0 + bridge_stp off + bridge_fd 0 + mtu 1450 + ip-forward on + arp-accept on + vrf vrf_evpn + +auto vrf_evpn +iface vrf_evpn + vrf-table auto + post-up ip route add vrf vrf_evpn unreachable default metric 4278198272 + +auto vrfbr_evpn +iface vrfbr_evpn + bridge-ports vrfvx_evpn + bridge_stp off + bridge_fd 0 + mtu 1450 + vrf vrf_evpn + +auto vrfvx_evpn +iface vrfvx_evpn + vxlan-id 100 + vxlan-local-tunnelip 10.10.10.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 + +auto vxlan_vnet0 +iface vxlan_vnet0 + vxlan-id 123456 + vxlan-local-tunnelip 10.10.10.1 + bridge-learning off + bridge-arp-nd-suppress on + mtu 1450 + +auto dummy_test +iface dummy_test inet static + address 10.10.10.1/32 + link-type dummy + ip-forward 1 + +auto ens18 +iface ens18 inet manual + ip-forward 1 + +auto ens19 +iface ens19 inet manual + ip-forward 1 diff --git a/src/test/zones/evpn/bgp_fabric/interfaces b/src/test/zones/evpn/bgp_fabric/interfaces new file mode 100644 index 00000000..08874137 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/interfaces @@ -0,0 +1,6 @@ +auto vmbr0 +iface vmbr0 inet static + address 10.10.10.1/32 + bridge-ports eth0 + bridge-stp off + bridge-fd 0 diff --git a/src/test/zones/evpn/bgp_fabric/sdn_config b/src/test/zones/evpn/bgp_fabric/sdn_config new file mode 100644 index 00000000..080f1e98 --- /dev/null +++ b/src/test/zones/evpn/bgp_fabric/sdn_config @@ -0,0 +1,85 @@ +{ + 'zones' => { + 'ids' => { + 'evpn' => { + 'type' => 'evpn', + 'ipam' => 'pve', + 'mac' => 'BC:24:11:3B:39:34', + 'controller' => 'ctrl', + 'vrf-vxlan' => 100 + } + } + }, + 'vnets' => { + 'ids' => { + 'vnet0' => { + 'zone' => 'evpn', + 'type' => 'vnet', + 'tag' => 123456 + } + } + }, + 'version' => 1, + 'subnets' => { + 'ids' => { + 'evpn-10.123.123.0-24' => { + 'vnet' => 'vnet0', + 'type' => 'subnet', + 'gateway' => '10.123.123.1' + } + } + }, + 'controllers' => { + 'ids' => { + 'ctrl' => { + 'fabric' => 'test', + 'asn' => 65000, + 'type' => 'evpn' + } + } + }, + 'fabrics' => { + 'ids' => { + 'test' => { + 'type' => 'bgp_fabric', + 'id' => 'test', + 'bfd' => 0, + 'ip_prefix' => '10.10.10.0/24', + }, + 'test_localhost' => { + 'asn' => 65100, + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'id' => 'test_localhost', + 'type' => 'bgp_node', + 'ip' => '10.10.10.1', + 'role' => 'internal', + }, + 'test_pathfinder' => { + 'asn' => 65101, + 'id' => 'test_pathfinder', + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'ip' => '10.10.10.2', + 'type' => 'bgp_node', + 'role' => 'internal', + }, + 'test_raider' => { + 'asn' => 65102, + 'ip' => '10.10.10.3', + 'type' => 'bgp_node', + 'interfaces' => [ + 'name=ens18', + 'name=ens19' + ], + 'id' => 'test_raider', + 'role' => 'internal', + } + } + } + }; + -- 2.47.3