public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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.





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