[PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets

Hannes Laimer
2026-02-18T10:23:39Z

With this we allow enabling and configuring router-advertisements for
subnets in EVPN zones that have a ipv6 prefix configured.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
src/PVE/API2/Network/SDN/Subnets.pm | 8 +++
src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 42 ++++++++++-
src/PVE/Network/SDN/SubnetPlugin.pm | 70 +++++++++++++++++++
3 files changed, 119 insertions(+), 1 deletion(-)

diff --git a/src/PVE/API2/Network/SDN/Subnets.pm b/src/PVE/API2/Network/SDN/Subnets.pm
index fc56532..f4a4521 100644
--- a/src/PVE/API2/Network/SDN/Subnets.pm
+++ b/src/PVE/API2/Network/SDN/Subnets.pm
@@ -225,6 +225,10 @@ __PACKAGE__->register_method({
$id = "$zoneid-$id";

my $opts = PVE::Network::SDN::SubnetPlugin->check_config($id, $param, 1, 1);
+ if ($opts->{'nd-ra-flag-auto'} && !$opts->{'nd-ra-enable'}) {
+ raise_param_exc(
+ { 'nd-ra-flag-auto' => "SLAAC requires IPv6 RA to be enabled" });
+ }

my $scfg = undef;
if ($scfg = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id, 1)) {
@@ -300,6 +304,10 @@ __PACKAGE__->register_method({
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
}
$data->{$_} = $opts->{$_} for keys $opts->%*;
+ if ($data->{'nd-ra-flag-auto'} && !$data->{'nd-ra-enable'}) {
+ raise_param_exc(
+ { 'nd-ra-flag-auto' => "SLAAC requires IPv6 RA to be enabled" });
+ }

my $subnet = PVE::Network::SDN::Subnets::sdn_subnets_config($cfg, $id);
PVE::Network::SDN::SubnetPlugin->on_update_hook($zone, $id, $subnet, $scfg);
diff --git a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
index cc21712..b89c3bf 100644
--- a/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Controllers/EvpnPlugin.pm
@@ -447,6 +447,47 @@ sub generate_zone_frr_config {
sub generate_vnet_frr_config {
my ($class, $plugin_config, $controller, $zone, $zoneid, $vnetid, $config) = @_;

+ my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
+ my $has_v6 = 0;
+ my $managed = 0;
+ my $other = 0;
+ my $rdnss = undef;
+ my @prefix_opts = ();
+
+ foreach my $subnetid (sort keys %{$subnets}) {
+ my $subnet = $subnets->{$subnetid};
+ my $cidr = $subnet->{cidr};
+ my ($ip, $mask) = split(/\//, $cidr);
+ my $is_v6 = Net::IP::ip_is_ipv6($ip);
+ next if !$is_v6;
+
+ # only enable RA if explicitly enabled on the subnet
+ next if !$subnet->{'nd-ra-enable'};
+
+ $has_v6 = 1;
+
+ $managed = 1 if $subnet->{'nd-ra-flag-managed'};
+ $other = 1 if $subnet->{'nd-ra-flag-other'};
+ $rdnss = $subnet->{'nd-ra-rdnss'} if $subnet->{'nd-ra-rdnss'};
+
+ if (!$subnet->{'nd-ra-flag-auto'}) {
+ push @prefix_opts, "ipv6 nd prefix $cidr no-autoconfig";
+ } else {
+ my $valid = $subnet->{'nd-prefix-valid-lifetime'} // 2592000;
+ my $preferred = $subnet->{'nd-prefix-preferred-lifetime'} // 604800;
+ push @prefix_opts, "ipv6 nd prefix $cidr $valid $preferred";
+ }
+ }
+
+ if ($has_v6) {
+ my $iface_rules = ($config->{frr_interfaces}->{$vnetid} //= []);
+ push @$iface_rules, "no ipv6 nd suppress-ra";
+ push @$iface_rules, "ipv6 nd managed-config-flag" if $managed;
+ push @$iface_rules, "ipv6 nd other-config-flag" if $other;
+ push @$iface_rules, "ipv6 nd rdnss $rdnss" if $rdnss;
+ push @$iface_rules, @prefix_opts;
+ }
+
my $exitnodes = $zone->{'exitnodes'};
my $exitnodes_local_routing = $zone->{'exitnodes-local-routing'};

@@ -457,7 +498,6 @@ sub generate_vnet_frr_config {

return if !$is_gateway;

- my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
my @controller_config = ();
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..0732640 100644
--- a/src/PVE/Network/SDN/SubnetPlugin.pm
+++ b/src/PVE/Network/SDN/SubnetPlugin.pm
@@ -177,6 +177,43 @@ sub properties {
description => 'IP address for the DNS server',
optional => 1,
},
+ 'nd-ra-flag-auto' => {
+ type => 'boolean',
+ description => "enable SLAAC for this subnet",
+ },
+ 'nd-ra-flag-managed' => {
+ type => 'boolean',
+ description => "enable DHCP Managed (M) flag for this subnet",
+ },
+ 'nd-ra-flag-other' => {
+ type => 'boolean',
+ description => "enable DHCP Other (O) flag for this subnet",
+ },
+ 'nd-ra-rdnss' => {
+ type => 'string',
+ format => 'ipv6',
+ description => "RDNSS address for this subnet",
+ optional => 1,
+ },
+ 'nd-ra-enable' => {
+ type => 'boolean',
+ description =>
+ "enable IPv6 Router Advertisement (RA) for this subnet, only possible if gateway is specified",
+ },
+ 'nd-prefix-valid-lifetime' => {
+ type => 'integer',
+ description => "Valid lifetime for the prefix (seconds)",
+ minimum => 0,
+ default => 2592000,
+ optional => 1,
+ },
+ 'nd-prefix-preferred-lifetime' => {
+ type => 'integer',
+ description => "Preferred lifetime for the prefix (seconds)",
+ minimum => 0,
+ default => 604800,
+ optional => 1,
+ },
};
}

@@ -189,6 +226,13 @@ sub options {
dnszoneprefix => { optional => 1 },
'dhcp-range' => { optional => 1 },
'dhcp-dns-server' => { optional => 1 },
+ 'nd-ra-flag-auto' => { optional => 1 },
+ 'nd-ra-flag-managed' => { optional => 1 },
+ 'nd-ra-flag-other' => { optional => 1 },
+ 'nd-ra-rdnss' => { optional => 1 },
+ 'nd-ra-enable' => { optional => 1 },
+ 'nd-prefix-valid-lifetime' => { optional => 1 },
+ 'nd-prefix-preferred-lifetime' => { optional => 1 },
};
}

@@ -206,6 +250,32 @@ sub on_update_hook {
my $dns = $zone->{dns};
my $dnszone = $zone->{dnszone};
my $reversedns = $zone->{reversedns};
+ my $ra_enable = $subnet->{'nd-ra-enable'};
+ my $slaac = $subnet->{'nd-ra-flag-auto'};
+ my $ra_flag_managed = $subnet->{'nd-ra-flag-managed'};
+ my $ra_flag_other = $subnet->{'nd-ra-flag-other'};
+
+ if ($slaac) {
+ raise_param_exc({ slaac => "SLAAC is only supported on IPv6 subnets" })
+ if !PVE::JSONSchema::pve_verify_cidrv6($cidr, 1);
+
+ raise_param_exc({ slaac => "SLAAC requires IPv6 RA to be enabled" }) if !$ra_enable;
+
+ my $v6_mask = $mask;
+ if (!defined($v6_mask) && $cidr =~ /\/(\d+)$/) {
+ $v6_mask = $1;
+ }
+ raise_param_exc({ slaac => "SLAAC is only supported on IPv6 subnets with a /64 mask" })
+ if $v6_mask != 64;
+ }
+
+ if ($ra_enable) {
+ raise_param_exc({ 'ipv6-ra' => "IPv6 RA can only be enabled on IPv6 subnets" })
+ if !PVE::JSONSchema::pve_verify_cidrv6($cidr, 1);
+
+ raise_param_exc({ 'ipv6-ra' => "IPv6 RA requires a gateway to be defined" })
+ if !$gateway;
+ }

my $mac = undef;

--
2.47.3