public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Hannes Laimer <h.laimer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-network v2 04/11] sdn: evpn: add IPv6 RA / SLAAC support
Date: Thu, 30 Apr 2026 16:29:46 +0200	[thread overview]
Message-ID: <20260430142953.315412-5-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260430142953.315412-1-h.laimer@proxmox.com>

Configure IPv6 Router Advertisements on EVPN vnets so guests can
autoconfigure addresses via SLAAC and pick up DNS / DHCP hints,
without standing up a DHCPv6 server alongside.

The configuration is split between the VNet and the subnet, matching
the protocol's per-RA / per-prefix layers. Defaults are picked so
the typical SLAAC case needs no per-subnet configuration.

The API rejects each option where it does not apply: the RA toggle
outside EVPN zones, per-prefix overrides on IPv4 subnets, the
autonomous flag on non-/64 prefixes, and SNAT on IPv6 subnets.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 12 +++-
 src/PVE/Network/SDN/SubnetPlugin.pm           | 67 +++++++++++++++++++
 src/PVE/Network/SDN/VnetPlugin.pm             | 67 +++++++++++++++++++
 .../evpn/slaac/expected_controller_config     | 53 +++++++++++++++
 .../zones/evpn/slaac/expected_sdn_interfaces  | 43 ++++++++++++
 src/test/zones/evpn/slaac/interfaces          |  7 ++
 src/test/zones/evpn/slaac/sdn_config          | 41 ++++++++++++
 7 files changed, 289 insertions(+), 1 deletion(-)
 create mode 100644 src/test/zones/evpn/slaac/expected_controller_config
 create mode 100644 src/test/zones/evpn/slaac/expected_sdn_interfaces
 create mode 100644 src/test/zones/evpn/slaac/interfaces
 create mode 100644 src/test/zones/evpn/slaac/sdn_config

diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index f50387d..96cd619 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -11,6 +11,7 @@ use PVE::RESTEnvironment qw(log_warn);
 use PVE::Network::SDN::Controllers::Plugin;
 use PVE::Network::SDN::Zones::Plugin;
 use PVE::Network::SDN::Fabrics;
+use PVE::RS::SDN::IPv6RA;
 use Net::IP;
 
 use base('PVE::Network::SDN::Controllers::Plugin');
@@ -547,6 +548,16 @@ sub generate_zone_frr_config {
 sub generate_vnet_frr_config {
     my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_;
 
+    # Read the pending subnets (subnets.cfg) so dry-run sees pending changes. After an
+    # apply this matches the running-config snapshot anyway.
+    my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 0) // {};
+
+    if ($plugin_config->{'ipv6-ra'}) {
+        my $nd_iface =
+            PVE::RS::SDN::IPv6RA::build_interface($vnetid, $plugin_config, $subnets);
+        $config->{frr}->{nd_interfaces}->{$vnetid} = $nd_iface if $nd_iface;
+    }
+
     my $exitnodes = $zone->{'exitnodes'};
     my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'};
 
@@ -557,7 +568,6 @@ sub generate_vnet_frr_config {
 
     return if !$is_gateway;
 
-    my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
     $config->{frr}->{ip_routes} //= [];
     foreach my $subnetid (sort keys %{$subnets}) {
         my $subnet = $subnets->{$subnetid};
diff --git a/src/PVE/Network/SDN/SubnetPlugin.pm b/src/PVE/Network/SDN/SubnetPlugin.pm
index e2a0e50..66c184b 100644
--- a/src/PVE/Network/SDN/SubnetPlugin.pm
+++ b/src/PVE/Network/SDN/SubnetPlugin.pm
@@ -177,6 +177,37 @@ sub properties {
             description => 'IP address for the DNS server',
             optional => 1,
         },
+        # Per-prefix Router Advertisement overrides. Only meaningful for IPv6 subnets
+        # whose vnet has `ipv6-ra` enabled, silently ignored otherwise.
+        'nd-prefix-autonomous' => {
+            type => 'boolean',
+            description =>
+                "Set the autonomous (A) flag for this prefix in RAs, enabling SLAAC for"
+                . " hosts on this subnet (default: enabled). Only valid on /64 subnets.",
+            optional => 1,
+            default => 1,
+        },
+        'nd-prefix-on-link' => {
+            type => 'boolean',
+            description =>
+                "Set the on-link (L) flag for this prefix in RAs (default: on-link).",
+            optional => 1,
+            default => 1,
+        },
+        'nd-prefix-valid-lifetime' => {
+            type => 'integer',
+            description => "Valid lifetime for this prefix in RAs, in seconds.",
+            minimum => 0,
+            default => 2592000,
+            optional => 1,
+        },
+        'nd-prefix-preferred-lifetime' => {
+            type => 'integer',
+            description => "Preferred lifetime for this prefix in RAs, in seconds.",
+            minimum => 0,
+            default => 604800,
+            optional => 1,
+        },
     };
 }
 
@@ -189,6 +220,10 @@ sub options {
         dnszoneprefix => { optional => 1 },
         'dhcp-range' => { optional => 1 },
         'dhcp-dns-server' => { optional => 1 },
+        'nd-prefix-autonomous' => { optional => 1 },
+        'nd-prefix-on-link' => { optional => 1 },
+        'nd-prefix-valid-lifetime' => { optional => 1 },
+        'nd-prefix-preferred-lifetime' => { optional => 1 },
     };
 }
 
@@ -206,6 +241,38 @@ sub on_update_hook {
     my $dns = $zone->{dns};
     my $dnszone = $zone->{dnszone};
     my $reversedns = $zone->{reversedns};
+    my $is_ipv6 = PVE::JSONSchema::pve_verify_cidrv6($cidr, 1);
+
+    # Per-prefix RA overrides only apply on IPv6 subnets. Reject explicit overrides on
+    # IPv4 subnets so the user notices their config has no effect.
+    my $has_nd_override =
+        defined($subnet->{'nd-prefix-autonomous'})
+        || defined($subnet->{'nd-prefix-on-link'})
+        || defined($subnet->{'nd-prefix-valid-lifetime'})
+        || defined($subnet->{'nd-prefix-preferred-lifetime'});
+
+    if ($has_nd_override && !$is_ipv6) {
+        raise_param_exc(
+            { 'nd-prefix' => "nd-prefix-* options are only valid on IPv6 subnets" });
+    }
+
+    # SLAAC (autonomous flag) requires the prefix to be /64. Default for autonomous is on,
+    # so the check fires unless the user explicitly opts out on a non-/64 IPv6 subnet.
+    if ($is_ipv6 && $mask != 64) {
+        my $autonomous = $subnet->{'nd-prefix-autonomous'};
+        $autonomous = 1 if !defined($autonomous);
+        if ($autonomous) {
+            raise_param_exc({
+                'nd-prefix-autonomous' =>
+                    "autonomous (SLAAC) flag requires a /64 prefix, set nd-prefix-autonomous=0 to advertise this prefix without SLAAC",
+            });
+        }
+    }
+
+    # SNAT is meaningless on IPv6 subnets.
+    if ($subnet->{snat} && $is_ipv6) {
+        raise_param_exc({ snat => "SNAT is not supported on IPv6 subnets" });
+    }
 
     my $mac = undef;
 
diff --git a/src/PVE/Network/SDN/VnetPlugin.pm b/src/PVE/Network/SDN/VnetPlugin.pm
index e041575..28779c8 100644
--- a/src/PVE/Network/SDN/VnetPlugin.pm
+++ b/src/PVE/Network/SDN/VnetPlugin.pm
@@ -7,6 +7,8 @@ use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file);
 use PVE::Exception qw(raise raise_param_exc);
 use PVE::JSONSchema qw(get_standard_option);
 
+use PVE::Network::SDN::Zones;
+
 use PVE::SectionConfig;
 use base qw(PVE::SectionConfig);
 
@@ -83,6 +85,57 @@ sub properties {
                 "If true, sets the isolated property for all interfaces on the bridge of this VNet.",
             optional => 1,
         },
+        # IPv6 Router Advertisement settings (EVPN zones only). The master toggle gates
+        # the rest. Per-prefix flags live on the subnet (nd-prefix-*).
+        'ipv6-ra' => {
+            type => 'boolean',
+            description =>
+                "Emit IPv6 Router Advertisements on this VNet's bridge. Requires the VNet to belong"
+                . " to an EVPN zone with at least one IPv6 subnet.",
+            optional => 1,
+        },
+        'ipv6-ra-managed' => {
+            type => 'boolean',
+            description => "Set the managed-address (M) flag in emitted RAs.",
+            optional => 1,
+        },
+        'ipv6-ra-other' => {
+            type => 'boolean',
+            description => "Set the other-configuration (O) flag in emitted RAs.",
+            optional => 1,
+        },
+        'ipv6-ra-rdnss' => {
+            type => 'array',
+            description => "RDNSS (Recursive DNS Server) addresses advertised in RAs.",
+            optional => 1,
+            items => {
+                type => 'string',
+                format => 'ipv6',
+            },
+        },
+        'ipv6-ra-router-lifetime' => {
+            type => 'integer',
+            description =>
+                "Router lifetime advertised in RAs (seconds). 0 tells hosts not to use this"
+                . " router as a default gateway.",
+            optional => 1,
+            minimum => 0,
+            maximum => 9000,
+        },
+        'ipv6-ra-interval' => {
+            type => 'integer',
+            description => "Maximum interval between unsolicited RAs (seconds).",
+            optional => 1,
+            minimum => 4,
+            maximum => 1800,
+        },
+        'ipv6-ra-mtu' => {
+            type => 'integer',
+            description => "MTU advertised in RAs.",
+            optional => 1,
+            minimum => 1280,
+            maximum => 65535,
+        },
     };
 }
 
@@ -93,6 +146,13 @@ sub options {
         alias => { optional => 1 },
         vlanaware => { optional => 1 },
         'isolate-ports' => { optional => 1 },
+        'ipv6-ra' => { optional => 1 },
+        'ipv6-ra-managed' => { optional => 1 },
+        'ipv6-ra-other' => { optional => 1 },
+        'ipv6-ra-rdnss' => { optional => 1 },
+        'ipv6-ra-router-lifetime' => { optional => 1 },
+        'ipv6-ra-interval' => { optional => 1 },
+        'ipv6-ra-mtu' => { optional => 1 },
     };
 }
 
@@ -117,6 +177,13 @@ sub on_update_hook {
         raise_param_exc({ vlanaware => "vlanaware vnet is not compatible with subnets" })
             if $subnets;
     }
+
+    if ($vnet->{'ipv6-ra'}) {
+        my $zone_cfg = PVE::Network::SDN::Zones::config();
+        my $zone = $zone_cfg->{ids}->{ $vnet->{zone} };
+        raise_param_exc({ 'ipv6-ra' => "IPv6 RA is only supported on EVPN zones" })
+            if !$zone || $zone->{type} ne 'evpn';
+    }
 }
 
 1;
diff --git a/src/test/zones/evpn/slaac/expected_controller_config b/src/test/zones/evpn/slaac/expected_controller_config
new file mode 100644
index 0000000..b8fafdf
--- /dev/null
+++ b/src/test/zones/evpn/slaac/expected_controller_config
@@ -0,0 +1,53 @@
+frr version 10.4.1
+frr defaults datacenter
+hostname localhost
+log syslog informational
+service integrated-vtysh-config
+!
+vrf vrf_myzone
+ vni 1000
+exit-vrf
+!
+router bgp 65000
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp default ipv4-unicast
+ coalesce-time 1000
+ no bgp graceful-restart notification
+ neighbor VTEP peer-group
+ neighbor VTEP remote-as 65000
+ neighbor VTEP bfd
+ neighbor 192.168.0.2 peer-group VTEP
+ neighbor 192.168.0.3 peer-group VTEP
+ !
+ address-family l2vpn evpn
+  neighbor VTEP activate
+  neighbor VTEP route-map MAP_VTEP_IN in
+  neighbor VTEP route-map MAP_VTEP_OUT out
+  advertise-all-vni
+ exit-address-family
+exit
+!
+router bgp 65000 vrf vrf_myzone
+ bgp router-id 192.168.0.1
+ no bgp hard-administrative-reset
+ no bgp graceful-restart notification
+exit
+!
+route-map MAP_VTEP_IN permit 1
+exit
+!
+route-map MAP_VTEP_OUT permit 1
+exit
+!
+interface myvnet
+ no ipv6 nd suppress-ra
+ ipv6 nd managed-config-flag
+ ipv6 nd other-config-flag
+ ipv6 nd rdnss 2a08:2142:302:3::53
+ ipv6 nd prefix 2a08:2142:302:3::/64 7200 3600
+ ipv6 nd prefix fd00:1::/64 no-autoconfig
+exit
+!
+line vty
+!
diff --git a/src/test/zones/evpn/slaac/expected_sdn_interfaces b/src/test/zones/evpn/slaac/expected_sdn_interfaces
new file mode 100644
index 0000000..2e479a3
--- /dev/null
+++ b/src/test/zones/evpn/slaac/expected_sdn_interfaces
@@ -0,0 +1,43 @@
+#version:1
+
+auto myvnet
+iface myvnet
+	address 2a08:2142:302:3::1/64
+	address fd00:1::1/64
+	hwaddress A2:1D:CB:1A:C0:8B
+	bridge_ports vxlan_myvnet
+	bridge_stp off
+	bridge_fd 0
+	mtu 1450
+	ip6-forward on
+	arp-accept on
+	vrf vrf_myzone
+
+auto vrf_myzone
+iface vrf_myzone
+	vrf-table auto
+	post-up ip route add vrf vrf_myzone unreachable default metric 4278198272
+
+auto vrfbr_myzone
+iface vrfbr_myzone
+	bridge-ports vrfvx_myzone
+	bridge_stp off
+	bridge_fd 0
+	mtu 1450
+	vrf vrf_myzone
+
+auto vrfvx_myzone
+iface vrfvx_myzone
+	vxlan-id 1000
+	vxlan-local-tunnelip 192.168.0.1
+	bridge-learning off
+	bridge-arp-nd-suppress on
+	mtu 1450
+
+auto vxlan_myvnet
+iface vxlan_myvnet
+	vxlan-id 100
+	vxlan-local-tunnelip 192.168.0.1
+	bridge-learning off
+	bridge-arp-nd-suppress on
+	mtu 1450
diff --git a/src/test/zones/evpn/slaac/interfaces b/src/test/zones/evpn/slaac/interfaces
new file mode 100644
index 0000000..66bb826
--- /dev/null
+++ b/src/test/zones/evpn/slaac/interfaces
@@ -0,0 +1,7 @@
+auto vmbr0
+iface vmbr0 inet static
+	address 192.168.0.1/24
+	gateway 192.168.0.254
+        bridge-ports eth0
+        bridge-stp off
+        bridge-fd 0
diff --git a/src/test/zones/evpn/slaac/sdn_config b/src/test/zones/evpn/slaac/sdn_config
new file mode 100644
index 0000000..e6294e3
--- /dev/null
+++ b/src/test/zones/evpn/slaac/sdn_config
@@ -0,0 +1,41 @@
+{
+  version => 1,
+  vnets   => {
+               ids => {
+                        myvnet => {
+                                    tag => "100",
+                                    type => "vnet",
+                                    zone => "myzone",
+                                    'ipv6-ra' => 1,
+                                    'ipv6-ra-managed' => 1,
+                                    'ipv6-ra-other' => 1,
+                                    'ipv6-ra-rdnss' => ['2a08:2142:302:3::53'],
+                                  },
+                      },
+             },
+
+  zones   => {
+               ids => { myzone => { ipam => "pve", type => "evpn", controller => "evpnctl", 'vrf-vxlan' => 1000, 'mac' => 'A2:1D:CB:1A:C0:8B' } },
+             },
+  controllers  => {
+               ids => { evpnctl => { type => "evpn", 'peers' => '192.168.0.1,192.168.0.2,192.168.0.3', asn => "65000" } },
+             },
+
+  subnets => {
+              ids => {
+                        'myzone-2a08:2142:302:3::-64' => {
+                                                           'type' => 'subnet',
+                                                           'vnet' => 'myvnet',
+                                                           'gateway' => '2a08:2142:302:3::1',
+                                                           'nd-prefix-valid-lifetime' => 7200,
+                                                           'nd-prefix-preferred-lifetime' => 3600,
+                                                         },
+                        'myzone-fd00:1::-64' => {
+                                                  'type' => 'subnet',
+                                                  'vnet' => 'myvnet',
+                                                  'gateway' => 'fd00:1::1',
+                                                  'nd-prefix-autonomous' => 0,
+                                                },
+                     }
+             }
+}
-- 
2.47.3





  parent reply	other threads:[~2026-04-30 14:31 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-30 14:29 [PATCH docs/manager/network/proxmox{-ve-rs,-perl-rs} v2 00/11] sdn: evpn: add IPv6 RA / SLAAC support Hannes Laimer
2026-04-30 14:29 ` [PATCH proxmox-ve-rs v2 01/11] frr: add IPv6 router advertisement support Hannes Laimer
2026-04-30 14:29 ` [PATCH proxmox-ve-rs v2 02/11] ve-config: add per-vnet IPv6 RA configuration Hannes Laimer
2026-04-30 14:29 ` [PATCH proxmox-perl-rs v2 03/11] pve-rs: sdn: add IPv6 RA builder binding Hannes Laimer
2026-04-30 14:29 ` Hannes Laimer [this message]
2026-04-30 14:29 ` [PATCH pve-network v2 05/11] sdn: evpn: derive IP version from CIDR for gateway-less subnets Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 06/11] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-network v2 07/11] api: vnet: include zone-type in vnet list Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-manager v2 08/11] ui: sdn: disable SNAT for IPv6 subnets Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-manager v2 09/11] ui: sdn: add IPv6 RA / SLAAC support Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-docs v2 10/11] sdn: document IPv6 RA / SLAAC configuration Hannes Laimer
2026-04-30 14:29 ` [PATCH pve-docs v2 11/11] sdn: add example for IPv6 in an EVPN zone 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=20260430142953.315412-5-h.laimer@proxmox.com \
    --to=h.laimer@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