From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 1433C1FF138 for ; Wed, 18 Feb 2026 11:23:39 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id AD01B17F8B; Wed, 18 Feb 2026 11:24:38 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets Date: Wed, 18 Feb 2026 11:23:45 +0100 Message-ID: <20260218102350.211294-2-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260218102350.211294-1-h.laimer@proxmox.com> References: <20260218102350.211294-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: 1771410229658 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.062 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: UTQAN2RJJAFVEVNFBAWLGXS2Q46IGFDH X-Message-ID-Hash: UTQAN2RJJAFVEVNFBAWLGXS2Q46IGFDH 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: 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 --- 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