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-manager v2 09/11] ui: sdn: add IPv6 RA / SLAAC support
Date: Thu, 30 Apr 2026 16:29:51 +0200	[thread overview]
Message-ID: <20260430142953.315412-10-h.laimer@proxmox.com> (raw)
In-Reply-To: <20260430142953.315412-1-h.laimer@proxmox.com>

Surface the per-vnet RA settings and per-subnet per-prefix overrides
in the SDN edit dialogs, matching the schema split.

The vnet edit gains an "IPv6 Router Advertisement" tab, only enabled
for EVPN zones. The subnet edit's IPv6 panel is reduced to per-prefix
overrides, only active on IPv6 CIDRs and with the autonomous flag
further gated on /64.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---
 www/manager6/form/SDNVnetSelector.js |   2 +-
 www/manager6/sdn/SubnetEdit.js       |  96 +++++++++++++++-
 www/manager6/sdn/SubnetView.js       |   6 +-
 www/manager6/sdn/VnetEdit.js         | 157 ++++++++++++++++++++++++++-
 www/manager6/sdn/VnetView.js         |   6 +-
 5 files changed, 259 insertions(+), 8 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 630b10b8..a462b563 100644
--- a/www/manager6/sdn/SubnetEdit.js
+++ b/www/manager6/sdn/SubnetEdit.js
@@ -45,6 +45,11 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
                     if (panel) {
                         panel.updateSnatState(value);
                     }
+                    let win = field.up('window');
+                    let ipv6 = win?.down('#ipv6Panel');
+                    if (ipv6) {
+                        ipv6.updateForCidr(value);
+                    }
                 },
             },
         },
@@ -82,6 +87,77 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
     ],
 });
 
+Ext.define('PVE.sdn.SubnetIPv6Panel', {
+    extend: 'Proxmox.panel.InputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    updateForCidr: function (cidr) {
+        let me = this;
+
+        let addr = cidr ? cidr.split('/')[0] : '';
+        let mask = cidr ? parseInt(cidr.split('/')[1], 10) : NaN;
+        let isV6 = !!addr && Proxmox.Utils.IP6_match.test(addr);
+        let isSlash64 = isV6 && mask === 64;
+
+        me.setDisabled(!isV6);
+
+        let autonomous = me.down('[name=nd-prefix-autonomous]');
+        if (autonomous) {
+            autonomous.setDisabled(!isSlash64);
+            if (isV6 && !isSlash64) {
+                autonomous.setValue(false);
+            }
+        }
+    },
+
+    items: [
+        {
+            xtype: 'proxmoxcheckbox',
+            name: 'nd-prefix-autonomous',
+            uncheckedValue: 0,
+            defaultValue: 1,
+            checked: true,
+            fieldLabel: gettext('SLAAC (A)'),
+            cbind: {
+                deleteDefaultValue: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxcheckbox',
+            name: 'nd-prefix-on-link',
+            uncheckedValue: 0,
+            defaultValue: 1,
+            checked: true,
+            fieldLabel: gettext('On-link (L)'),
+            cbind: {
+                deleteDefaultValue: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'nd-prefix-valid-lifetime',
+            fieldLabel: gettext('Valid Lifetime (s)'),
+            minValue: 0,
+            allowBlank: true,
+            emptyText: '2592000',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'nd-prefix-preferred-lifetime',
+            fieldLabel: gettext('Preferred Lifetime (s)'),
+            minValue: 0,
+            allowBlank: true,
+            emptyText: '604800',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+    ],
+});
+
 Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
     extend: 'Ext.form.FieldContainer',
     mixins: ['Ext.form.field.Field'],
@@ -261,6 +337,7 @@ Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
 Ext.define('PVE.sdn.SubnetEdit', {
     extend: 'Proxmox.window.Edit',
 
+    onlineHelp: 'pvesdn_config_subnet_nd_prefix',
     subject: gettext('Subnet'),
 
     subnet: undefined,
@@ -268,6 +345,7 @@ Ext.define('PVE.sdn.SubnetEdit', {
     width: 350,
 
     base_url: undefined,
+    zoneType: undefined,
 
     bodyPadding: 0,
 
@@ -295,23 +373,37 @@ Ext.define('PVE.sdn.SubnetEdit', {
             name: 'dhcp-range',
         });
 
+        let tabItems = [ipanel, dhcpPanel];
+        let ipv6Panel;
+        if (me.zoneType === 'evpn') {
+            ipv6Panel = Ext.create('PVE.sdn.SubnetIPv6Panel', {
+                isCreate: me.isCreate,
+                itemId: 'ipv6Panel',
+                title: gettext('IPv6 Prefix Options'),
+            });
+            tabItems.push(ipv6Panel);
+        }
+
         Ext.apply(me, {
             items: [
                 {
                     xtype: 'tabpanel',
                     bodyPadding: 10,
-                    items: [ipanel, dhcpPanel],
+                    items: tabItems,
                 },
             ],
         });
 
         me.callParent();
 
-        if (!me.isCreate) {
+        if (me.isCreate) {
+            ipv6Panel?.updateForCidr(undefined);
+        } else {
             me.load({
                 success: function (response, options) {
                     me.setValues(response.result.data);
                     ipanel.updateSnatState(response.result.data.cidr);
+                    ipv6Panel?.updateForCidr(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/VnetEdit.js b/www/manager6/sdn/VnetEdit.js
index 34e382c7..8dc48edc 100644
--- a/www/manager6/sdn/VnetEdit.js
+++ b/www/manager6/sdn/VnetEdit.js
@@ -56,6 +56,9 @@ Ext.define('PVE.sdn.VnetInputPanel', {
 
                     let panel = me.up('panel');
                     panel.setZoneType(zoneType);
+
+                    let raPanel = me.up('window').down('#ipv6RaPanel');
+                    raPanel?.setDisabled(zoneType !== 'evpn');
                 },
             },
         },
@@ -117,14 +120,148 @@ Ext.define('PVE.sdn.VnetInputPanel', {
     },
 });
 
+Ext.define('PVE.sdn.VnetIPv6RAPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onGetValues: function (values) {
+        if (values['ipv6-ra-rdnss']) {
+            values['ipv6-ra-rdnss'] = values['ipv6-ra-rdnss']
+                .split(/[\s,]+/)
+                .filter((v) => v.length > 0);
+        }
+        return values;
+    },
+
+    setValues: function (values) {
+        let me = this;
+
+        if (Ext.isArray(values['ipv6-ra-rdnss'])) {
+            values = Ext.apply({}, values);
+            values['ipv6-ra-rdnss'] = values['ipv6-ra-rdnss'].join(', ');
+        }
+        me.callParent([values]);
+    },
+
+    updateGatedState: function () {
+        let me = this;
+
+        let raEnabled = !!me.down('[name=ipv6-ra]').getValue();
+        for (let name of [
+            'ipv6-ra-managed',
+            'ipv6-ra-other',
+            'ipv6-ra-rdnss',
+            'ipv6-ra-router-lifetime',
+            'ipv6-ra-interval',
+            'ipv6-ra-mtu',
+        ]) {
+            let field = me.down(`[name=${name}]`);
+            if (field) {
+                field.setDisabled(!raEnabled);
+            }
+        }
+    },
+
+    items: [
+        {
+            xtype: 'proxmoxcheckbox',
+            name: 'ipv6-ra',
+            uncheckedValue: 0,
+            defaultValue: 0,
+            checked: false,
+            fieldLabel: gettext('Send Router Advertisements'),
+            cbind: {
+                deleteDefaultValue: '{!isCreate}',
+            },
+            listeners: {
+                change: function (field) {
+                    field.up('inputpanel').updateGatedState();
+                },
+            },
+        },
+        {
+            xtype: 'proxmoxcheckbox',
+            name: 'ipv6-ra-managed',
+            uncheckedValue: 0,
+            defaultValue: 0,
+            checked: false,
+            fieldLabel: gettext('DHCP Managed (M)'),
+            cbind: {
+                deleteDefaultValue: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxcheckbox',
+            name: 'ipv6-ra-other',
+            uncheckedValue: 0,
+            defaultValue: 0,
+            checked: false,
+            fieldLabel: gettext('DHCP Other (O)'),
+            cbind: {
+                deleteDefaultValue: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            name: 'ipv6-ra-rdnss',
+            fieldLabel: 'RDNSS',
+            emptyText: gettext('Comma-separated IPv6 addresses'),
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+    ],
+    advancedItems: [
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'ipv6-ra-router-lifetime',
+            fieldLabel: gettext('Router Lifetime (s)'),
+            emptyText: '1800',
+            minValue: 0,
+            maxValue: 9000,
+            allowBlank: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'ipv6-ra-interval',
+            fieldLabel: gettext('RA Interval (s)'),
+            emptyText: '600',
+            minValue: 4,
+            maxValue: 1800,
+            allowBlank: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'ipv6-ra-mtu',
+            fieldLabel: gettext('Advertised MTU'),
+            minValue: 1280,
+            maxValue: 65535,
+            allowBlank: true,
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+    ],
+});
+
 Ext.define('PVE.sdn.VnetEdit', {
     extend: 'Proxmox.window.Edit',
 
+    onlineHelp: 'pvesdn_config_vnet',
     subject: gettext('VNet'),
 
     vnet: undefined,
+    zoneType: undefined,
+
+    width: 400,
 
-    width: 350,
+    bodyPadding: 0,
 
     initComponent: function () {
         var me = this;
@@ -141,10 +278,24 @@ Ext.define('PVE.sdn.VnetEdit', {
 
         let ipanel = Ext.create('PVE.sdn.VnetInputPanel', {
             isCreate: me.isCreate,
+            title: gettext('General'),
+        });
+
+        let raPanel = Ext.create('PVE.sdn.VnetIPv6RAPanel', {
+            isCreate: me.isCreate,
+            itemId: 'ipv6RaPanel',
+            title: gettext('IPv6 Router Advertisement'),
+            disabled: me.zoneType !== 'evpn',
         });
 
         Ext.apply(me, {
-            items: [ipanel],
+            items: [
+                {
+                    xtype: 'tabpanel',
+                    bodyPadding: 10,
+                    items: [ipanel, raPanel],
+                },
+            ],
         });
 
         me.callParent();
@@ -154,6 +305,8 @@ Ext.define('PVE.sdn.VnetEdit', {
                 success: function (response, options) {
                     let values = response.result.data;
                     ipanel.setValues(values);
+                    raPanel.setValues(values);
+                    raPanel.updateGatedState();
                 },
             });
         }
diff --git a/www/manager6/sdn/VnetView.js b/www/manager6/sdn/VnetView.js
index 1c576db6..ce8c314e 100644
--- a/www/manager6/sdn/VnetView.js
+++ b/www/manager6/sdn/VnetView.js
@@ -36,6 +36,7 @@ Ext.define('PVE.sdn.VnetView', {
                 autoShow: true,
                 onlineHelp: 'pvesdn_config_vnet',
                 vnet: rec.data.vnet,
+                zoneType: rec.data['zone-type'],
             });
             win.on('destroy', reload);
         };
@@ -141,10 +142,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





  parent reply	other threads:[~2026-04-30 14:30 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 ` [PATCH pve-network v2 04/11] sdn: evpn: add IPv6 RA / SLAAC support Hannes Laimer
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 ` Hannes Laimer [this message]
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-10-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