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 71E351FF13C for ; Thu, 30 Apr 2026 16:30:45 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4757CBB04; Thu, 30 Apr 2026 16:30:09 +0200 (CEST) From: Hannes Laimer 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 Message-ID: <20260430142953.315412-10-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260430142953.315412-1-h.laimer@proxmox.com> References: <20260430142953.315412-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: 1777559297116 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.080 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: 22YRAXYMD2NN6KYOU5B735WTOXE5NDKY X-Message-ID-Hash: 22YRAXYMD2NN6KYOU5B735WTOXE5NDKY 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: 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 --- 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