+ $res->{$k} = $v;
+ $sha->add($k, ':', $v, "\n");
+ }
+
+ my $digest = $sha->hexdigest;
+
+ $res->{digest} = $digest;
+
+ return wantarray ? ($res, $digest) : $res;
+}
+
+sub copy_list_with_digest {
+ my ($list) = @_;
+
+ my $sha = Digest::SHA->new('sha1');
+
+ my $res = [];
+ foreach my $entry (@$list) {
+ my $data = {};
+ foreach my $k (sort keys %$entry) {
+ my $v = $entry->{$k};
+ next if !defined($v);
+ $data->{$k} = $v;
+ # Note: digest ignores refs ($rule->{errors})
+ $sha->add($k, ':', $v, "\n") if !ref($v); ;
+ }
+ push @$res, $data;
+ }
+
+ my $digest = $sha->hexdigest;
+
+ foreach my $entry (@$res) {
+ $entry->{digest} = $digest;
+ }
+
+ return wantarray ? ($res, $digest) : $res;
+}
+
+my $rule_properties = {
+ pos => {
+ description => "Update rule at position <pos>.",
+ type => 'integer',
+ minimum => 0,
+ optional => 1,
+ },
+ digest => get_standard_option('pve-config-digest'),
+ type => {
+ type => 'string',
+ optional => 1,
+ enum => ['in', 'out', 'group'],
+ },
+ action => {
+ description => "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.",
+ type => 'string',
+ optional => 1,
+ pattern => $security_group_name_pattern,
+ maxLength => 20,
+ minLength => 2,
+ },
+ macro => {
+ type => 'string',
+ optional => 1,
+ maxLength => 128,
+ },
+ iface => get_standard_option('pve-iface', { optional => 1 }),
+ source => {
+ type => 'string', format => 'pve-fw-v4addr-spec',
+ optional => 1,
+ },
+ dest => {
+ type => 'string', format => 'pve-fw-v4addr-spec',
+ optional => 1,
+ },
+ proto => {
+ type => 'string', format => 'pve-fw-protocol-spec',
+ optional => 1,
+ },
+ enable => {
+ type => 'boolean',
+ optional => 1,
+ },
+ sport => {
+ type => 'string', format => 'pve-fw-port-spec',
+ optional => 1,
+ },
+ dport => {
+ type => 'string', format => 'pve-fw-port-spec',
+ optional => 1,
+ },
+ comment => {
+ type => 'string',
+ optional => 1,
+ },
+};
+
+sub add_rule_properties {
+ my ($properties) = @_;
+
+ foreach my $k (keys %$rule_properties) {
+ my $h = $rule_properties->{$k};
+ # copy data, so that we can modify later without side effects
+ foreach my $opt (keys %$h) { $properties->{$k}->{$opt} = $h->{$opt}; }
+ }
+
+ return $properties;
+}
+
+sub delete_rule_properties {
+ my ($rule, $delete_str) = @_;
+
+ foreach my $opt (PVE::Tools::split_list($delete_str)) {
+ raise_param_exc({ 'delete' => "no such property ('$opt')"})
+ if !defined($rule_properties->{$opt});
+ raise_param_exc({ 'delete' => "unable to delete required property '$opt'"})
+ if $opt eq 'type' || $opt eq 'action';
+ delete $rule->{$opt};
+ }
+
+ return $rule;
+}
+
+my $apply_macro = sub {
+ my ($macro_name, $param, $verify) = @_;
+
+ my $macro_rules = $pve_fw_parsed_macros->{$macro_name};
+ die "unknown macro '$macro_name'\n" if !$macro_rules; # should not happen
+
+ my $rules = [];
+
+ foreach my $templ (@$macro_rules) {
+ my $rule = {};
+ my $param_used = {};
+ foreach my $k (keys %$templ) {
+ my $v = $templ->{$k};
+ if ($v eq 'PARAM') {
+ $v = $param->{$k};
+ $param_used->{$k} = 1;
+ } elsif ($v eq 'DEST') {
+ $v = $param->{dest};
+ $param_used->{dest} = 1;
+ } elsif ($v eq 'SOURCE') {
+ $v = $param->{source};
+ $param_used->{source} = 1;
+ }
+
+ if (!defined($v)) {
+ my $msg = "missing parameter '$k' in macro '$macro_name'";
+ raise_param_exc({ macro => $msg }) if $verify;
+ die "$msg\n";
+ }
+ $rule->{$k} = $v;
+ }
+ foreach my $k (keys %$param) {
+ next if $k eq 'macro';
+ next if !defined($param->{$k});
+ next if $param_used->{$k};
+ if (defined($rule->{$k})) {
+ if ($rule->{$k} ne $param->{$k}) {
+ my $msg = "parameter '$k' already define in macro (value = '$rule->{$k}')";
+ raise_param_exc({ $k => $msg }) if $verify;
+ die "$msg\n";
+ }
+ } else {
+ $rule->{$k} = $param->{$k};
+ }
+ }
+ push @$rules, $rule;
+ }
+
+ return $rules;
+};
+
+sub verify_rule {
+ my ($rule, $allow_groups, $noerr) = @_;
+
+ my $errors = {};
+ my $error_count = 0;
+
+ my $add_error = sub {
+ my ($param, $msg) = @_;
+ raise_param_exc({ $param => $msg }) if !$noerr;
+ $error_count++;
+ $errors->{$param} = $msg if !$errors->{$param};
+ };
+
+ my $type = $rule->{type};
+ my $action = $rule->{action};
+
+ &$add_error('type', "missing property") if !$type;
+ &$add_error('action', "missing property") if !$action;
+
+ if ($type) {
+ if ($type eq 'in' || $type eq 'out') {
+ &$add_error('action', "unknown action '$action'")
+ if $action && ($action !~ m/^(ACCEPT|DROP|REJECT)$/);
+ } elsif ($type eq 'group') {
+ &$add_error('type', "security groups not allowed")
+ if !$allow_groups;
+ &$add_error('action', "invalid characters in security group name")
+ if $action && ($action !~ m/^${security_group_name_pattern}$/);
+ } else {
+ &$add_error('type', "unknown rule type '$type'");
+ }
+ }
+
+ if ($rule->{iface}) {
+ eval { PVE::JSONSchema::pve_verify_iface($rule->{iface}); };
+ &$add_error('iface', $@) if $@;
+ }
+
+ if ($rule->{macro}) {
+ if (my $preferred_name = $pve_fw_preferred_macro_names->{lc($rule->{macro})}) {
+ $rule->{macro} = $preferred_name;
+ } else {
+ &$add_error('macro', "unknown macro '$rule->{macro}'");
+ }
+ }
+
+ if ($rule->{proto}) {
+ eval { pve_fw_verify_protocol_spec($rule->{proto}); };
+ &$add_error('proto', $@) if $@;
+ }
+
+ if ($rule->{dport}) {
+ eval { parse_port_name_number_or_range($rule->{dport}); };
+ &$add_error('dport', $@) if $@;
+ &$add_error('proto', "missing property - 'dport' requires this property")
+ if !$rule->{proto};
+ }
+
+ if ($rule->{sport}) {
+ eval { parse_port_name_number_or_range($rule->{sport}); };
+ &$add_error('sport', $@) if $@;
+ &$add_error('proto', "missing property - 'sport' requires this property")
+ if !$rule->{proto};
+ }
+
+ if ($rule->{source}) {
+ eval { parse_address_list($rule->{source}); };
+ &$add_error('source', $@) if $@;
+ }
+
+ if ($rule->{dest}) {
+ eval { parse_address_list($rule->{dest}); };
+ &$add_error('dest', $@) if $@;
+ }
+
+ if ($rule->{macro}) {
+ eval { &$apply_macro($rule->{macro}, $rule, 1); };
+ if (my $err = $@) {
+ if (ref($err) eq "PVE::Exception" && $err->{errors}) {
+ my $eh = $err->{errors};
+ foreach my $p (keys %$eh) {
+ &$add_error($p, $eh->{$p});
+ }
+ } else {
+ &$add_error('macro', "$err");
+ }
+ }
+ }
+
+ $rule->{errors} = $errors if $error_count;
+
+ return $rule;
+}
+
+sub copy_rule_data {
+ my ($rule, $param) = @_;
+
+ foreach my $k (keys %$rule_properties) {
+ if (defined(my $v = $param->{$k})) {
+ if ($v eq '' || $v eq '-') {
+ delete $rule->{$k};
+ } else {
+ $rule->{$k} = $v;
+ }
+ }