X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=5d1a58410605ee7a496d678e6fbc0b3b9c13c150;hb=29e5ce15369c288b74fec101bcf0f2ad5ff27a9c;hp=ae67bcd0b70be577c311efb5089a43059a906d8c;hpb=94e4ec75ca20f1cbb797ab0442e6400cb9a84672;p=pve-firewall.git diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index ae67bcd..5d1a584 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -394,6 +394,10 @@ my $pve_fw_macros = { { action => 'PARAM', proto => 'udp', dport => '5632' }, { action => 'PARAM', proto => 'tcp', dport => '5631' }, ], + 'PMG' => [ + "Proxmox Mail Gateway web interface", + { action => 'PARAM', proto => 'tcp', dport => '8006' }, + ], 'POP3' => [ "POP3 traffic", { action => 'PARAM', proto => 'tcp', dport => '110' }, @@ -781,12 +785,14 @@ my $icmp_type_names = { # ip6tables -p icmpv6 -h my $icmpv6_type_names = { - 'any' => 1, 'destination-unreachable' => 1, 'no-route' => 1, 'communication-prohibited' => 1, + 'beyond-scope' => 1, 'address-unreachable' => 1, 'port-unreachable' => 1, + 'failed-policy' => 1, + 'reject-route' => 1, 'packet-too-big' => 1, 'time-exceeded' => 1, 'ttl-zero-during-transit' => 1, @@ -978,8 +984,8 @@ sub local_network { } # ipset names are limited to 31 characters, -# and we use '-v4' or '-v6' to indicate IP versions, -# and we use '_swap' suffix for atomic update, +# and we use '-v4' or '-v6' to indicate IP versions, +# and we use '_swap' suffix for atomic update, # for example PVEFW-${VMID}-${ipset_name}_swap my $max_iptables_ipset_name_length = 31 - length("PVEFW-") - length("_swap"); @@ -1619,6 +1625,8 @@ sub verify_rule { if !$allow_groups; &$add_error('action', "invalid characters in security group name") if $action && ($action !~ m/^${security_group_name_pattern}$/); + &$add_error('action', "security group '$action' does not exist") + if $action && !defined($cluster_conf->{groups}->{$action}); } else { &$add_error('type', "unknown rule type '$type'"); } @@ -1672,7 +1680,7 @@ sub verify_rule { } if ($rule->{source}) { - eval { + eval { my $source_ipversion = parse_address_list($rule->{source}); &$set_ip_version($source_ipversion); }; @@ -1681,8 +1689,8 @@ sub verify_rule { } if ($rule->{dest}) { - eval { - my $dest_ipversion = parse_address_list($rule->{dest}); + eval { + my $dest_ipversion = parse_address_list($rule->{dest}); &$set_ip_version($dest_ipversion); }; &$add_error('dest', $@) if $@; @@ -2037,11 +2045,17 @@ sub ipt_rule_to_cmds { # Note: we use dport to store --icmp-type die "unknown icmp-type '$rule->{dport}'\n" if $rule->{dport} !~ /^\d+$/ && !defined($icmp_type_names->{$rule->{dport}}); + # values for icmp-type range between 0 and 255 + # higher values and iptables-restore fails + die "invalid icmp-type '$rule->{dport}'\n" if ($rule->{dport} =~ m/^(\d+)$/) && ($1 > 255); push @match, "-m icmp --icmp-type $rule->{dport}"; } elsif ($proto eq 'icmpv6') { # Note: we use dport to store --icmpv6-type die "unknown icmpv6-type '$rule->{dport}'\n" if $rule->{dport} !~ /^\d+$/ && !defined($icmpv6_type_names->{$rule->{dport}}); + # values for icmpv6-type range between 0 and 255 + # higher values and iptables-restore fails + die "invalid icmpv6-type '$rule->{dport}'\n" if ($rule->{dport} =~ m/^(\d+)$/) && ($1 > 255); push @match, "-m icmpv6 --icmpv6-type $rule->{dport}"; } elsif (!$PROTOCOLS_WITH_PORTS->{$proto}) { die "protocol $proto does not have ports\n"; @@ -2260,7 +2274,7 @@ sub ruleset_create_vm_chain { if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) { if ($ipversion == 4) { if ($direction eq 'OUT') { - ruleset_generate_rule($ruleset, $chain, $ipversion, + ruleset_generate_rule($ruleset, $chain, $ipversion, { action => 'PVEFW-SET-ACCEPT-MARK', proto => 'udp', sport => 68, dport => 67 }); } else { @@ -2487,6 +2501,7 @@ sub enable_host_firewall { $rule->{iface_in} = $rule->{iface} if $rule->{iface}; eval { + $rule->{logmsg} = "$rule->{action}: "; if ($rule->{type} eq 'group') { ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'IN', $accept_action, $ipversion); } elsif ($rule->{type} eq 'in') { @@ -2838,7 +2853,7 @@ sub parse_ip_or_cidr { my ($cidr) = @_; my $ipversion; - + if ($cidr =~ m!^(?:$IPV6RE)(/(\d+))?$!) { $cidr =~ s|/128$||; $ipversion = 6; @@ -2892,6 +2907,8 @@ sub generic_fw_config_parser { } return {} if !$raw; + my $curr_group_keys = {}; + my $linenr = 0; while ($raw =~ /^\h*(.*?)\h*$/gm) { my $line = $1; @@ -2925,7 +2942,7 @@ sub generic_fw_config_parser { warn "$prefix: $err"; next; } - + $res->{$section}->{$group} = []; $res->{group_comments}->{$group} = decode('utf8', $comment) if $comment; @@ -2941,7 +2958,7 @@ sub generic_fw_config_parser { $section = 'ipset'; $group = lc($1); my $comment = $2; - eval { + eval { die "ipset name too long\n" if length($group) > $max_ipset_name_length; die "invalid ipset name '$group'\n" if $group !~ m/^${ipset_name_pattern}$/; }; @@ -2952,6 +2969,8 @@ sub generic_fw_config_parser { } $res->{$section}->{$group} = []; + $curr_group_keys = {}; + $res->{ipset_comments}->{$group} = decode('utf8', $comment) if $comment; next; @@ -3010,12 +3029,14 @@ sub generic_fw_config_parser { $errors->{nomatch} = "nomatch not supported by kernel"; } - eval { + eval { if ($cidr =~ m/^${ip_alias_pattern}$/) { resolve_alias($cluster_conf, $res, $cidr); # make sure alias exists } else { $cidr = parse_ip_or_cidr($cidr); } + die "duplicate ipset entry for '$cidr'\n" + if defined($curr_group_keys->{$cidr}); }; if (my $err = $@) { chomp $err; @@ -3039,6 +3060,7 @@ sub generic_fw_config_parser { } push @{$res->{$section}->{$group}}, $entry; + $curr_group_keys->{$cidr} = 1; } else { warn "$prefix: skip line - unknown section\n"; next; @@ -3048,6 +3070,8 @@ sub generic_fw_config_parser { return $res; } +# this is only used to prevent concurrent runs of rule compilation/application +# see lock_*_conf for cfs locks protectiong config modification sub run_locked { my ($code, @param) = @_; @@ -3096,6 +3120,18 @@ sub read_local_vm_config { return $vmdata; }; +sub lock_vmfw_conf { + my ($vmid, $timeout, $code, @param) = @_; + + die "can't lock VM firewall config for undefined VMID\n" + if !defined($vmid); + + my $res = PVE::Cluster::cfs_lock_firewall("vm-$vmid", $timeout, $code, @param); + die $@ if $@; + + return $res; +} + sub load_vmfw_conf { my ($cluster_conf, $rule_env, $vmid, $dir) = @_; @@ -3188,7 +3224,7 @@ my $format_aliases = sub { my $format_ipsets = sub { my ($fw_conf) = @_; - + my $raw = ''; foreach my $ipset (sort keys %{$fw_conf->{ipset}}) { @@ -3202,7 +3238,13 @@ my $format_ipsets = sub { my $nethash = {}; foreach my $entry (@$options) { - $nethash->{$entry->{cidr}} = $entry; + my $cidr = $entry->{cidr}; + if (defined($nethash->{$cidr})) { + warn "ignoring duplicate ipset entry '$cidr'\n"; + next; + } + + $nethash->{$cidr} = $entry; } foreach my $cidr (sort keys %$nethash) { @@ -3263,13 +3305,15 @@ sub clone_vmfw_conf { my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw"; my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw"; - if (-f $clonevm_conffile) { - unlink $clonevm_conffile; - } - if (-f $sourcevm_conffile) { - my $data = PVE::Tools::file_get_contents($sourcevm_conffile); - PVE::Tools::file_set_contents($clonevm_conffile, $data); - } + lock_vmfw_conf($newid, 10, sub { + if (-f $clonevm_conffile) { + unlink $clonevm_conffile; + } + if (-f $sourcevm_conffile) { + my $data = PVE::Tools::file_get_contents($sourcevm_conffile); + PVE::Tools::file_set_contents($clonevm_conffile, $data); + } + }); } sub read_vm_firewall_configs { @@ -3279,12 +3323,12 @@ sub read_vm_firewall_configs { foreach my $vmid (keys %{$vmdata->{qemu}}) { my $vmfw_conf = load_vmfw_conf($cluster_conf, 'vm', $vmid, $dir); - next if !$vmfw_conf->{options}; # skip if file does not exists + next if !$vmfw_conf->{options}; # skip if file does not exist $vmfw_configs->{$vmid} = $vmfw_conf; } foreach my $vmid (keys %{$vmdata->{lxc}}) { my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir); - next if !$vmfw_conf->{options}; # skip if file does not exists + next if !$vmfw_conf->{options}; # skip if file does not exist $vmfw_configs->{$vmid} = $vmfw_conf; } @@ -3443,6 +3487,15 @@ my $set_global_log_ratelimit = sub { } }; +sub lock_clusterfw_conf { + my ($timeout, $code, @param) = @_; + + my $res = PVE::Cluster::cfs_lock_firewall("cluster", $timeout, $code, @param); + die $@ if $@; + + return $res; +} + sub load_clusterfw_conf { my ($filename) = @_; @@ -3475,7 +3528,7 @@ sub save_clusterfw_conf { $raw .= &$format_aliases($aliases) if $aliases && scalar(keys %$aliases); $raw .= &$format_ipsets($cluster_conf) if $cluster_conf->{ipset}; - + my $rules = $cluster_conf->{rules}; if ($rules && scalar(@$rules)) { $raw .= "[RULES]\n\n"; @@ -3506,6 +3559,15 @@ sub save_clusterfw_conf { } } +sub lock_hostfw_conf { + my ($timeout, $code, @param) = @_; + + my $res = PVE::Cluster::cfs_lock_firewall("host-$nodename", $timeout, $code, @param); + die $@ if $@; + + return $res; +} + sub load_hostfw_conf { my ($cluster_conf, $filename) = @_; @@ -3728,7 +3790,7 @@ sub compile_ipsets { my $localnet_ver; ($localnet, $localnet_ver) = parse_ip_or_cidr(local_network() || '127.0.0.0/8'); - $cluster_conf->{aliases}->{local_network} = { + $cluster_conf->{aliases}->{local_network} = { name => 'local_network', cidr => $localnet, ipversion => $localnet_ver }; } @@ -3899,7 +3961,9 @@ sub compile_ebtables_filter { # ebtables changes this to a .0/MASK network but we just # want the address here, no network - see #2193 $ip =~ s|/(\d+)$||; - push @$arpfilter, $ip; + if ($ip ne 'dhcp') { + push @$arpfilter, $ip; + } } generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter); } @@ -4441,7 +4505,7 @@ sub remove_pvefw_chains_ipset { my $ipset_chains = ipset_get_chains(); my $cmdlist = ""; - + foreach my $chain (keys %$ipset_chains) { $cmdlist .= "flush $chain\n"; $cmdlist .= "destroy $chain\n";