From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 353A31FF0E3 for ; Tue, 27 Oct 2026 10:01:46 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id ACEF8216E5; Tue, 27 Oct 2026 09:57:09 +0100 (CET) From: Hannes Laimer To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v3 38/45] ui: sdn: microseg: add narrowing to the rule form Date: Tue, 27 Oct 2026 09:55:20 +0100 Message-ID: <20261027085527.702738-39-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261027085527.702738-1-h.laimer@proxmox.com> References: <20261027085527.702738-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: 1793091374933 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.348 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: YI7ZCW6XS5PLBNZPXK24LEKQEJ3AJ7UK X-Message-ID-Hash: YI7ZCW6XS5PLBNZPXK24LEKQEJ3AJ7UK 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: Reuse the firewall's macro, protocol and ICMP type selectors, so a narrowed rule is entered the same way as a firewall rule. Signed-off-by: Hannes Laimer --- www/manager6/grid/FirewallRules.js | 2 + www/manager6/sdn/microseg/RuleEdit.js | 111 +++++++++++++++++++++++++- www/manager6/sdn/microseg/RuleView.js | 42 +++++++++- 3 files changed, 151 insertions(+), 4 deletions(-) diff --git a/www/manager6/grid/FirewallRules.js b/www/manager6/grid/FirewallRules.js index 0db817ebce0e..d408bd00b7f0 100644 --- a/www/manager6/grid/FirewallRules.js +++ b/www/manager6/grid/FirewallRules.js @@ -81,6 +81,7 @@ Ext.define('PVE.form.ICMPTypeSelector', { }); let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', { + storeId: 'pve-icmp-type-names', field: ['type', 'name'], data: [ { type: 'any', name: 'any' }, @@ -123,6 +124,7 @@ let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', { ], }); let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', { + storeId: 'pve-icmpv6-type-names', field: ['type', 'name'], data: [ { type: '1', name: 'destination-unreachable' }, diff --git a/www/manager6/sdn/microseg/RuleEdit.js b/www/manager6/sdn/microseg/RuleEdit.js index 4ec5467f61be..3ee9c3b36e46 100644 --- a/www/manager6/sdn/microseg/RuleEdit.js +++ b/www/manager6/sdn/microseg/RuleEdit.js @@ -60,6 +60,37 @@ Ext.define('PVE.sdn.microseg.RuleInputPanel', { }, }); + let setNarrowing = (field, enabled, clear) => { + field.setDisabled(!enabled); + if (clear) { + field.setValue(''); + } + }; + let updateNarrowing = () => { + let macro = me.down('field[name=macro]'); + let proto = me.down('field[name=proto]'); + let byMacro = !Ext.isEmpty(macro.getValue()); + if (me.isCreate) { + setNarrowing(proto, !byMacro, byMacro); + } + let editable = me.isCreate && !byMacro; + let protocol = proto.getValue(); + let icmpv4 = protocol === 'icmp'; + let icmpv6 = protocol === 'icmpv6' || protocol === 'ipv6-icmp'; + let v4 = me.down('#microsegIcmpv4Type'); + let v6 = me.down('#microsegIcmpv6Type'); + v4.setHidden(!icmpv4); + setNarrowing(v4, icmpv4 && editable); + v6.setHidden(!icmpv6); + setNarrowing(v6, icmpv6 && editable); + let ports = !icmpv4 && !icmpv6; + for (const name of ['sport', 'dport']) { + let field = me.down(`field[name=${name}]`); + field.setHidden(!ports); + setNarrowing(field, ports && editable, me.isCreate && !editable); + } + }; + me.column1 = [ me.srcSelector, { @@ -76,6 +107,69 @@ Ext.define('PVE.sdn.microseg.RuleInputPanel', { }, }, }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '', + }, + { + xtype: 'proxmoxtextfield', + name: 'sport', + fieldLabel: gettext('Source port'), + disabled: !me.isCreate, + }, + { + xtype: 'pveICMPTypeSelector', + name: 'icmp-type', + itemId: 'microsegIcmpv4Type', + autoSelect: false, + editable: true, + hidden: true, + disabled: true, + value: '', + skipEmptyText: true, + fieldLabel: gettext('ICMP type'), + store: 'pve-icmp-type-names', + }, + { + xtype: 'pveICMPTypeSelector', + name: 'icmp-type', + itemId: 'microsegIcmpv6Type', + autoSelect: false, + editable: true, + hidden: true, + disabled: true, + value: '', + skipEmptyText: true, + fieldLabel: gettext('ICMP type'), + store: 'pve-icmpv6-type-names', + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + fieldLabel: gettext('Macro'), + editable: true, + allowBlank: true, + skipEmptyText: true, + disabled: !me.isCreate, + listeners: { + change: updateNarrowing, + }, + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + skipEmptyText: true, + fieldLabel: gettext('Protocol'), + disabled: !me.isCreate, + listeners: { + change: updateNarrowing, + }, + }, { xtype: 'proxmoxcheckbox', name: 'bidirectional', @@ -85,7 +179,7 @@ Ext.define('PVE.sdn.microseg.RuleInputPanel', { autoEl: { tag: 'div', 'data-qtip': gettext( - 'Also apply the rule with source and destination swapped.', + 'Also apply the rule with source and destination swapped, ports included.', ), }, }, @@ -108,6 +202,19 @@ Ext.define('PVE.sdn.microseg.RuleInputPanel', { }, }, }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '', + }, + { + xtype: 'proxmoxtextfield', + name: 'dport', + fieldLabel: gettext('Dest. port'), + hideMode: 'visibility', + disabled: !me.isCreate, + }, { xtype: 'proxmoxintegerfield', name: 'prio', @@ -119,7 +226,7 @@ Ext.define('PVE.sdn.microseg.RuleInputPanel', { autoEl: { tag: 'div', 'data-qtip': gettext( - 'The highest priority wins. At a tie, the most specific rules decide and a deny wins among equals.', + 'The highest priority wins. At a tie, the most specific rules decide and a deny wins among equals. A narrowed rule is checked ahead of one without any narrowing at the same priority.', ), }, }, diff --git a/www/manager6/sdn/microseg/RuleView.js b/www/manager6/sdn/microseg/RuleView.js index 57ca0eddc1c9..016e1dc4455e 100644 --- a/www/manager6/sdn/microseg/RuleView.js +++ b/www/manager6/sdn/microseg/RuleView.js @@ -36,6 +36,7 @@ Ext.define( inbound = sel.inbound.has(data.id); } return { + rank: me.rankOf(data), direction: out && inbound ? 'both' : out ? 'out' : inbound ? 'in' : '', }; }, @@ -91,7 +92,10 @@ Ext.define( sortProperty: 'prio', direction: 'DESC', }, - sorters: [{ property: 'id', direction: 'ASC' }], + sorters: [ + { property: 'rank', direction: 'ASC' }, + { property: 'id', direction: 'ASC' }, + ], }); let matchWords = { @@ -149,6 +153,26 @@ Ext.define( return rec.data.bidirectional ? bothWaysIcon : arrowIcon; }; + let matchText = (rec) => { + let data = rec.data; + if (data.macro) { + return data.macro; + } + let proto = data.proto; + if (!proto) { + return ''; + } + let text = proto; + if (data['icmp-type']) { + text += ` ${data['icmp-type']}`; + } else if (data.sport) { + text += ` ${data.sport} -> ${data.dport || '*'}`; + } else if (data.dport) { + text += `/${data.dport}`; + } + return text; + }; + let ruleName = (rec) => { let arrow = rec.data.bidirectional ? '<->' : '->'; let src = PVE.Utils.format_sdnmicroseg_predicate( @@ -159,7 +183,9 @@ Ext.define( rec.data['dst-match'], rec.data.dst, ); - return `${src} ${arrow} ${dst}`; + let name = `${src} ${arrow} ${dst}`; + let match = matchText(rec); + return match ? `${name} (${match})` : name; }; me.columns = [ @@ -189,6 +215,12 @@ Ext.define( flex: 2, renderer: (v, m, rec) => renderPredicate(rec.data['dst-match'], v), }, + { + text: gettext('Match'), + dataIndex: 'proto', + flex: 1, + renderer: (v, m, rec) => Ext.String.htmlEncode(matchText(rec)), + }, { xtype: 'pveSDNMicrosegStateColumn' }, { text: gettext('Comment'), @@ -235,7 +267,13 @@ Ext.define( 'prio', 'allow', 'bidirectional', + 'proto', + 'sport', + 'dport', + 'icmp-type', + 'macro', 'direction', + 'rank', ], }); }, -- 2.47.3