X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=da1784cf66cbbf7c4f72771724f8193691b9cd41;hp=db16e0fb62b2538ffa7e5689664128065530749d;hb=be99c6a0e95108bd9fe84b3c238e216ed2b31e64;hpb=ac5dd88e976e565fef44f068ac9644666615f5e4 diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index db16e0f..da1784c 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' }, @@ -978,8 +982,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 +1623,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 +1678,7 @@ sub verify_rule { } if ($rule->{source}) { - eval { + eval { my $source_ipversion = parse_address_list($rule->{source}); &$set_ip_version($source_ipversion); }; @@ -1681,8 +1687,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 +2043,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 +2272,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 +2499,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') { @@ -2505,6 +2518,7 @@ sub enable_host_firewall { ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 5900:5999", "-j $accept_action"); # PVE VNC Console ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 3128", "-j $accept_action"); # SPICE Proxy ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 22", "-j $accept_action"); # SSH + ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 60000:60050", "-j $accept_action"); # Migration # corosync inbound rules if (defined($corosync_conf)) { @@ -2837,7 +2851,7 @@ sub parse_ip_or_cidr { my ($cidr) = @_; my $ipversion; - + if ($cidr =~ m!^(?:$IPV6RE)(/(\d+))?$!) { $cidr =~ s|/128$||; $ipversion = 6; @@ -2891,6 +2905,8 @@ sub generic_fw_config_parser { } return {} if !$raw; + my $curr_group_keys = {}; + my $linenr = 0; while ($raw =~ /^\h*(.*?)\h*$/gm) { my $line = $1; @@ -2924,7 +2940,7 @@ sub generic_fw_config_parser { warn "$prefix: $err"; next; } - + $res->{$section}->{$group} = []; $res->{group_comments}->{$group} = decode('utf8', $comment) if $comment; @@ -2940,7 +2956,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}$/; }; @@ -2951,6 +2967,8 @@ sub generic_fw_config_parser { } $res->{$section}->{$group} = []; + $curr_group_keys = {}; + $res->{ipset_comments}->{$group} = decode('utf8', $comment) if $comment; next; @@ -3009,12 +3027,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; @@ -3038,6 +3058,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; @@ -3047,6 +3068,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) = @_; @@ -3095,6 +3118,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) = @_; @@ -3187,7 +3222,7 @@ my $format_aliases = sub { my $format_ipsets = sub { my ($fw_conf) = @_; - + my $raw = ''; foreach my $ipset (sort keys %{$fw_conf->{ipset}}) { @@ -3201,7 +3236,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) { @@ -3262,13 +3303,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 { @@ -3278,12 +3321,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; } @@ -3442,6 +3485,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) = @_; @@ -3474,7 +3526,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"; @@ -3505,6 +3557,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) = @_; @@ -3727,7 +3788,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 }; } @@ -3898,7 +3959,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); } @@ -4440,7 +4503,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";