X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=74bd8d962eaad4868b0bcaaa9449c568e58a1f63;hp=ea8853edc5775c4ce924be5b0c3eb885ac0f5d66;hb=9d6f90e67337dde709a2482b44d2003d15be1a84;hpb=ebd54ae918365a722c1091109e74ec21ca7f4be6 diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index ea8853e..74bd8d9 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -6,6 +6,7 @@ use POSIX; use Data::Dumper; use Digest::SHA; use PVE::INotify; +use PVE::Exception qw(raise raise_param_exc); use PVE::JSONSchema qw(get_standard_option); use PVE::Cluster; use PVE::ProcFSTools; @@ -692,31 +693,37 @@ sub parse_port_name_number_or_range { my ($str) = @_; my $services = PVE::Firewall::get_etc_services(); - my $nbports = 0; + my $count = 0; + my $icmp_port = 0; + foreach my $item (split(/,/, $str)) { - my $portlist = ""; - my $oldpon = undef; - $nbports++; - foreach my $pon (split(':', $item, 2)) { - $pon = $services->{byname}->{$pon}->{port} if $services->{byname}->{$pon}->{port}; - if ($pon =~ m/^\d+$/){ - die "invalid port '$pon'\n" if $pon < 0 && $pon > 65535; - die "port '$pon' must be bigger than port '$oldpon' \n" if $oldpon && ($pon < $oldpon); - $oldpon = $pon; - }else{ - die "invalid port $services->{byname}->{$pon}\n" if !$services->{byname}->{$pon}; + $count++; + if ($item =~ m/^(\d+):(\d+)$/) { + my ($port1, $port2) = ($1, $2); + die "invalid port '$port1'\n" if $port1 > 65535; + die "invalid port '$port2'\n" if $port2 > 65535; + } elsif ($item =~ m/^(\d+)$/) { + my $port = $1; + die "invalid port '$port'\n" if $port > 65535; + } else { + if ($icmp_type_names->{$item}) { + $icmp_port = 1; + } else { + die "invalid port '$item'\n" if !$services->{byname}->{$item}; } } } - return ($nbports); + die "ICPM ports not allowed in port range\n" if $icmp_port && $count > 1; + + return $count; } PVE::JSONSchema::register_format('pve-fw-port-spec', \&pve_fw_verify_port_spec); sub pve_fw_verify_port_spec { my ($portstr) = @_; - parse_port_name_number_or_range($portstr); + parse_port_name_number_or_range($portstr); return $portstr; } @@ -848,6 +855,112 @@ sub delete_rule_properties { 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) = @_; + + my $type = $rule->{type}; + + raise_param_exc({ type => "missing property"}) if !$type; + raise_param_exc({ action => "missing property"}) if !$rule->{action}; + + if ($type eq 'in' || $type eq 'out') { + raise_param_exc({ action => "unknown action '$rule->{action}'"}) + if $rule->{action} !~ m/^(ACCEPT|DROP|REJECT)$/; + } elsif ($type eq 'group') { + raise_param_exc({ type => "security groups not allowed"}) + if !$allow_groups; + raise_param_exc({ action => "invalid characters in security group name"}) + if $rule->{action} !~ m/^[A-Za-z0-9_\-]+$/; + } else { + raise_param_exc({ type => "unknown rule type '$type'"}); + } + + # fixme: verify $rule->{iface}? + + if ($rule->{macro}) { + my $preferred_name = $pve_fw_preferred_macro_names->{lc($rule->{macro})}; + raise_param_exc({ macro => "unknown macro '$rule->{macro}'"}) if !$preferred_name; + $rule->{macro} = $preferred_name; + } + + if ($rule->{dport}) { + eval { parse_port_name_number_or_range($rule->{dport}); }; + raise_param_exc({ dport => $@ }) if $@; + } + + if ($rule->{sport}) { + eval { parse_port_name_number_or_range($rule->{sport}); }; + raise_param_exc({ sport => $@ }) if $@; + } + + if ($rule->{source}) { + eval { parse_address_list($rule->{source}); }; + raise_param_exc({ source => $@ }) if $@; + } + + if ($rule->{dest}) { + eval { parse_address_list($rule->{dest}); }; + raise_param_exc({ dest => $@ }) if $@; + } + + if ($rule->{macro}) { + &$apply_macro($rule->{macro}, $rule, 1); + } + + return $rule; +} + sub copy_rule_data { my ($rule, $param) = @_; @@ -862,6 +975,9 @@ sub copy_rule_data { delete $rule->{$k}; } } + + # verify rule now + return $rule; } @@ -1101,50 +1217,6 @@ sub ruleset_generate_cmdstr { return scalar(@cmd) ? join(' ', @cmd) : undef; } -my $apply_macro = sub { - my ($macro_name, $param) = @_; - - 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; - } - - die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v); - $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})) { - die "parameter '$k' already define in macro (value = '$rule->{$k}')\n" - if $rule->{$k} ne $param->{$k}; - } else { - $rule->{$k} = $param->{$k}; - } - } - push @$rules, $rule; - } - - return $rules; -}; - sub ruleset_generate_rule { my ($ruleset, $chain, $rule, $actions, $goto, $cluster_conf) = @_; @@ -1933,29 +2005,26 @@ sub parse_cluster_fw_rules { } push @{$res->{$section}->{$group}}, $rule; } elsif ($section eq 'ipset') { - chomp $line; - $line =~ m/^(\!)?\s*((\d+)\.(\d+)\.(\d+)\.(\d+)(\/(\d+))?)/; + # we can add single line comments to the end of the rule + my $comment = decode('utf8', $1) if $line =~ s/#\s*(.*?)\s*$//; + + $line =~ m/^(\!)?\s*((?:\d+)\.(?:\d+)\.(?:\d+)\.(?:\d+))(?:\/(\d+))?\s*$/; my $nomatch = $1; - my $ip = $2; + my $cidr = $2; - if(!$ip){ - warn "$prefix: $line is not an valid ip address\n"; - next; - } - if (!Net::IP->new($ip)) { - warn "$prefix: $line is not an valid ip address\n"; + $cidr .= "/$3" if defined($3) && $3 != 32; + + if (!Net::IP->new($cidr)) { + my $err = Net::IP::Error(); + warn "$prefix: $cidr - $err\n"; next; } - if ($nomatch) { - if ($feature_ipset_nomatch) { - push @{$res->{$section}->{$group}}, "$ip nomatch"; - } else { - warn "$prefix: ignore $line - nomatch not supported by kernel\n"; - } - } else { - push @{$res->{$section}->{$group}}, $ip; - } + my $entry = { cidr => $cidr }; + $entry->{nomatch} = 1 if $nomatch; + $entry->{comment} = $comment if $comment; + + push @{$res->{$section}->{$group}}, $entry; } } @@ -2033,7 +2102,11 @@ my $format_rules = sub { if ($rule->{type} eq 'in' || $rule->{type} eq 'out') { $raw .= '|' if defined($rule->{enable}) && !$rule->{enable}; $raw .= uc($rule->{type}); - $raw .= " " . $rule->{action}; + if ($rule->{macro}) { + $raw .= " $rule->{macro}($rule->{action})"; + } else { + $raw .= " " . $rule->{action}; + } $raw .= " " . ($rule->{iface} || '-') if $need_iface; $raw .= " " . ($rule->{source} || '-'); $raw .= " " . ($rule->{dest} || '-'); @@ -2065,6 +2138,22 @@ my $format_options = sub { return $raw; }; +my $format_ipset = sub { + my ($options) = @_; + + my $raw = ''; + + foreach my $entry (@$options) { + my $line = $entry->{nomatch} ? '!' : ''; + $line .= $entry->{cidr}; + $line .= " # " . encode('utf8', $entry->{comment}) + if $entry->{comment} && $entry->{comment} !~ m/^\s*$/; + $raw .= "$line\n"; + } + + return $raw; +}; + sub save_vmfw_conf { my ($vmid, $vmfw_conf) = @_; @@ -2164,8 +2253,18 @@ sub generate_ipset { push @{$ipset_ruleset->{$name}}, "create $name hash:net family inet hashsize $hashsize maxelem $hashsize"; - foreach my $ip (@$options) { - push @{$ipset_ruleset->{$name}}, "add $name $ip"; + foreach my $entry (@$options) { + my $cidr = $entry->{cidr}; + my $cmd = "add $name $cidr"; + if ($entry->{nomatch}) { + if ($feature_ipset_nomatch) { + push @{$ipset_ruleset->{$name}}, "$cmd nomatch"; + } else { + warn "ignore !$cidr - nomatch not supported by kernel\n"; + } + } else { + push @{$ipset_ruleset->{$name}}, $cmd; + } } } @@ -2247,7 +2346,12 @@ sub save_clusterfw_conf { my $options = $cluster_conf->{options}; $raw .= &$format_options($options) if scalar(keys %$options); - # fixme: save ipset + foreach my $ipset (sort keys %{$cluster_conf->{ipset}}) { + $raw .= "[IPSET $ipset]\n\n"; + my $options = $cluster_conf->{ipset}->{$ipset}; + $raw .= &$format_ipset($options); + $raw .= "\n"; + } my $rules = $cluster_conf->{rules}; if (scalar(@$rules)) {