X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FAPI2%2FFirewall%2FIPSet.pm;h=068c1a41ca1821bb11a7f71cab0b8f4718d5bacd;hb=43be6155b467d6ba1170c0740cbf5eda88c1ab40;hp=856e5f854a351c3c5b7fda2aae81191acd51ab46;hpb=e74a87f5ce727e5e3e87869d6bd5f578eaef6a29;p=pve-firewall.git diff --git a/src/PVE/API2/Firewall/IPSet.pm b/src/PVE/API2/Firewall/IPSet.pm index 856e5f8..068c1a4 100644 --- a/src/PVE/API2/Firewall/IPSet.pm +++ b/src/PVE/API2/Firewall/IPSet.pm @@ -12,7 +12,7 @@ use base qw(PVE::RESTHandler); my $api_properties = { cidr => { description => "Network/IP specification in CIDR format.", - type => 'string', format => 'IPv4orCIDR', + type => 'string', format => 'IPorCIDRorAlias', }, name => get_standard_option('ipset-name'), comment => { @@ -30,15 +30,33 @@ sub load_config { die "implement this in subclass"; - #return ($fw_conf, $rules); + #return ($cluster_conf, $fw_conf, $ipset); } -sub save_rules { - my ($class, $param, $fw_conf, $rules) = @_; +sub save_config { + my ($class, $param, $fw_conf) = @_; die "implement this in subclass"; } +sub rule_env { + my ($class, $param) = @_; + + die "implement this in subclass"; +} + +sub save_ipset { + my ($class, $param, $fw_conf, $ipset) = @_; + + if (!defined($ipset)) { + delete $fw_conf->{ipset}->{$param->{name}}; + } else { + $fw_conf->{ipset}->{$param->{name}} = $ipset; + } + + $class->save_config($param, $fw_conf); +} + my $additional_param_hash = {}; sub additional_parameters { @@ -67,6 +85,7 @@ sub register_get_ipset { path => '', method => 'GET', description => "List IPSet content", + permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), parameters => { additionalProperties => 0, properties => $properties, @@ -86,7 +105,8 @@ sub register_get_ipset { nomatch => { type => 'boolean', optional => 1, - }, + }, + digest => get_standard_option('pve-config-digest', { optional => 0} ), }, }, links => [ { rel => 'child', href => "{cidr}" } ], @@ -94,9 +114,42 @@ sub register_get_ipset { code => sub { my ($param) = @_; - my ($fw_conf, $ipset) = $class->load_config($param); + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); + + return PVE::Firewall::copy_list_with_digest($ipset); + }}); +} + +sub register_delete_ipset { + my ($class) = @_; + + my $properties = $class->additional_parameters(); + + $properties->{name} = get_standard_option('ipset-name'); + + $class->register_method({ + name => 'delete_ipset', + path => '', + method => 'DELETE', + description => "Delete IPSet", + protected => 1, + permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), + parameters => { + additionalProperties => 0, + properties => $properties, + }, + returns => { type => 'null' }, + code => sub { + my ($param) = @_; + + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); + + die "IPSet '$param->{name}' is not empty\n" + if scalar(@$ipset); + + $class->save_ipset($param, $fw_conf, undef); - return $ipset; + return undef; }}); } @@ -109,13 +162,14 @@ sub register_create_ip { $properties->{cidr} = $api_properties->{cidr}; $properties->{nomatch} = $api_properties->{nomatch}; $properties->{comment} = $api_properties->{comment}; - + $class->register_method({ name => 'create_ip', path => '', method => 'POST', description => "Add IP or Network to IPSet.", protected => 1, + permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), parameters => { additionalProperties => 0, properties => $properties, @@ -124,7 +178,7 @@ sub register_create_ip { code => sub { my ($param) = @_; - my ($fw_conf, $ipset) = $class->load_config($param); + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); my $cidr = $param->{cidr}; @@ -133,7 +187,15 @@ sub register_create_ip { if $entry->{cidr} eq $cidr; } + raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" }) + if $cidr =~ m!/0+$!; + + # make sure alias exists (if $cidr is an alias) + PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $cidr) + if $cidr =~ m/^${PVE::Firewall::ip_alias_pattern}$/; + my $data = { cidr => $cidr }; + $data->{nomatch} = 1 if $param->{nomatch}; $data->{comment} = $param->{comment} if $param->{comment}; @@ -158,6 +220,7 @@ sub register_read_ip { path => '{cidr}', method => 'GET', description => "Read IP or Network settings from IPSet.", + permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), protected => 1, parameters => { additionalProperties => 0, @@ -167,10 +230,14 @@ sub register_read_ip { code => sub { my ($param) = @_; - my ($fw_conf, $ipset) = $class->load_config($param); + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); - foreach my $entry (@$ipset) { - return $entry if $entry->{cidr} eq $param->{cidr}; + my $list = PVE::Firewall::copy_list_with_digest($ipset); + + foreach my $entry (@$list) { + if ($entry->{cidr} eq $param->{cidr}) { + return $entry; + } } raise_param_exc({ cidr => "no such IP/Network" }); @@ -186,13 +253,15 @@ sub register_update_ip { $properties->{cidr} = $api_properties->{cidr}; $properties->{nomatch} = $api_properties->{nomatch}; $properties->{comment} = $api_properties->{comment}; - + $properties->{digest} = get_standard_option('pve-config-digest'); + $class->register_method({ name => 'update_ip', path => '{cidr}', method => 'PUT', description => "Update IP or Network settings", protected => 1, + permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), parameters => { additionalProperties => 0, properties => $properties, @@ -201,7 +270,10 @@ sub register_update_ip { code => sub { my ($param) = @_; - my ($fw_conf, $ipset) = $class->load_config($param); + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); + + my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset); + PVE::Tools::assert_if_modified($digest, $param->{digest}); foreach my $entry (@$ipset) { if($entry->{cidr} eq $param->{cidr}) { @@ -223,13 +295,15 @@ sub register_delete_ip { $properties->{name} = $api_properties->{name}; $properties->{cidr} = $api_properties->{cidr}; - + $properties->{digest} = get_standard_option('pve-config-digest'); + $class->register_method({ name => 'remove_ip', path => '{cidr}', method => 'DELETE', description => "Remove IP or Network from IPSet.", protected => 1, + permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), parameters => { additionalProperties => 0, properties => $properties, @@ -238,7 +312,10 @@ sub register_delete_ip { code => sub { my ($param) = @_; - my ($fw_conf, $ipset) = $class->load_config($param); + my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); + + my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset); + PVE::Tools::assert_if_modified($digest, $param->{digest}); my $new = []; @@ -255,6 +332,7 @@ sub register_delete_ip { sub register_handlers { my ($class) = @_; + $class->register_delete_ipset(); $class->register_get_ipset(); $class->register_create_ip(); $class->register_read_ip(); @@ -269,6 +347,12 @@ use warnings; use base qw(PVE::API2::Firewall::IPSetBase); +sub rule_env { + my ($class, $param) = @_; + + return 'cluster'; +} + sub load_config { my ($class, $param) = @_; @@ -276,18 +360,93 @@ sub load_config { my $ipset = $fw_conf->{ipset}->{$param->{name}}; die "no such IPSet '$param->{name}'\n" if !defined($ipset); - return ($fw_conf, $ipset); + return (undef, $fw_conf, $ipset); } -sub save_ipset { - my ($class, $param, $fw_conf, $ipset) = @_; +sub save_config { + my ($class, $param, $fw_conf) = @_; - $fw_conf->{ipset}->{$param->{name}} = $ipset; PVE::Firewall::save_clusterfw_conf($fw_conf); } __PACKAGE__->register_handlers(); +package PVE::API2::Firewall::VMIPset; + +use strict; +use warnings; +use PVE::JSONSchema qw(get_standard_option); + +use base qw(PVE::API2::Firewall::IPSetBase); + +sub rule_env { + my ($class, $param) = @_; + + return 'vm'; +} + +__PACKAGE__->additional_parameters({ + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), +}); + +sub load_config { + my ($class, $param) = @_; + + my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); + my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); + my $ipset = $fw_conf->{ipset}->{$param->{name}}; + die "no such IPSet '$param->{name}'\n" if !defined($ipset); + + return ($cluster_conf, $fw_conf, $ipset); +} + +sub save_config { + my ($class, $param, $fw_conf) = @_; + + PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); +} + +__PACKAGE__->register_handlers(); + +package PVE::API2::Firewall::CTIPset; + +use strict; +use warnings; +use PVE::JSONSchema qw(get_standard_option); + +use base qw(PVE::API2::Firewall::IPSetBase); + +sub rule_env { + my ($class, $param) = @_; + + return 'ct'; +} + +__PACKAGE__->additional_parameters({ + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), +}); + +sub load_config { + my ($class, $param) = @_; + + my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); + my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); + my $ipset = $fw_conf->{ipset}->{$param->{name}}; + die "no such IPSet '$param->{name}'\n" if !defined($ipset); + + return ($cluster_conf, $fw_conf, $ipset); +} + +sub save_config { + my ($class, $param, $fw_conf) = @_; + + PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); +} + +__PACKAGE__->register_handlers(); + package PVE::API2::Firewall::BaseIPSetList; use strict; @@ -298,16 +457,75 @@ use PVE::Firewall; use base qw(PVE::RESTHandler); +sub load_config { + my ($class, $param) = @_; + + die "implement this in subclass"; + + #return ($cluster_conf, $fw_conf); +} + +sub save_config { + my ($class, $param, $fw_conf) = @_; + + die "implement this in subclass"; +} + +sub rule_env { + my ($class, $param) = @_; + + die "implement this in subclass"; +} + +my $additional_param_hash_list = {}; + +sub additional_parameters { + my ($class, $new_value) = @_; + + if (defined($new_value)) { + $additional_param_hash_list->{$class} = $new_value; + } + + # return a copy + my $copy = {}; + my $org = $additional_param_hash_list->{$class} || {}; + foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; } + return $copy; +} + +my $get_ipset_list = sub { + my ($fw_conf) = @_; + + my $res = []; + foreach my $name (sort keys %{$fw_conf->{ipset}}) { + my $data = { + name => $name, + }; + if (my $comment = $fw_conf->{ipset_comments}->{$name}) { + $data->{comment} = $comment; + } + push @$res, $data; + } + + my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res); + + return wantarray ? ($list, $digest) : $list; +}; + sub register_index { my ($class) = @_; + my $properties = $class->additional_parameters(); + $class->register_method({ name => 'ipset_index', path => '', method => 'GET', description => "List IPSets", + permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), parameters => { additionalProperties => 0, + properties => $properties, }, returns => { type => 'array', @@ -315,6 +533,11 @@ sub register_index { type => "object", properties => { name => get_standard_option('ipset-name'), + digest => get_standard_option('pve-config-digest', { optional => 0} ), + comment => { + type => 'string', + optional => 1, + } }, }, links => [ { rel => 'child', href => "{name}" } ], @@ -322,128 +545,202 @@ sub register_index { code => sub { my ($param) = @_; - my $fw_conf = $class->load_config(); - - my $res = []; - foreach my $name (keys %{$fw_conf->{ipset}}) { - push @$res, { name => $name, count => scalar(@{$fw_conf->{ipset}->{$name}}) }; - } + my ($cluster_conf, $fw_conf) = $class->load_config($param); - return $res; + return &$get_ipset_list($fw_conf); }}); } sub register_create { my ($class) = @_; + my $properties = $class->additional_parameters(); + + $properties->{name} = get_standard_option('ipset-name'); + + $properties->{comment} = { type => 'string', optional => 1 }; + + $properties->{digest} = get_standard_option('pve-config-digest'); + + $properties->{rename} = get_standard_option('ipset-name', { + description => "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.", + optional => 1 }); + $class->register_method({ name => 'create_ipset', path => '', method => 'POST', description => "Create new IPSet", protected => 1, + permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), parameters => { additionalProperties => 0, - properties => { - name => get_standard_option('ipset-name'), - rename => get_standard_option('ipset-name', { - description => "Rename an existing IPSet.", - optional => 1, - }), - } + properties => $properties, }, returns => { type => 'null' }, code => sub { my ($param) = @_; - my $fw_conf = $class->load_config(); - - foreach my $name (keys %{$fw_conf->{ipset}}) { - raise_param_exc({ name => "IPSet '$name' already exists" }) - if $name eq $param->{name}; - } + my ($cluster_conf, $fw_conf) = $class->load_config($param); if ($param->{rename}) { + my (undef, $digest) = &$get_ipset_list($fw_conf); + PVE::Tools::assert_if_modified($digest, $param->{digest}); + raise_param_exc({ name => "IPSet '$param->{rename}' does not exists" }) if !$fw_conf->{ipset}->{$param->{rename}}; + + # prevent overwriting existing ipset + raise_param_exc({ name => "IPSet '$param->{name}' does already exist"}) + if $fw_conf->{ipset}->{$param->{name}} && + $param->{name} ne $param->{rename}; + my $data = delete $fw_conf->{ipset}->{$param->{rename}}; $fw_conf->{ipset}->{$param->{name}} = $data; - } else { + if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) { + $fw_conf->{ipset_comments}->{$param->{name}} = $comment; + } + $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment}); + } else { + foreach my $name (keys %{$fw_conf->{ipset}}) { + raise_param_exc({ name => "IPSet '$name' already exists" }) + if $name eq $param->{name}; + } + $fw_conf->{ipset}->{$param->{name}} = []; + $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment}); } - $class->save_config($fw_conf); + $class->save_config($param, $fw_conf); return undef; }}); } -sub register_delete { +sub register_handlers { my ($class) = @_; - $class->register_method({ - name => 'delete_ipset', - path => '{name}', - method => 'DELETE', - description => "Delete IPSet", - protected => 1, - parameters => { - additionalProperties => 0, - properties => { - name => get_standard_option('ipset-name'), - } - }, - returns => { type => 'null' }, - code => sub { - my ($param) = @_; - - my $fw_conf = $class->load_config(); + $class->register_index(); + $class->register_create(); +} - return undef if !$fw_conf->{ipset}->{$param->{name}}; +package PVE::API2::Firewall::ClusterIPSetList; - die "IPSet '$param->{name}' is not empty\n" - if scalar(@{$fw_conf->{ipset}->{$param->{name}}}); +use strict; +use warnings; +use PVE::Firewall; - delete $fw_conf->{ipset}->{$param->{name}}; +use base qw(PVE::API2::Firewall::BaseIPSetList); - $class->save_config($fw_conf); +sub rule_env { + my ($class, $param) = @_; + + return 'cluster'; +} - return undef; - }}); +sub load_config { + my ($class, $param) = @_; + + my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); + return (undef, $cluster_conf); } -sub register_handlers { - my ($class) = @_; +sub save_config { + my ($class, $param, $fw_conf) = @_; - $class->register_index(); - $class->register_create(); - $class->register_delete(); + PVE::Firewall::save_clusterfw_conf($fw_conf); } -package PVE::API2::Firewall::ClusterIPSetList; +__PACKAGE__->register_handlers(); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Firewall::ClusterIPset", + path => '{name}', + # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' + fragmentDelimiter => '', +}); + +package PVE::API2::Firewall::VMIPSetList; use strict; use warnings; +use PVE::JSONSchema qw(get_standard_option); use PVE::Firewall; use base qw(PVE::API2::Firewall::BaseIPSetList); +__PACKAGE__->additional_parameters({ + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), +}); + +sub rule_env { + my ($class, $param) = @_; + + return 'vm'; +} + sub load_config { - my ($class) = @_; + my ($class, $param) = @_; - return PVE::Firewall::load_clusterfw_conf(); + my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); + my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); + return ($cluster_conf, $fw_conf); } sub save_config { - my ($class, $fw_conf) = @_; + my ($class, $param, $fw_conf) = @_; - PVE::Firewall::save_clusterfw_conf($fw_conf); + PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); } __PACKAGE__->register_handlers(); __PACKAGE__->register_method ({ - subclass => "PVE::API2::Firewall::ClusterIPset", + subclass => "PVE::API2::Firewall::VMIPset", + path => '{name}', + # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' + fragmentDelimiter => '', +}); + +package PVE::API2::Firewall::CTIPSetList; + +use strict; +use warnings; +use PVE::JSONSchema qw(get_standard_option); +use PVE::Firewall; + +use base qw(PVE::API2::Firewall::BaseIPSetList); + +__PACKAGE__->additional_parameters({ + node => get_standard_option('pve-node'), + vmid => get_standard_option('pve-vmid'), +}); + +sub rule_env { + my ($class, $param) = @_; + + return 'ct'; +} + +sub load_config { + my ($class, $param) = @_; + + my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); + my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); + return ($cluster_conf, $fw_conf); +} + +sub save_config { + my ($class, $param, $fw_conf) = @_; + + PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); +} + +__PACKAGE__->register_handlers(); + +__PACKAGE__->register_method ({ + subclass => "PVE::API2::Firewall::CTIPset", path => '{name}', # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' fragmentDelimiter => '',