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 v5 4/9] ui: sdn: add IPv6 RA / SLAAC support
Date: Thu, 29 Oct 2026 16:41:43 +0100	[thread overview]
Message-ID: <20261029154148.2882594-5-h.laimer@proxmox.com> (raw)
In-Reply-To: <20261029154148.2882594-1-h.laimer@proxmox.com>

Add an "IPv6 Router Advertisement" tab to the vnet dialog for the
per-vnet RA settings, and an "IPv6 Prefix Options" tab to the subnet
dialog for the per-prefix flags and lifetimes.

The dialogs offer the settings only where the API accepts them. The
vnet tab is enabled only for EVPN zones. The subnet tab appears only
in EVPN zones and is enabled only for an IPv6 prefix. Its SLAAC flag
is disabled for any prefix other than a /64.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
---

Notes:
    v4:
     - the router lifetime field shows Default instead of 1800, FRR derives it from the interval

 www/manager6/form/SDNVnetSelector.js |   2 +-
 www/manager6/sdn/SubnetEdit.js       | 130 ++++++++++++++++-
 www/manager6/sdn/SubnetView.js       |   7 +-
 www/manager6/sdn/VnetEdit.js         | 202 ++++++++++++++++++++++++++-
 www/manager6/sdn/VnetView.js         |   6 +-
 5 files changed, 339 insertions(+), 8 deletions(-)

diff --git a/www/manager6/form/SDNVnetSelector.js b/www/manager6/form/SDNVnetSelector.js
index 9e54159c1405..5ccc3cfb1699 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 a360842891a3..020df23efd11 100644
--- a/www/manager6/sdn/SubnetEdit.js
+++ b/www/manager6/sdn/SubnetEdit.js
@@ -24,6 +24,14 @@ Ext.define('PVE.sdn.SubnetInputPanel', {
             flex: 1,
             allowBlank: false,
             fieldLabel: gettext('Subnet'),
+            listeners: {
+                change: function (field, value) {
+                    let ipv6 = field.up('window')?.down('#ipv6Panel');
+                    if (ipv6) {
+                        ipv6.updateForCidr(value);
+                    }
+                },
+            },
         },
         {
             xtype: 'proxmoxtextfield',
@@ -59,6 +67,102 @@ 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);
+        if (!isV6) {
+            return;
+        }
+
+        let autonomous = me.down('[name=nd-prefix-autonomous]');
+        if (autonomous) {
+            autonomous.setDisabled(!isSlash64);
+            if (!isSlash64) {
+                if (autonomous.getValue()) {
+                    me.autonomousAutoCleared = true;
+                    autonomous.setValue(false);
+                }
+            } else if (me.autonomousAutoCleared) {
+                me.autonomousAutoCleared = false;
+                autonomous.setValue(true);
+            }
+        }
+    },
+
+    onGetValues: function (values) {
+        let me = this;
+
+        if (me.isCreate) {
+            for (let name of ['nd-prefix-autonomous', 'nd-prefix-on-link']) {
+                if (String(values[name]) === '1') {
+                    delete values[name];
+                }
+            }
+        }
+
+        return values;
+    },
+
+    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,
+            maxValue: 4294967295,
+            allowBlank: true,
+            emptyText: '2592000',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+        {
+            xtype: 'proxmoxintegerfield',
+            name: 'nd-prefix-preferred-lifetime',
+            fieldLabel: gettext('Preferred Lifetime (s)'),
+            minValue: 0,
+            maxValue: 4294967295,
+            allowBlank: true,
+            emptyText: '604800',
+            cbind: {
+                deleteEmpty: '{!isCreate}',
+            },
+        },
+    ],
+});
+
 Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
     extend: 'Ext.form.FieldContainer',
     mixins: ['Ext.form.field.Field'],
@@ -238,13 +342,16 @@ Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
 Ext.define('PVE.sdn.SubnetEdit', {
     extend: 'Proxmox.window.Edit',
 
+    onlineHelp: 'pvesdn_config_subnet',
     subject: gettext('Subnet'),
 
     subnet: undefined,
+    cidr: undefined,
 
     width: 350,
 
     base_url: undefined,
+    zoneType: undefined,
 
     bodyPadding: 0,
 
@@ -272,22 +379,41 @@ Ext.define('PVE.sdn.SubnetEdit', {
             name: 'dhcp-range',
         });
 
+        let tabItems = [ipanel, dhcpPanel];
+        let ipv6Panel;
+        let showIpv6Panel = me.zoneType === 'evpn';
+        if (showIpv6Panel && !me.isCreate && me.cidr) {
+            let addr = me.cidr.split('/')[0];
+            showIpv6Panel = Proxmox.Utils.IP6_match.test(addr);
+        }
+        if (showIpv6Panel) {
+            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);
+                    ipv6Panel?.updateForCidr(response.result.data.cidr);
                 },
             });
         }
diff --git a/www/manager6/sdn/SubnetView.js b/www/manager6/sdn/SubnetView.js
index c61458e02457..e9e601bb90bb 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();
@@ -49,7 +51,9 @@ Ext.define(
                 let win = Ext.create('PVE.sdn.SubnetEdit', {
                     autoShow: true,
                     subnet: rec.data.subnet,
+                    cidr: rec.data.cidr,
                     base_url: me.base_url,
+                    zoneType: me.zone_type,
                 });
                 win.on('destroy', reload);
             };
@@ -62,6 +66,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 1c2f5293aa65..28fb8a63b738 100644
--- a/www/manager6/sdn/VnetEdit.js
+++ b/www/manager6/sdn/VnetEdit.js
@@ -80,6 +80,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');
                 },
             },
         },
@@ -145,14 +148,193 @@ Ext.define('PVE.sdn.VnetInputPanel', {
     },
 });
 
+Ext.define('PVE.sdn.VnetIPv6RAPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+        me.updateGatedState();
+    },
+
+    onGetValues: function (values) {
+        let me = this;
+
+        if (values['ipv6-ra-rdnss']) {
+            values['ipv6-ra-rdnss'] = values['ipv6-ra-rdnss']
+                .split(/[\s,]+/)
+                .filter((v) => v.length > 0);
+        }
+
+        if (me.isCreate) {
+            for (let name of ['ipv6-ra', 'ipv6-ra-managed', 'ipv6-ra-other']) {
+                if (String(values[name]) === '0') {
+                    delete values[name];
+                }
+            }
+            return values;
+        }
+
+        let addDelete = (key) => {
+            if (values.delete) {
+                if (Ext.isArray(values.delete)) {
+                    values.delete.push(key);
+                } else {
+                    values.delete = [values.delete, key];
+                }
+            } else {
+                values.delete = [key];
+            }
+            delete values[key];
+        };
+
+        for (let name of [
+            'ipv6-ra',
+            'ipv6-ra-managed',
+            'ipv6-ra-other',
+            'ipv6-ra-rdnss',
+            'ipv6-ra-router-lifetime',
+            'ipv6-ra-interval',
+            'ipv6-ra-mtu',
+        ]) {
+            if (me.down(`[name=${name}]`).isDisabled()) {
+                addDelete(name);
+            }
+        }
+
+        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: Proxmox.Utils.defaultText,
+            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;
@@ -169,10 +351,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();
@@ -182,6 +378,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 1c576db66158..ce8c314eba6c 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-10-29 15:42 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 15:41 [PATCH docs/manager/network/proxmox{-perl-rs,-ve-rs} v5 0/9] add IPv6 RA / SLAAC support to EVPN zones Hannes Laimer
2026-10-29 15:41 ` [PATCH proxmox-perl-rs v5 1/9] pve-rs: sdn: add IPv6 RA builder binding Hannes Laimer
2026-10-29 15:41 ` [PATCH proxmox-ve-rs v5 2/9] frr: add IPv6 router advertisement support Hannes Laimer
2026-10-29 15:41 ` [PATCH proxmox-ve-rs v5 3/9] ve-config: sdn: add IPv6 router advertisement settings Hannes Laimer
2026-10-29 15:41 ` Hannes Laimer [this message]
2026-10-29 15:41 ` [PATCH pve-network v5 5/9] sdn: evpn: derive IP version from CIDR for gateway-less subnets Hannes Laimer
2026-10-29 15:41 ` [PATCH pve-network v5 6/9] sdn: evpn: accept untracked IPv6 neighbor advertisements Hannes Laimer
2026-10-29 15:41 ` [PATCH pve-network v5 7/9] sdn: evpn: add IPv6 RA / SLAAC support Hannes Laimer
2026-10-29 15:41 ` [PATCH pve-network v5 8/9] api: vnets: include the zone type in the vnet list Hannes Laimer
2026-10-29 15:41 ` [PATCH pve-docs v5 9/9] sdn: add IPv6 RA / SLAAC section Hannes Laimer
2026-10-30 15:07 ` [PATCH docs/manager/network/proxmox{-perl-rs,-ve-rs} v5 0/9] add IPv6 RA / SLAAC support to EVPN zones Lukas Sichert
2026-10-30 16:30   ` Lukas Sichert

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=20261029154148.2882594-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