all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones
@ 2026-02-18 10:23 Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets Hannes Laimer
                   ` (5 more replies)
  0 siblings, 6 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

Rather than just enabling SLAAC, this allows configuring/enabling RAs.
So SLAAC support is rather a consequence of being able to configure RAs.

The second patch for `pve-network` contains some context from why we set
`accept_untracked_na=2`.

This also adds a new example setup to the sdn docs, specifically how to
setup a evpn zone with an openfabric underlay and two ipv6 subnets. One
with SLAAC and one with static v6 addresses. The second one is a `/96`
and should show how to configure smaller than `/64` GUA prefixes without
SLAAC.


pve-network:

Hannes Laimer (3):
  sdn: evpn: add ipv6-nd support for subnets
  sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges
  api: vnet: include zone-type in vnet list

 src/PVE/API2/Network/SDN/Subnets.pm           |  8 +++
 src/PVE/API2/Network/SDN/Vnets.pm             | 13 ++++
 src/PVE/Network/SDN/Controllers/EvpnPlugin.pm | 42 ++++++++++-
 src/PVE/Network/SDN/SubnetPlugin.pm           | 70 +++++++++++++++++++
 src/PVE/Network/SDN/Zones/EvpnPlugin.pm       | 14 +++-
 .../exitnode_snat/expected_sdn_interfaces     |  1 +
 .../exitnodenullroute/expected_sdn_interfaces |  1 +
 .../evpn/ipv4ipv6/expected_sdn_interfaces     |  1 +
 .../ipv4ipv6nogateway/expected_sdn_interfaces |  1 +
 .../zones/evpn/ipv6/expected_sdn_interfaces   |  1 +
 .../evpn/ipv6underlay/expected_sdn_interfaces |  1 +
 11 files changed, 151 insertions(+), 2 deletions(-)


pve-manager:

Hannes Laimer (1):
  ui: sdn: add ipv6 options for subnets in evpns zones

 www/manager6/form/SDNVnetSelector.js |   2 +-
 www/manager6/sdn/SubnetEdit.js       | 132 ++++++++++++++++++++++++++-
 www/manager6/sdn/SubnetView.js       |   6 +-
 www/manager6/sdn/VnetView.js         |   5 +-
 4 files changed, 140 insertions(+), 5 deletions(-)


pve-docs:

Hannes Laimer (2):
  sdn: add subnet ipv6 options section
  sdn: add exmaple for ipv6 in an evpn zone

 pvesdn.adoc | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 240 insertions(+)


Summary over all repositories:
  16 files changed, 531 insertions(+), 7 deletions(-)

-- 
Generated by murpp 0.9.0




^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 2/3] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges Hannes Laimer
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

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





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-network 2/3] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 3/3] api: vnet: include zone-type in vnet list Hannes Laimer
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

In EVPN setups with per-node anycast first-hop gateways, guests may use
the shared gateway link-local address as next-hop, while return traffic
targets the guest GUA. If an exit node has no neighbor entry for that
GUA, it sends an NS.

Only the exit-node kernel tracks that NS state. Because ND traffic is
seen across EVPN nodes, a non-exit node can receive the guest's NA
without a matching local INCOMPLETE entry and can treat it as untracked.
Ignoring that NA prevents neighbor learning and can break IPv6 return
traffic.

Set `accept_untracked_na=2`[1] on EVPN vnet bridges that have IPv6
subnets so valid NA replies are accepted in this distributed gateway
topology.

Router Advertisements can trigger this, but RA presence is neither a
necessary nor a sufficient selector. Keying this to EVPN vnets with IPv6
subnets is therefore more robust, even though it is broader than just
looking at whether RAs are enabled.

Without this, deployments depend on pre-populated neighbor state (for
example guest-initiated traffic/pings first), which is fragile and
causes intermittent first-packet IPv6 failures.

[1] https://docs.kernel.org/networking/ip-sysctl.html#proc-sys-net-ipv6-variables

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/PVE/Network/SDN/Zones/EvpnPlugin.pm            | 14 +++++++++++++-
 .../evpn/exitnode_snat/expected_sdn_interfaces     |  1 +
 .../evpn/exitnodenullroute/expected_sdn_interfaces |  1 +
 .../zones/evpn/ipv4ipv6/expected_sdn_interfaces    |  1 +
 .../evpn/ipv4ipv6nogateway/expected_sdn_interfaces |  1 +
 src/test/zones/evpn/ipv6/expected_sdn_interfaces   |  1 +
 .../evpn/ipv6underlay/expected_sdn_interfaces      |  1 +
 7 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
index 8e7ddfd..c2895bc 100644
--- a/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
+++ b/src/PVE/Network/SDN/Zones/EvpnPlugin.pm
@@ -219,6 +219,7 @@ sub generate_sdn_config {
     my $address = {};
     my $ipv4 = undef;
     my $ipv6 = undef;
+    my $has_ipv6_subnet = undef;
     my $enable_forward_v4 = undef;
     my $enable_forward_v6 = undef;
     my $subnets = PVE::Network::SDN::Vnets::get_subnets($vnetid, 1);
@@ -226,6 +227,7 @@ sub generate_sdn_config {
         my $subnet = $subnets->{$subnetid};
         my $cidr = $subnet->{cidr};
         my $mask = $subnet->{mask};
+        my ($subnet_ip) = split(/\//, $cidr);
 
         my $gateway = $subnet->{gateway};
         if ($gateway) {
@@ -233,9 +235,16 @@ sub generate_sdn_config {
             $address->{$gateway} = 1;
         }
 
+        $has_ipv6_subnet = 1 if $subnet_ip && Net::IP::ip_is_ipv6($subnet_ip);
+
         my $iptables = undef;
         my $checkrouteip = undef;
-        my $ipversion = Net::IP::ip_is_ipv6($gateway) ? 6 : 4;
+        my $ipversion = 4;
+        if ($gateway) {
+            $ipversion = Net::IP::ip_is_ipv6($gateway) ? 6 : 4;
+        } elsif ($subnet_ip) {
+            $ipversion = Net::IP::ip_is_ipv6($subnet_ip) ? 6 : 4;
+        }
 
         if ($ipversion == 6) {
             $ipv6 = 1;
@@ -278,6 +287,9 @@ sub generate_sdn_config {
     push @iface_config, "ip-forward on" if $enable_forward_v4;
     push @iface_config, "ip6-forward on" if $enable_forward_v6;
     push @iface_config, "arp-accept on" if $ipv4 || $ipv6;
+    push @iface_config,
+        "post-up echo 2 > /proc/sys/net/ipv6/conf/$vnetid/accept_untracked_na || true"
+        if $has_ipv6_subnet;
     push @iface_config, "vrf $vrf_iface" if $vrf_iface;
     push(@{ $config->{$vnetid} }, @iface_config) if !$config->{$vnetid};
 
diff --git a/src/test/zones/evpn/exitnode_snat/expected_sdn_interfaces b/src/test/zones/evpn/exitnode_snat/expected_sdn_interfaces
index 47df77a..e63c409 100644
--- a/src/test/zones/evpn/exitnode_snat/expected_sdn_interfaces
+++ b/src/test/zones/evpn/exitnode_snat/expected_sdn_interfaces
@@ -28,6 +28,7 @@ iface myvnet2
 	mtu 1450
 	ip6-forward on
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet2/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto vrf_myzone
diff --git a/src/test/zones/evpn/exitnodenullroute/expected_sdn_interfaces b/src/test/zones/evpn/exitnodenullroute/expected_sdn_interfaces
index 4bf5ccf..81a3b39 100644
--- a/src/test/zones/evpn/exitnodenullroute/expected_sdn_interfaces
+++ b/src/test/zones/evpn/exitnodenullroute/expected_sdn_interfaces
@@ -15,6 +15,7 @@ iface myvnet
 	ip-forward on
 	ip6-forward on
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto myvnet2
diff --git a/src/test/zones/evpn/ipv4ipv6/expected_sdn_interfaces b/src/test/zones/evpn/ipv4ipv6/expected_sdn_interfaces
index 7a5d741..7b1727b 100644
--- a/src/test/zones/evpn/ipv4ipv6/expected_sdn_interfaces
+++ b/src/test/zones/evpn/ipv4ipv6/expected_sdn_interfaces
@@ -12,6 +12,7 @@ iface myvnet
 	ip-forward on
 	ip6-forward on
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto vrf_myzone
diff --git a/src/test/zones/evpn/ipv4ipv6nogateway/expected_sdn_interfaces b/src/test/zones/evpn/ipv4ipv6nogateway/expected_sdn_interfaces
index 378fa77..4d904be 100644
--- a/src/test/zones/evpn/ipv4ipv6nogateway/expected_sdn_interfaces
+++ b/src/test/zones/evpn/ipv4ipv6nogateway/expected_sdn_interfaces
@@ -8,6 +8,7 @@ iface myvnet
 	bridge_fd 0
 	mtu 1450
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto vrf_myzone
diff --git a/src/test/zones/evpn/ipv6/expected_sdn_interfaces b/src/test/zones/evpn/ipv6/expected_sdn_interfaces
index b2bdbfe..f776122 100644
--- a/src/test/zones/evpn/ipv6/expected_sdn_interfaces
+++ b/src/test/zones/evpn/ipv6/expected_sdn_interfaces
@@ -10,6 +10,7 @@ iface myvnet
 	mtu 1450
 	ip6-forward on
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto vrf_myzone
diff --git a/src/test/zones/evpn/ipv6underlay/expected_sdn_interfaces b/src/test/zones/evpn/ipv6underlay/expected_sdn_interfaces
index 3b91f75..ab5988f 100644
--- a/src/test/zones/evpn/ipv6underlay/expected_sdn_interfaces
+++ b/src/test/zones/evpn/ipv6underlay/expected_sdn_interfaces
@@ -10,6 +10,7 @@ iface myvnet
 	mtu 1450
 	ip6-forward on
 	arp-accept on
+	post-up echo 2 > /proc/sys/net/ipv6/conf/myvnet/accept_untracked_na || true
 	vrf vrf_myzone
 
 auto vrf_myzone
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-network 3/3] api: vnet: include zone-type in vnet list
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-network 2/3] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-manager 1/1] ui: sdn: add ipv6 options for subnets in evpns zones Hannes Laimer
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

Helpful for subnet options that are only available for specific zones.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 src/PVE/API2/Network/SDN/Vnets.pm | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/PVE/API2/Network/SDN/Vnets.pm b/src/PVE/API2/Network/SDN/Vnets.pm
index b8faeac..e0b8607 100644
--- a/src/PVE/API2/Network/SDN/Vnets.pm
+++ b/src/PVE/API2/Network/SDN/Vnets.pm
@@ -155,6 +155,12 @@ __PACKAGE__->register_method({
                     optional => 0,
                     description => 'Name of the VNet.',
                 },
+                'zone-type' => {
+                    type => 'string',
+                    description => 'Type of the zone this VNet belongs to.',
+                    enum => PVE::Network::SDN::Zones::Plugin->lookup_types(),
+                    optional => 1,
+                },
                 pending => {
                     type => 'object',
                     description =>
@@ -174,15 +180,20 @@ __PACKAGE__->register_method({
         my $authuser = $rpcenv->get_user();
 
         my $cfg = {};
+        my $zone_cfg = {};
         if ($param->{pending}) {
             my $running_cfg = PVE::Network::SDN::running_config();
             my $config = PVE::Network::SDN::Vnets::config();
+            my $zone_config = PVE::Network::SDN::Zones::config();
             $cfg = PVE::Network::SDN::pending_config($running_cfg, $config, 'vnets');
+            $zone_cfg = PVE::Network::SDN::pending_config($running_cfg, $zone_config, 'zones');
         } elsif ($param->{running}) {
             my $running_cfg = PVE::Network::SDN::running_config();
             $cfg = $running_cfg->{vnets};
+            $zone_cfg = $running_cfg->{zones};
         } else {
             $cfg = PVE::Network::SDN::Vnets::config();
+            $zone_cfg = PVE::Network::SDN::Zones::config();
         }
 
         my @sids = PVE::Network::SDN::Vnets::sdn_vnets_ids($cfg);
@@ -192,6 +203,8 @@ __PACKAGE__->register_method({
             my $scfg = &$api_sdn_vnets_config($cfg, $id);
             my $zoneid = $scfg->{zone} // $scfg->{pending}->{zone};
             next if !$rpcenv->check_any($authuser, "/sdn/zones/$zoneid/$id", $privs, 1);
+            $scfg->{'zone-type'} = $zone_cfg->{ids}->{$zoneid}->{type}
+                if $zone_cfg->{ids}->{$zoneid};
 
             push @$res, $scfg;
         }
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-manager 1/1] ui: sdn: add ipv6 options for subnets in evpns zones
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
                   ` (2 preceding siblings ...)
  2026-02-18 10:23 ` [PATCH pve-network 3/3] api: vnet: include zone-type in vnet list Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-docs 1/2] sdn: add subnet ipv6 options section Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-docs 2/2] sdn: add exmaple for ipv6 in an evpn zone Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 www/manager6/form/SDNVnetSelector.js |   2 +-
 www/manager6/sdn/SubnetEdit.js       | 132 ++++++++++++++++++++++++++-
 www/manager6/sdn/SubnetView.js       |   6 +-
 www/manager6/sdn/VnetView.js         |   5 +-
 4 files changed, 140 insertions(+), 5 deletions(-)

diff --git a/www/manager6/form/SDNVnetSelector.js b/www/manager6/form/SDNVnetSelector.js
index 9e54159c..5ccc3cfb 100644
--- a/www/manager6/form/SDNVnetSelector.js
+++ b/www/manager6/form/SDNVnetSelector.js
@@ -52,7 +52,7 @@ Ext.define(
     function () {
         Ext.define('pve-sdn-vnet', {
             extend: 'Ext.data.Model',
-            fields: ['alias', 'tag', 'type', 'vnet', 'zone'],
+            fields: ['alias', 'tag', 'type', 'vnet', 'zone', 'zone-type'],
             proxy: {
                 type: 'proxmox',
                 url: '/api2/json/cluster/sdn/vnets',
diff --git a/www/manager6/sdn/SubnetEdit.js b/www/manager6/sdn/SubnetEdit.js
index a3608428..89cabce4 100644
--- a/www/manager6/sdn/SubnetEdit.js
+++ b/www/manager6/sdn/SubnetEdit.js
@@ -2,6 +2,21 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
     extend: 'Proxmox.panel.InputPanel',
     mixins: ['Proxmox.Mixin.CBind'],
 
+    updateSnatState: function (cidr) {
+        let me = this;
+        let snatField = me.down('[name=snat]');
+        if (!snatField) {
+            return;
+        }
+
+        let addr = cidr ? cidr.split('/')[0] : '';
+        let isV6 = !!addr && Proxmox.Utils.IP6_match.test(addr);
+        snatField.setDisabled(isV6);
+        if (isV6) {
+            snatField.setValue(false);
+        }
+    },
+
     onGetValues: function (values) {
         let me = this;
 
@@ -24,6 +39,14 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
             flex: 1,
             allowBlank: false,
             fieldLabel: gettext('Subnet'),
+            listeners: {
+                change: function (field, value) {
+                    let panel = field.up('inputpanel');
+                    if (panel) {
+                        panel.updateSnatState(value);
+                    }
+                },
+            },
         },
         {
             xtype: 'proxmoxtextfield',
@@ -59,6 +82,100 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
     ],
 });
 
+Ext.define('PVE.sdn.SubnetIPv6Panel', {
+    extend: 'Proxmox.panel.InputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    items: [
+        {
+            xtype: 'fieldset',
+            title: 'Router Advertisement',
+            items: [
+                {
+                    xtype: 'proxmoxcheckbox',
+                    name: 'nd-ra-enable',
+                    uncheckedValue: null,
+                    checked: false,
+                    fieldLabel: 'Enable RA',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+                {
+                    xtype: 'proxmoxtextfield',
+                    name: 'nd-ra-rdnss',
+                    fieldLabel: 'RDNSS',
+                    vtype: 'IP64Address',
+                    allowBlank: true,
+                    skipEmptyText: true,
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+                {
+                    xtype: 'proxmoxcheckbox',
+                    name: 'nd-ra-flag-managed',
+                    uncheckedValue: null,
+                    checked: false,
+                    fieldLabel: 'DHCP Managed (M)',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+                {
+                    xtype: 'proxmoxcheckbox',
+                    name: 'nd-ra-flag-other',
+                    uncheckedValue: null,
+                    checked: false,
+                    fieldLabel: 'DHCP Other (O)',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+                {
+                    xtype: 'proxmoxcheckbox',
+                    name: 'nd-ra-flag-auto',
+                    uncheckedValue: null,
+                    checked: false,
+                    fieldLabel: 'SLAAC (A)',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+            ],
+        },
+        {
+            xtype: 'fieldset',
+            itemId: 'slaacFieldset',
+            title: 'SLAAC',
+            items: [
+                {
+                    xtype: 'proxmoxintegerfield',
+                    name: 'nd-prefix-valid-lifetime',
+                    fieldLabel: gettext('Valid Prefix Lifetime'),
+                    minValue: 0,
+                    allowBlank: true,
+                    emptyText: '2592000 (seconds)',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+                {
+                    xtype: 'proxmoxintegerfield',
+                    name: 'nd-prefix-preferred-lifetime',
+                    fieldLabel: gettext('Preferred Prefix Lifetime'),
+                    minValue: 0,
+                    allowBlank: true,
+                    emptyText: '604800 (seconds)',
+                    cbind: {
+                        deleteEmpty: '{!isCreate}',
+                    },
+                },
+            ],
+        },
+    ],
+});
+
 Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
     extend: 'Ext.form.FieldContainer',
     mixins: ['Ext.form.field.Field'],
@@ -238,6 +355,7 @@ Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
 Ext.define('PVE.sdn.SubnetEdit', {
     extend: 'Proxmox.window.Edit',
 
+    onlineHelp: 'pvesdn_config_subnet_ipv6',
     subject: gettext('Subnet'),
 
     subnet: undefined,
@@ -245,6 +363,7 @@ Ext.define('PVE.sdn.SubnetEdit', {
     width: 350,
 
     base_url: undefined,
+    zoneType: undefined,
 
     bodyPadding: 0,
 
@@ -272,12 +391,22 @@ Ext.define('PVE.sdn.SubnetEdit', {
             name: 'dhcp-range',
         });
 
+        let tabItems = [ipanel, dhcpPanel];
+        if (me.zoneType === 'evpn') {
+            let ipv6Panel = Ext.create('PVE.sdn.SubnetIPv6Panel', {
+                isCreate: me.isCreate,
+                itemId: 'ipv6Panel',
+                title: gettext('IPv6 Options'),
+            });
+            tabItems.push(ipv6Panel);
+        }
+
         Ext.apply(me, {
             items: [
                 {
                     xtype: 'tabpanel',
                     bodyPadding: 10,
-                    items: [ipanel, dhcpPanel],
+                    items: tabItems,
                 },
             ],
         });
@@ -288,6 +417,7 @@ Ext.define('PVE.sdn.SubnetEdit', {
             me.load({
                 success: function (response, options) {
                     me.setValues(response.result.data);
+                    ipanel.updateSnatState(response.result.data.cidr);
                 },
             });
         }
diff --git a/www/manager6/sdn/SubnetView.js b/www/manager6/sdn/SubnetView.js
index c61458e0..1eee33d4 100644
--- a/www/manager6/sdn/SubnetView.js
+++ b/www/manager6/sdn/SubnetView.js
@@ -8,13 +8,15 @@ Ext.define(
         stateId: 'grid-sdn-subnet',
 
         base_url: undefined,
+        zone_type: undefined,
 
         remove_btn: undefined,
 
-        setBaseUrl: function (url) {
+        setBaseUrl: function (url, zoneType) {
             let me = this;
 
             me.base_url = url;
+            me.zone_type = zoneType;
 
             if (url === undefined) {
                 me.store.removeAll();
@@ -50,6 +52,7 @@ Ext.define(
                     autoShow: true,
                     subnet: rec.data.subnet,
                     base_url: me.base_url,
+                    zoneType: me.zone_type,
                 });
                 win.on('destroy', reload);
             };
@@ -62,6 +65,7 @@ Ext.define(
                         autoShow: true,
                         base_url: me.base_url,
                         type: 'subnet',
+                        zoneType: me.zone_type,
                     });
                     win.on('destroy', reload);
                 },
diff --git a/www/manager6/sdn/VnetView.js b/www/manager6/sdn/VnetView.js
index 1c576db6..4ba97a2f 100644
--- a/www/manager6/sdn/VnetView.js
+++ b/www/manager6/sdn/VnetView.js
@@ -141,10 +141,11 @@ Ext.define('PVE.sdn.VnetView', {
                 show: reload,
                 select: function (_sm, rec) {
                     let url = `/cluster/sdn/vnets/${rec.data.vnet}/subnets`;
-                    me.subnetview_panel.setBaseUrl(url);
+                    let zoneType = rec.data['zone-type'];
+                    me.subnetview_panel.setBaseUrl(url, zoneType);
                 },
                 deselect: function () {
-                    me.subnetview_panel.setBaseUrl(undefined);
+                    me.subnetview_panel.setBaseUrl(undefined, undefined);
                 },
             },
         });
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-docs 1/2] sdn: add subnet ipv6 options section
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
                   ` (3 preceding siblings ...)
  2026-02-18 10:23 ` [PATCH pve-manager 1/1] ui: sdn: add ipv6 options for subnets in evpns zones Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  2026-02-18 10:23 ` [PATCH pve-docs 2/2] sdn: add exmaple for ipv6 in an evpn zone Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pvesdn.adoc | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index d20a0eb..267ce97 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -437,6 +437,34 @@ SNAT:: Enable Source NAT which allows VMs from inside a
 DNS Zone Prefix:: Add a prefix to the domain registration, like
   <hostname>.prefix.<domain>  Optional.
 
+[[pvesdn_config_subnet_ipv6]]
+IPv6 subnet options (EVPN)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For VNets in an EVPN zone, subnets can optionally configure IPv6 Router
+Advertisement (RA) and related Neighbor Discovery options.
+
+These options are available in the subnet editor under `IPv6 Options`.
+
+Enable RA:: Enable IPv6 Router Advertisements for this subnet.
+
+RDNSS:: Recursive DNS Server advertised via RA.
+
+DHCP Managed (M):: Set the managed-address configuration flag in RA.
+
+DHCP Other (O):: Set the other-configuration flag in RA.
+
+SLAAC (A):: Enable autonomous address configuration for the prefix.
+
+Valid Prefix Lifetime:: Valid lifetime advertised for the prefix in seconds.
+
+Preferred Prefix Lifetime:: Preferred lifetime advertised for the prefix in
+  seconds.
+
+NOTE: `SLAAC` footnote:[RFC-7217 https://datatracker.ietf.org/doc/html/rfc7217]
+is only possible on `/64` prefixes. Router Advertisements have to be enabled for
+SLAAC to work.
+
 
 [[pvesdn_config_controllers]]
 Controllers
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH pve-docs 2/2] sdn: add exmaple for ipv6 in an evpn zone
  2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
                   ` (4 preceding siblings ...)
  2026-02-18 10:23 ` [PATCH pve-docs 1/2] sdn: add subnet ipv6 options section Hannes Laimer
@ 2026-02-18 10:23 ` Hannes Laimer
  5 siblings, 0 replies; 7+ messages in thread
From: Hannes Laimer @ 2026-02-18 10:23 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 pvesdn.adoc | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 212 insertions(+)

diff --git a/pvesdn.adoc b/pvesdn.adoc
index 267ce97..e68a889 100644
--- a/pvesdn.adoc
+++ b/pvesdn.adoc
@@ -1424,6 +1424,218 @@ can reply back.
 If you have configured an external BGP router, the BGP-EVPN routes (10.0.1.0/24
 and 10.0.2.0/24 in this example), will be announced dynamically.
 
+[[pvesdn_setup_example_evpn_ipv6]]
+EVPN with IPv6 Setup Example
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The example assumes a cluster with three nodes (node1, node2, node3) with P2P
+connections between `node1<->node2` and `node2<->node3`. Additionally we'll
+also need an `/48` IPv6 prefix routed to the exit node.
+
+We will start by creating an OpenFabric which will be the underlay for our EVPN
+zone. Then we'll set up a VNet with two subnets, one with SLAAC and a `/96` one
+with RA enabled and statically assigned IPs.
+
+.Prefixes
+. to `node2` routed `/48`: `2001:db8:abcd::/48`
+. `/64` subnet with SLAAC: `2001:db8:abcd:1::/64`
+. `/96` subnet without SLAAC: `2001:db8:abcd:2::/96`
+
+.IPv6 forwarding
+This has to be enabled on all nodes.
+----
+sysctl -w net.ipv6.conf.all.forwarding=1
+----
+
+NOTE: The underlay will be IPv4 based, the overlay IPv6.
+
+Setup `OpenFabric`
+^^^^^^^^^^^^^^^^^^
+
+. Create the fabric with `10.0.0.0/24` as its IPv4 prefix and `underlay` as its
+  name, we can leave the defaults for the rest
+. Add the three nodes to the fabric,
+.. `node1` with `10.0.0.1`, select P2P NIC between `node1<->node2`
+.. `node2` with `10.0.0.2`, select both P2P NICs between `node1<->node2` and
+   `node2<->node3`
+.. `node3` with `10.0.0.3`, select P2P NIC between `node2<->node3`
+. Apply the changes
+
+The resulting config in `/etc/pve/sdn/fabrics.cfg` should look something like
+this:
+
+----
+openfabric_fabric: underlay
+	ip_prefix 10.0.0.0/24
+
+openfabric_node: underlay_node1
+	interfaces name=ens19
+	ip 10.0.0.1
+
+openfabric_node: underlay_node2
+	interfaces name=ens20
+	interfaces name=ens19
+	ip 10.0.0.2
+
+openfabric_node: underlay_node3
+	interfaces name=ens19
+	ip 10.0.0.3
+----
+
+Each node should now also be able to reach all of the others, this can quickly
+be checked with a
+
+----
+ping 10.0.0.X
+----
+
+With `ip -4 r` you should also be able to see the respective `onlink` routes.
+
+----
+10.0.0.2 nhid 28 via 10.0.0.2 dev ens19 proto openfabric src 10.0.0.1 metric 20 onlink
+10.0.0.3 nhid 28 via 10.0.0.2 dev ens19 proto openfabric src 10.0.0.1 metric 20 onlink
+----
+
+If this works we can continue.
+
+Create controller
+^^^^^^^^^^^^^^^^^
+
+Create an EVPN controller, keep `65000` as `ASN#` and select the `underlay`
+fabric we have just created. For this we'll name the controller `v6ctl`. Then
+apply the changes.
+
+The relevant config here is `/etc/pve/sdn/controllers.cfg`, and should look
+like this:
+
+----
+evpn: v6ctl
+	asn 65000
+	fabric underlay
+----
+
+Setup the zone
+^^^^^^^^^^^^^^
+
+Create a new EVPN zone, select the `v6ctl` as its controller, set the
+`VRF_VXLAN Tag` to `6` and name it `v6zone`. As exit node select the node that
+the `/48` prefix is routed to. Then apply the changes.
+
+The relevant config here is `/etc/pve/sdn/zones.cfg`, and after applying should
+look something like this:
+
+----
+evpn: v6zone
+	controller v6ctl
+	vrf-vxlan 6
+	exitnodes node2
+	ipam pve
+	mac BC:24:11:F2:72:B8
+----
+
+
+Create the VNets
+^^^^^^^^^^^^^^^^
+
+.The bigger `/64` with SLAAC
+Create a new VNet, name it `net64`, select `v6zone` as zone and set `64` as tag.
+
+.The smaller `/96` without SLAAC and static IPs
+Create a new VNet, name it `net96`, select `v6zone` as zone and set `96` as tag.
+
+After applying `/etc/pve/sdn/vnets.cfg` should contain:
+
+----
+vnet: net64
+	zone v6zone
+	tag 64
+
+vnet: net96
+	zone v6zone
+	tag 96
+----
+
+
+Setup the subnets
+^^^^^^^^^^^^^^^^^
+
+First we'll configure the `/64` subnet in `net64` with SLAAC:
+
+. Select the `net64` vnet
+. Create subnet with `2001:db8:abcd:1::/64` and `2001:db8:abcd:1::1/64` as
+  gateway
+. Under "IPv6 Options" check both `Enable RA` and `SLAAC (A)`
+
+Then configure the `/96` subnet in `net96`:
+
+. Select the `net96` vnet
+. Create subnet with `2001:db8:abcd:2::/96` and `2001:db8:abcd:2::1/96` as
+  gateway
+. Under "IPv6 Options" only check `Enable RA`
+
+NOTE: With RAs enabled a default gateway is advertised so we don't have to
+specify a gateway explicitly for guests with a static IP. It'll also allow us to
+advertise a DNS server, which can be handy because usually we'd need a whole DHCP
+server for that. Whether the advertised DNS is used is somewhat client
+dependent, as sometimes this option is ignored.
+
+The relevant config for these is `/etc/pve/sdn/subnets.cfg`, and after applying
+these changes should contain
+
+----
+subnet: v6zone-2001:db8:abcd:1::-64
+	vnet net64
+	gateway 2001:db8:abcd:1::1
+	nd-ra-enable 1
+	nd-ra-flag-auto 1
+
+subnet: v6zone-2001:db8:abcd:2::-96
+	vnet net96
+	gateway 2001:db8:abcd:2::1
+	nd-ra-enable 1
+----
+
+Result
+^^^^^^
+
+Now we can create two guests to test this, we'll use Debian CTs here, but any
+guest will do.
+
+. configure first guest
+.. Select `net64` as bridge during
+.. leave the IPv4 section `static` and the IP field empty
+.. for IPv6 select `SLAAC`
+. configure second guest
+.. select `net96` as bridge during
+.. leave the IPv4 section `static` and the IP field empty
+.. for IPv6 select `static` and set `2001:db8:abcd:2::200/96` as `IPv6`
+
+NOTE: We can leave `gateway` empty because we have RAs enabled on `net96`,
+setting it is also not a problem, just not needed.
+
+Both guests should have a default route via the gateway's link-local address,
+`ip -6 r`
+
+----
+default via fe80::be24:11ff:fef2:72b8 dev eth0 proto ra metric 1024 expires 1799sec hoplimit 64 pref medium
+----
+
+This address will match the link-local address on the host's `net64` (or `net96`) interfaces, `ip a show net64`.
+
+----
+7: net64: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master vrf_v6zone state UP group default qlen 1000
+    link/ether bc:24:11:f2:72:b8 brd ff:ff:ff:ff:ff:ff
+    inet6 2001:db8:abcd:1::1/64 scope global
+       valid_lft forever preferred_lft forever
+    inet6 fe80::be24:11ff:fef2:72b8/64 scope link proto kernel_ll
+       valid_lft forever preferred_lft forever
+----
+
+Before the guests first traffic, `ip -6 neigh show vrf vrf_v6zone` will only
+contain an entry for the guests link-local address. After the first `ping
+2a00:1450:4001:818::2003`, also the guests GUA should have an entry.
+
+
 
 [[pvesdn_notes]]
 Notes
-- 
2.47.3





^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-02-18 10:24 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-18 10:23 [PATCH docs/manager/network 0/6] add SLAAC support for subnets in EVPN zones Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-network 1/3] sdn: evpn: add ipv6-nd support for subnets Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-network 2/3] sdn: evpn: accept untracked IPv6 NA on EVPN vnet bridges Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-network 3/3] api: vnet: include zone-type in vnet list Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-manager 1/1] ui: sdn: add ipv6 options for subnets in evpns zones Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-docs 1/2] sdn: add subnet ipv6 options section Hannes Laimer
2026-02-18 10:23 ` [PATCH pve-docs 2/2] sdn: add exmaple for ipv6 in an evpn zone Hannes Laimer

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal