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 C1A051FF146 for ; Tue, 23 Jun 2026 14:58:03 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 406E734D37; Tue, 23 Jun 2026 14:57:36 +0200 (CEST) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v3 4/9] ui: sdn: add IPv6 RA / SLAAC support Date: Tue, 23 Jun 2026 14:56:21 +0200 Message-ID: <20260623125626.1195681-5-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260623125626.1195681-1-h.laimer@proxmox.com> References: <20260623125626.1195681-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: 1782219403720 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.086 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: YWOUBJA22LV34B6OB3WSLO5QSDEIG3XD X-Message-ID-Hash: YWOUBJA22LV34B6OB3WSLO5QSDEIG3XD 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: Expose the per-vnet RA settings and per-subnet per-prefix overrides in the SDN edit dialogs, matching the schema split. Add an "IPv6 Router Advertisement" tab to the vnet edit, enabled only for EVPN zones, and an "IPv6 Prefix Options" tab to the subnet edit, enabled only for IPv6 subnets, with the autonomous (SLAAC) flag further gated on /64. Signed-off-by: Hannes Laimer --- 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 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..020df23e 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 c61458e0..e9e601bb 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 1c2f5293..a475d140 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: '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; @@ -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 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