From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: Re: [PATCH pve-firewall 2/5] ipset: Add option to update references on rename/delete
Date: Thu, 16 Jul 2026 15:17:25 +0200 [thread overview]
Message-ID: <055351eb-f575-46c3-ac37-01925306c449@proxmox.com> (raw)
In-Reply-To: <20260407073658.90818-3-a.bied-charreton@proxmox.com>
On 4/7/26 9:36 AM, Arthur Bied-Charreton wrote:
> Renaming or deleting an IPSet that is referenced by rules or security
> groups would leave dangling references, creating broken configurations.
>
> Add option to the POST and DELETE endpoints for /firewall/ipset to
> update or delete those references from cluster, hosts, and guests
> firewall configs.
>
> If these endpoints are hit from the cluster environment (/cluster/),
> all firewall config files in the cluster (cluster + all nodes + all
> guests) have to be checked and possibly updated.
>
> Renaming a cluster ipset referenced by 10.000 different config files in
> a 3-node test cluster takes about 4.8 seconds on my machine. Doing the
> same with an unreferenced ipset (i.e. config files checked but not
> written back due to lack of changes) takes about 2 seconds.
>
> Signed-off-by: Arthur Bied-Charreton <a.bied-charreton@proxmox.com>
> ---
> src/PVE/API2/Firewall/IPSet.pm | 108 ++++++++++++++++++++++++++++++++-
> 1 file changed, 107 insertions(+), 1 deletion(-)
>
> diff --git a/src/PVE/API2/Firewall/IPSet.pm b/src/PVE/API2/Firewall/IPSet.pm
> index 43a51a7..9896fdd 100644
> --- a/src/PVE/API2/Firewall/IPSet.pm
> +++ b/src/PVE/API2/Firewall/IPSet.pm
> @@ -5,6 +5,7 @@ use warnings;
> use PVE::Exception qw(raise raise_param_exc);
> use PVE::JSONSchema qw(get_standard_option);
>
> +use PVE::API2::Firewall::Helpers qw(filter_map foreach_conf_in_env);
> use PVE::Firewall;
>
> use base qw(PVE::RESTHandler);
> @@ -128,6 +129,92 @@ sub register_get_ipset {
> });
> }
>
> +# Apply $rewrite to each rule in $conf referencing $ipset, modifying $conf in place.
> +#
> +# $rewrite ->($rule) is expected to return the new rule, or undef if the goal is to remove matches.
> +#
> +# Returns a boolean indicating whether $conf was modified.
> +#
> +# If $is_guest is set to 1, references will be solved assuming $conf is a guest FW config.
> +# This is important because 2 different IPSets with the same name may be defined at the
> +# cluster level and at the guest level, in which case the guest's IPSet shadows the cluster's.
> +sub rewrite_ipset_refs_in_conf {
> + my ($conf, $ipset, $rule_env, $is_guest, $rewrite) = @_;
> + my $modified = 0;
> +
> + my @patterns;
> + if ($rule_env eq 'cluster') {
> + @patterns = ("+dc/$ipset");
> + # Guest config ipsets may shadow cluster config ones
> + push @patterns, "+$ipset" if !($is_guest && $conf->{ipset}->{$ipset});
> + } else {
> + @patterns = ("+$ipset", "+guest/$ipset");
> + }
> +
> + my $rule_matches = sub {
> + my ($rule) = @_;
> + grep { ($rule->{source} // '') eq $_ || ($rule->{dest} // '') eq $_ } @patterns;
> + };
> +
> + ($conf->{rules}, my $changed) = filter_map($conf->{rules}, $rewrite, $rule_matches);
> + $modified ||= $changed;
> +
> + if ($rule_env eq 'cluster') {
> + my $groups = $conf->{groups} // {};
> + for my $group (keys %$groups) {
> + ($groups->{$group}, $changed) = filter_map($groups->{$group}, $rewrite, $rule_matches);
> + $modified ||= $changed;
> + }
> + }
> +
> + return $modified;
> +}
> +
> +# Apply $rewrite to each rule in the current environment ($rule_env) referencing $ipset, modifying
> +# $conf in place. The caller is responsible for locking and saving $conf.
> +#
> +# If $rule_env is 'cluster', this function will also map over all guest and node firewall configs
> +# in the cluster, locking and saving them in the process, since those may reference IPSets defined
> +# in the cluster config.
> +sub rewrite_ipset_refs_in_env {
> + my ($conf, $ipset, $rule_env, $rewrite) = @_;
> + return foreach_conf_in_env(
> + $conf,
> + $rule_env,
> + sub {
> + my ($fw_conf, $effective_env, $is_guest) = @_;
> + return rewrite_ipset_refs_in_conf(
> + $fw_conf, $ipset, $effective_env, $is_guest, $rewrite,
> + );
> + },
> + );
> +}
> +
> +sub delete_ipset_refs {
> + my ($conf, $ipset, $rule_env) = @_;
> + return rewrite_ipset_refs_in_env($conf, lc($ipset), $rule_env, sub { undef });
> +}
> +
> +sub rename_ipset_refs {
> + my ($conf, $ipset, $new_name, $rule_env) = @_;
> + my $lc_ipset = lc($ipset);
> + return rewrite_ipset_refs_in_env(
> + $conf,
> + $lc_ipset,
> + $rule_env,
> + sub {
> + my ($rule) = @_;
> + for my $field (qw(source dest)) {
> + my $val = lc($rule->{$field} // '');
> + if ($val eq "+dc/$lc_ipset") { $rule->{$field} = "+dc/$new_name" }
> + elsif ($val eq "+$lc_ipset") { $rule->{$field} = "+$new_name" }
> + elsif ($val eq "+guest/$lc_ipset") { $rule->{$field} = "+guest/$new_name" }
In the Firewall parsing logic itself (verify_rule in Firewall.pm), we do
use regex matching:
$value =~ m@^\+(guest/|dc/|sdn/)?(${ipset_name_pattern})$@
We could utilize that to rewrite this as:
if ($value =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
$rule->{$field} = "$1/$new_name" if $2 eq $lc_ipset;
}
but that might be a bit overkill / overcomplicated.
I just figured I'd mention it since i've often written similar if /
elsif chains myself that might have been better expressed by utilizing
perl regexes.
> + }
> + return $rule;
> + },
> + );
> +}
> +
All the helpers contained here, might better fit in the general Helper
module, rather than the API module itself. Particularly if we implement
similar functionality when deleting SDN VNets, pve-network would be able
to use the Helper module instead of having to import API modules.
> sub register_delete_ipset {
> my ($class) = @_;
>
> @@ -139,6 +226,12 @@ sub register_delete_ipset {
> optional => 1,
> description => 'Delete all members of the IPSet, if there are any.',
> };
> + $properties->{'delete-references'} = {
> + type => 'boolean',
> + optional => 1,
> + description => 'Delete dangling references after deleting the IPSet',
> + default => 0,
> + };
>
> $class->register_method({
> name => 'delete_ipset',
> @@ -165,8 +258,10 @@ sub register_delete_ipset {
> die "IPSet '$param->{name}' is not empty\n"
> if scalar(@$ipset) && !$param->{force};
>
> - $class->save_ipset($param, $fw_conf, undef);
> + delete_ipset_refs($fw_conf, $param->{name}, $class->rule_env())
> + if $param->{'delete-references'};
>
> + $class->save_ipset($param, $fw_conf, undef);
> },
> );
>
> @@ -654,6 +749,13 @@ sub register_create {
> },
> );
>
> + $properties->{'update-references'} = {
> + type => 'boolean',
> + optional => 1,
> + description => 'Update dangling references when renaming an IPSet.',
> + default => 0,
> + };
> +
> $class->register_method({
> name => 'create_ipset',
> path => '',
> @@ -688,6 +790,10 @@ sub register_create {
> if $fw_conf->{ipset}->{ $param->{name} }
> && $param->{name} ne $param->{rename};
>
> + PVE::API2::Firewall::IPSetBase::rename_ipset_refs(
> + $fw_conf, $param->{rename}, $param->{name}, $class->rule_env(),
> + ) if $param->{'update-references'};
This suffers from timing issues, doesn't it? We update the references in
all configuration files, but only save the new IPSet *after* we have
updated all references. This leaves a window where rules get skipped by
the firewall since they reference a not-yet-existing IPSet.
In order to get atomicity we'd have to do it in two steps, wouldn't we?
* Save the new ipset and write it to the configuration, but still keep
the old one around.
* update the references one-by-one, now at every point in time a rule
references an IPSet that actually exists in the configuration
* Delete the old IPSet
Otherwise, introducing a sleep inbetween the rename call and the
save_config call seems to make this surface easily:
* Create an IPSet at cluster level
* Reference the IPSet in a host rule
* Update the name of the IPSet + update-references
Some firewall cycles fail due to the rules getting updated, but not the
IPSet name itself:
> Jul 16 14:48:28 fw-testi pve-firewall[1213]: /etc/pve/nodes/fw-testi/host.fw (line 7) - errors in rule parameters: IN ACCEPT -source +dc/owo -log nolog
> Jul 16 14:48:28 fw-testi pve-firewall[1213]: source: no such ipset 'owo'
Delete seems fine, since all references are deleted and only then the
IPSet itself afterwards.
next prev parent reply other threads:[~2026-07-16 13:18 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-07 7:36 [PATCH firewall/manager 0/5] Allow updating references to firewall objects when editing them Arthur Bied-Charreton
2026-04-07 7:36 ` [PATCH pve-firewall 1/5] Add helpers for updating alias and ipset references Arthur Bied-Charreton
2026-07-16 13:17 ` Stefan Hanreich
2026-04-07 7:36 ` [PATCH pve-firewall 2/5] ipset: Add option to update references on rename/delete Arthur Bied-Charreton
2026-07-16 13:17 ` Stefan Hanreich [this message]
2026-04-07 7:36 ` [PATCH pve-firewall 3/5] aliases: " Arthur Bied-Charreton
2026-07-16 13:17 ` Stefan Hanreich
2026-04-07 7:36 ` [PATCH pve-manager 4/5] ipset: " Arthur Bied-Charreton
2026-04-07 7:36 ` [PATCH pve-manager 5/5] aliases: " Arthur Bied-Charreton
2026-07-16 13:17 ` [PATCH firewall/manager 0/5] Allow updating references to firewall objects when editing them Stefan Hanreich
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=055351eb-f575-46c3-ac37-01925306c449@proxmox.com \
--to=s.hanreich@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.