X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=c34216758acfffdcd020c707521ac6bd0b26a468;hp=92b02aae5f07290b364da2711c9c52d2f1aa493d;hb=9388a8f47a6db83a8a1f8f2b5852b6792fffa0f4;hpb=40af93c4067341115877fc082a900be215122f0a diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index 92b02aa..c342167 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -2,27 +2,28 @@ package PVE::Firewall; use warnings; use strict; -use POSIX; -use Data::Dumper; + use Digest::SHA; -use Socket qw(AF_INET6 inet_ntop inet_pton); -use PVE::INotify; -use PVE::Exception qw(raise raise_param_exc); -use PVE::JSONSchema qw(register_standard_option get_standard_option); -use PVE::Cluster; -use PVE::ProcFSTools; -use PVE::Tools qw($IPV4RE $IPV6RE); -use PVE::Network; -use PVE::SafeSyslog; +use Encode; use File::Basename; use File::Path; use IO::File; use Net::IP; -use PVE::Tools qw(run_command lock_file dir_glob_foreach); -use Encode; +use POSIX; +use Socket qw(AF_INET AF_INET6 inet_ntop inet_pton); use Storable qw(dclone); -my $hostfw_conf_filename = "/etc/pve/local/host.fw"; +use PVE::Cluster; +use PVE::Corosync; +use PVE::Exception qw(raise raise_param_exc); +use PVE::INotify; +use PVE::JSONSchema qw(register_standard_option get_standard_option); +use PVE::Network; +use PVE::ProcFSTools; +use PVE::SafeSyslog; +use PVE::Tools qw($IPV4RE $IPV6RE); +use PVE::Tools qw(run_command lock_file dir_glob_foreach); + my $pvefw_conf_dir = "/etc/pve/firewall"; my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw"; @@ -41,7 +42,6 @@ eval { $have_lxc = 1; }; - my $pve_fw_status_dir = "/var/lib/pve-firewall"; mkdir $pve_fw_status_dir; # make sure this exists @@ -127,6 +127,7 @@ eval { }; my $nodename = PVE::INotify::nodename(); +my $hostfw_conf_filename = "/etc/pve/nodes/$nodename/host.fw"; my $pve_fw_lock_filename = "/var/lock/pvefw.lck"; @@ -213,7 +214,10 @@ my $pve_fw_macros = { ], 'Ceph' => [ "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Deamons)", + # Legacy port for protocol v1 { action => 'PARAM', proto => 'tcp', dport => '6789' }, + # New port for protocol v2 + { action => 'PARAM', proto => 'tcp', dport => '3300' }, { action => 'PARAM', proto => 'tcp', dport => '6800:7300' }, ], 'CVS' => [ @@ -390,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' }, @@ -666,14 +674,10 @@ $pve_std_chains_conf->{6} = { #{ action => 'DROP', dest => '224.0.0.0/4' }, ], 'PVEFW-reject' => [ - # same as shorewall 'reject' - #{ action => 'DROP', dsttype => 'BROADCAST' }, - #{ action => 'DROP', source => '224.0.0.0/4' }, { action => 'DROP', proto => 'icmpv6' }, { match => '-p tcp', target => '-j REJECT --reject-with tcp-reset' }, - #"-p udp -j REJECT --reject-with icmp-port-unreachable", - #"-p icmp -j REJECT --reject-with icmp-host-unreachable", - #"-j REJECT --reject-with icmp-host-prohibited", + { match => '-p udp', target => '-j REJECT --reject-with icmp6-port-unreachable' }, + { target => '-j REJECT --reject-with icmp6-adm-prohibited' }, ], 'PVEFW-Drop' => [ # same as shorewall 'Drop', which is equal to DROP, @@ -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"); @@ -1256,23 +1260,35 @@ our $host_option_properties = { tcpflags => { description => "Filter illegal combinations of TCP flags.", type => 'boolean', + default => 0, optional => 1, }, nf_conntrack_max => { description => "Maximum number of tracked connections.", type => 'integer', optional => 1, + default => 262144, minimum => 32768, }, nf_conntrack_tcp_timeout_established => { description => "Conntrack established timeout.", type => 'integer', optional => 1, + default => 432000, minimum => 7875, }, + nf_conntrack_tcp_timeout_syn_recv => { + description => "Conntrack syn recv timeout.", + type => 'integer', + optional => 1, + default => 60, + minimum => 30, + maximum => 60, + }, ndp => { - description => "Enable NDP.", + description => "Enable NDP (Neighbor Discovery Protocol).", type => 'boolean', + default => 0, optional => 1, }, nf_conntrack_allow_invalid => { @@ -1281,6 +1297,24 @@ our $host_option_properties = { default => 0, optional => 1, }, + protection_synflood => { + description => "Enable synflood protection", + type => 'boolean', + default => 0, + optional => 1, + }, + protection_synflood_rate => { + description => "Synflood protection rate syn/sec by ip src.", + type => 'integer', + optional => 1, + default => 200, + }, + protection_synflood_burst => { + description => "Synflood protection rate burst by ip src.", + type => 'integer', + optional => 1, + default => 1000, + }, log_nf_conntrack => { description => "Enable logging of conntrack information.", type => 'boolean', @@ -1293,21 +1327,25 @@ our $vm_option_properties = { enable => { description => "Enable/disable firewall rules.", type => 'boolean', + default => 0, optional => 1, }, macfilter => { description => "Enable/disable MAC address filter.", type => 'boolean', + default => 0, optional => 1, }, dhcp => { description => "Enable DHCP.", type => 'boolean', + default => 0, optional => 1, }, ndp => { - description => "Enable NDP.", + description => "Enable NDP (Neighbor Discovery Protocol).", type => 'boolean', + default => 0, optional => 1, }, radv => { @@ -1638,7 +1676,7 @@ sub verify_rule { } if ($rule->{source}) { - eval { + eval { my $source_ipversion = parse_address_list($rule->{source}); &$set_ip_version($source_ipversion); }; @@ -1647,8 +1685,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 $@; @@ -1749,33 +1787,36 @@ sub enable_bridge_firewall { } sub iptables_restore_cmdlist { - my ($cmdlist) = @_; + my ($cmdlist, $table) = @_; - run_command("/sbin/iptables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist"); + $table = 'filter' if !$table; + run_command(['iptables-restore', '-T', $table, '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist"); } sub ip6tables_restore_cmdlist { - my ($cmdlist) = @_; + my ($cmdlist, $table) = @_; - run_command("/sbin/ip6tables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist"); + $table = 'filter' if !$table; + run_command(['ip6tables-restore', '-T', $table, '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist"); } sub ipset_restore_cmdlist { my ($cmdlist) = @_; - run_command("/sbin/ipset restore", input => $cmdlist, errmsg => "ipset_restore_cmdlist"); + run_command(['ipset', 'restore'], input => $cmdlist, errmsg => "ipset_restore_cmdlist"); } sub ebtables_restore_cmdlist { my ($cmdlist) = @_; - run_command("/sbin/ebtables-restore", input => $cmdlist, errmsg => "ebtables_restore_cmdlist"); + run_command(['ebtables-restore'], input => $cmdlist, errmsg => "ebtables_restore_cmdlist"); } sub iptables_get_chains { - my ($iptablescmd) = @_; + my ($iptablescmd, $t) = @_; $iptablescmd = "iptables" if !$iptablescmd; + $t = 'filter' if !$t; my $res = {}; @@ -1810,7 +1851,7 @@ sub iptables_get_chains { return; } - return if $table ne 'filter'; + return if $table ne $t; if ($line =~ m/^:(\S+)\s/) { my $chain = $1; @@ -1820,7 +1861,7 @@ sub iptables_get_chains { my ($chain, $sig) = ($1, $2); return if !&$is_pvefw_chain($chain); $res->{$chain} = $sig; - } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD)\s+-j\s+PVEFW-\1$/) { + } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD|PREROUTING)\s+-j\s+PVEFW-\1$/) { $hooks->{$1} = 1; } else { # simply ignore the rest @@ -1828,7 +1869,7 @@ sub iptables_get_chains { } }; - run_command("/sbin/$iptablescmd-save", outfunc => $parser); + run_command(["$iptablescmd-save"], outfunc => $parser); return wantarray ? ($res, $hooks) : $res; } @@ -1872,7 +1913,7 @@ sub ipset_get_chains { } }; - run_command("/sbin/ipset save", outfunc => $parser); + run_command(['ipset', 'save'], outfunc => $parser); # compute digest for each chain foreach my $chain (keys %$chains) { @@ -1903,7 +1944,7 @@ sub ebtables_get_chains { } }; - run_command("/sbin/ebtables-save", outfunc => $parser); + run_command(['ebtables-save'], outfunc => $parser); # compute digest for each chain and store rules as well foreach my $chain (keys %$chains) { $res->{$chain}->{rules} = $chains->{$chain}; @@ -2223,7 +2264,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 { @@ -2363,10 +2404,10 @@ sub generate_tap_rules_direction { my $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion) if $options->{ipfilter} || $vmfw_conf->{ipset}->{$ipfilter_name}; - # create chain with mac and ip filter - ruleset_create_vm_chain($ruleset, $tapchain, $ipversion, $options, $macaddr, $ipfilter_ipset, $direction); - if ($options->{enable}) { + # create chain with mac and ip filter + ruleset_create_vm_chain($ruleset, $tapchain, $ipversion, $options, $macaddr, $ipfilter_ipset, $direction); + ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $tapchain, $netid, $direction, $options, $ipversion, $vmid); ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface); @@ -2391,21 +2432,40 @@ sub generate_tap_rules_direction { # plug the tap chain to bridge chain if ($direction eq 'IN') { ruleset_addrule($ruleset, "PVEFW-FWBR-IN", - "-m physdev --physdev-is-bridged --physdev-out $iface", "-j $tapchain", $loglevel, 'FWBR-IN: ', $vmid); + "-m physdev --physdev-is-bridged --physdev-out $iface", "-j $tapchain"); } else { ruleset_addrule($ruleset, "PVEFW-FWBR-OUT", - "-m physdev --physdev-is-bridged --physdev-in $iface", "-j $tapchain", $loglevel, 'FWBR-OUT: ', $vmid); + "-m physdev --physdev-is-bridged --physdev-in $iface", "-j $tapchain"); } } sub enable_host_firewall { - my ($ruleset, $hostfw_conf, $cluster_conf, $ipversion) = @_; + my ($ruleset, $hostfw_conf, $cluster_conf, $ipversion, $corosync_conf) = @_; my $options = $hostfw_conf->{options}; my $cluster_options = $cluster_conf->{options}; my $rules = $hostfw_conf->{rules}; my $cluster_rules = $cluster_conf->{rules}; + # corosync preparation + my $corosync_rule = "-p udp --dport 5404:5405"; + my $corosync_local_addresses = {}; + my $multicast_enabled; + my $local_hostname = PVE::INotify::nodename(); + if (defined($corosync_conf)) { + PVE::Corosync::for_all_corosync_addresses($corosync_conf, $ipversion, sub { + my ($node_name, $node_ip, $node_ipversion, $key) = @_; + + if ($node_name eq $local_hostname) { + $corosync_local_addresses->{$key} = $node_ip; + } + }); + + # allow multicast only if enabled in config + my $corosync_transport = $corosync_conf->{main}->{totem}->{transport}; + $multicast_enabled = defined($corosync_transport) && $corosync_transport eq 'udp'; + } + # host inbound firewall my $chain = "PVEFW-HOST-IN"; ruleset_create_chain($ruleset, $chain); @@ -2431,6 +2491,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') { @@ -2449,15 +2510,22 @@ 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 - my $localnet = $cluster_conf->{aliases}->{local_network}->{cidr}; - my $localnet_ver = $cluster_conf->{aliases}->{local_network}->{ipversion}; + # corosync inbound rules + if (defined($corosync_conf)) { + ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST $corosync_rule", "-j $accept_action") + if $multicast_enabled; - # corosync - if ($localnet && ($ipversion == $localnet_ver)) { - my $corosync_rule = "-p udp --dport 5404:5405"; - ruleset_addrule($ruleset, $chain, "-s $localnet -d $localnet $corosync_rule", "-j $accept_action"); - ruleset_addrule($ruleset, $chain, "-s $localnet -m addrtype --dst-type MULTICAST $corosync_rule", "-j $accept_action"); + PVE::Corosync::for_all_corosync_addresses($corosync_conf, $ipversion, sub { + my ($node_name, $node_ip, $node_ipversion, $key) = @_; + my $destination = $corosync_local_addresses->{$key}; + + if ($node_name ne $local_hostname && defined($destination)) { + # accept only traffic on same ring + ruleset_addrule($ruleset, $chain, "-d $destination -s $node_ip $corosync_rule", "-j $accept_action"); + } + }); } # implement input policy @@ -2500,15 +2568,30 @@ sub enable_host_firewall { } # allow standard traffic on cluster network + my $localnet = $cluster_conf->{aliases}->{local_network}->{cidr}; + my $localnet_ver = $cluster_conf->{aliases}->{local_network}->{ipversion}; + if ($localnet && ($ipversion == $localnet_ver)) { ruleset_addrule($ruleset, $chain, "-d $localnet -p tcp --dport 8006", "-j $accept_action"); # PVE API ruleset_addrule($ruleset, $chain, "-d $localnet -p tcp --dport 22", "-j $accept_action"); # SSH ruleset_addrule($ruleset, $chain, "-d $localnet -p tcp --dport 5900:5999", "-j $accept_action"); # PVE VNC Console ruleset_addrule($ruleset, $chain, "-d $localnet -p tcp --dport 3128", "-j $accept_action"); # SPICE Proxy + } + + # corosync outbound rules + if (defined($corosync_conf)) { + ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST $corosync_rule", "-j $accept_action") + if $multicast_enabled; - my $corosync_rule = "-p udp --dport 5404:5405"; - ruleset_addrule($ruleset, $chain, "-d $localnet $corosync_rule", "-j $accept_action"); - ruleset_addrule($ruleset, $chain, "-m addrtype --dst-type MULTICAST $corosync_rule", "-j $accept_action"); + PVE::Corosync::for_all_corosync_addresses($corosync_conf, $ipversion, sub { + my ($node_name, $node_ip, $node_ipversion, $key) = @_; + my $source = $corosync_local_addresses->{$key}; + + if ($node_name ne $local_hostname && defined($source)) { + # accept only traffic on same ring + ruleset_addrule($ruleset, $chain, "-s $source -d $node_ip $corosync_rule", "-j $accept_action"); + } + }); } # implement output policy @@ -2701,13 +2784,13 @@ sub parse_hostfw_option { my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog"; - if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid):\s*(0|1)\s*$/i) { + if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid|protection_synflood):\s*(0|1)\s*$/i) { $opt = lc($1); $value = int($2); } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) { $opt = lc($1); $value = $2 ? lc($3) : ''; - } elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established):\s*(\d+)\s*$/i) { + } elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established|nf_conntrack_tcp_timeout_syn_recv|protection_synflood_rate|protection_synflood_burst|protection_limit):\s*(\d+)\s*$/i) { $opt = lc($1); $value = int($2); } else { @@ -2760,7 +2843,7 @@ sub parse_ip_or_cidr { my ($cidr) = @_; my $ipversion; - + if ($cidr =~ m!^(?:$IPV6RE)(/(\d+))?$!) { $cidr =~ s|/128$||; $ipversion = 6; @@ -2799,20 +2882,29 @@ sub parse_alias { } sub generic_fw_config_parser { - my ($filename, $fh, $cluster_conf, $empty_conf, $rule_env) = @_; + my ($filename, $cluster_conf, $empty_conf, $rule_env) = @_; my $section; my $group; my $res = $empty_conf; - while (defined(my $line = <$fh>)) { + my $raw; + if ($filename =~ m!^/etc/pve/(.*)$!) { + $raw = PVE::Cluster::get_config($1); + } else { + $raw = eval { PVE::Tools::file_get_contents($filename) }; # ignore errors + } + return {} if !$raw; + + my $linenr = 0; + while ($raw =~ /^\h*(.*?)\h*$/gm) { + my $line = $1; + $linenr++; next if $line =~ m/^#/; next if $line =~ m/^\s*$/; - chomp $line; - my $linenr = $fh->input_line_number(); my $prefix = "$filename (line $linenr)"; if ($empty_conf->{options} && ($line =~ m/^\[options\]$/i)) { @@ -2838,7 +2930,7 @@ sub generic_fw_config_parser { warn "$prefix: $err"; next; } - + $res->{$section}->{$group} = []; $res->{group_comments}->{$group} = decode('utf8', $comment) if $comment; @@ -2854,7 +2946,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}$/; }; @@ -2923,7 +3015,7 @@ 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 { @@ -2961,47 +3053,8 @@ sub generic_fw_config_parser { return $res; } -sub parse_hostfw_config { - my ($filename, $fh, $cluster_conf) = @_; - - my $empty_conf = { rules => [], options => {}}; - - return generic_fw_config_parser($filename, $fh, $cluster_conf, $empty_conf, 'host'); -} - -sub parse_vmfw_config { - my ($filename, $fh, $cluster_conf, $rule_env) = @_; - - my $empty_conf = { - rules => [], - options => {}, - aliases => {}, - ipset => {} , - ipset_comments => {}, - }; - - return generic_fw_config_parser($filename, $fh, $cluster_conf, $empty_conf, $rule_env); -} - -sub parse_clusterfw_config { - my ($filename, $fh) = @_; - - my $section; - my $group; - - my $empty_conf = { - rules => [], - options => {}, - aliases => {}, - groups => {}, - group_comments => {}, - ipset => {} , - ipset_comments => {}, - }; - - return generic_fw_config_parser($filename, $fh, $empty_conf, $empty_conf, 'cluster'); -} - +# 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) = @_; @@ -3050,18 +3103,34 @@ 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) = @_; - my $vmfw_conf = {}; - $dir = $pvefw_conf_dir if !defined($dir); - my $filename = "$dir/$vmid.fw"; - if (my $fh = IO::File->new($filename, O_RDONLY)) { - $vmfw_conf = parse_vmfw_config($filename, $fh, $cluster_conf, $rule_env); - $vmfw_conf->{vmid} = $vmid; - } + + my $empty_conf = { + rules => [], + options => {}, + aliases => {}, + ipset => {} , + ipset_comments => {}, + }; + + my $vmfw_conf = generic_fw_config_parser($filename, $cluster_conf, $empty_conf, $rule_env); + $vmfw_conf->{vmid} = $vmid; return $vmfw_conf; } @@ -3138,7 +3207,7 @@ my $format_aliases = sub { my $format_ipsets = sub { my ($fw_conf) = @_; - + my $raw = ''; foreach my $ipset (sort keys %{$fw_conf->{ipset}}) { @@ -3213,13 +3282,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 { @@ -3229,12 +3300,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; } @@ -3393,17 +3464,31 @@ 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) = @_; $filename = $clusterfw_conf_filename if !defined($filename); + my $empty_conf = { + rules => [], + options => {}, + aliases => {}, + groups => {}, + group_comments => {}, + ipset => {} , + ipset_comments => {}, + }; - my $cluster_conf = {}; - if (my $fh = IO::File->new($filename, O_RDONLY)) { - $cluster_conf = parse_clusterfw_config($filename, $fh); - - $set_global_log_ratelimit->($cluster_conf->{options}); - } + my $cluster_conf = generic_fw_config_parser($filename, $empty_conf, $empty_conf, 'cluster'); + $set_global_log_ratelimit->($cluster_conf->{options}); return $cluster_conf; } @@ -3420,7 +3505,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"; @@ -3451,16 +3536,22 @@ 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) = @_; $filename = $hostfw_conf_filename if !defined($filename); - my $hostfw_conf = {}; - if (my $fh = IO::File->new($filename, O_RDONLY)) { - $hostfw_conf = parse_hostfw_config($filename, $fh, $cluster_conf); - } - return $hostfw_conf; + my $empty_conf = { rules => [], options => {}}; + return generic_fw_config_parser($filename, $cluster_conf, $empty_conf, 'host'); } sub save_hostfw_conf { @@ -3486,7 +3577,7 @@ sub save_hostfw_conf { } sub compile { - my ($cluster_conf, $hostfw_conf, $vmdata) = @_; + my ($cluster_conf, $hostfw_conf, $vmdata, $corosync_conf) = @_; my $vmfw_configs; @@ -3507,6 +3598,10 @@ sub compile { $hostfw_conf = load_hostfw_conf($cluster_conf, undef) if !$hostfw_conf; + # cfs_update is handled by daemon or API + $corosync_conf = PVE::Cluster::cfs_read_file("corosync.conf") + if !defined($corosync_conf) && PVE::Corosync::check_conf_exists(1); + $vmdata = read_local_vm_config(); $vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, undef); } @@ -3526,16 +3621,44 @@ sub compile { push @{$cluster_conf->{ipset}->{management}}, { cidr => $localnet }; - my $ruleset = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4); - my $rulesetv6 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6); + my $ruleset = {}; + my $rulesetv6 = {}; + $ruleset->{filter} = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4); + $ruleset->{raw} = compile_iptables_raw($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4); + $rulesetv6->{filter} = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 6); + $rulesetv6->{raw} = compile_iptables_raw($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 6); my $ebtables_ruleset = compile_ebtables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata); my $ipset_ruleset = compile_ipsets($cluster_conf, $vmfw_configs, $vmdata); return ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset); } +sub compile_iptables_raw { + my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, $ipversion) = @_; + + my $ruleset = {}; + + my $hostfw_options = $hostfw_conf->{options} || {}; + my $protection_synflood = $hostfw_options->{protection_synflood} || 0; + + if($protection_synflood) { + + my $protection_synflood_rate = $hostfw_options->{protection_synflood_rate} ? $hostfw_options->{protection_synflood_rate} : 200; + my $protection_synflood_burst = $hostfw_options->{protection_synflood_burst} ? $hostfw_options->{protection_synflood_burst} : 1000; + my $protection_synflood_limit = $hostfw_options->{protection_synflood_limit} ? $hostfw_options->{protection_synflood_limit} : 3000; + my $protection_synflood_expire = $hostfw_options->{nf_conntrack_tcp_timeout_syn_recv} ? $hostfw_options->{nf_conntrack_tcp_timeout_syn_recv} : 60; + $protection_synflood_expire = $protection_synflood_expire * 1000; + my $protection_synflood_mask = $ipversion == 4 ? 32 : 64; + + ruleset_create_chain($ruleset, "PVEFW-PREROUTING"); + ruleset_addrule($ruleset, "PVEFW-PREROUTING", "-p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m hashlimit --hashlimit-above $protection_synflood_rate/sec --hashlimit-burst $protection_synflood_burst --hashlimit-mode srcip --hashlimit-name syn --hashlimit-htable-size 2097152 --hashlimit-srcmask $protection_synflood_mask --hashlimit-htable-expire $protection_synflood_expire", "-j DROP"); + } + + return $ruleset; +} + sub compile_iptables_filter { - my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion) = @_; + my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, $ipversion) = @_; my $ruleset = {}; @@ -3565,7 +3688,7 @@ sub compile_iptables_filter { my $hostfw_enable = !(defined($hostfw_options->{enable}) && ($hostfw_options->{enable} == 0)); if ($hostfw_enable) { - eval { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf, $ipversion); }; + eval { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf, $ipversion, $corosync_conf); }; warn $@ if $@; # just to be sure - should not happen } @@ -3574,19 +3697,19 @@ sub compile_iptables_filter { eval { my $conf = $vmdata->{qemu}->{$vmid}; my $vmfw_conf = $vmfw_configs->{$vmid}; - return if !$vmfw_conf; + return if !$vmfw_conf || !$vmfw_conf->{options}->{enable}; foreach my $netid (sort keys %$conf) { next if $netid !~ m/^net(\d+)$/; my $net = PVE::QemuServer::parse_net($conf->{$netid}); next if !$net->{firewall}; - my $iface = "tap${vmid}i$1"; + my $iface = "tap${vmid}i$1"; my $macaddr = $net->{macaddr}; generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, - $vmfw_conf, $vmid, 'IN', $ipversion); + $vmfw_conf, $vmid, 'IN', $ipversion); generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, - $vmfw_conf, $vmid, 'OUT', $ipversion); + $vmfw_conf, $vmid, 'OUT', $ipversion); } }; warn $@ if $@; # just to be sure - should not happen @@ -3594,29 +3717,28 @@ sub compile_iptables_filter { # generate firewall rules for LXC containers foreach my $vmid (sort keys %{$vmdata->{lxc}}) { - eval { - my $conf = $vmdata->{lxc}->{$vmid}; - my $vmfw_conf = $vmfw_configs->{$vmid}; - return if !$vmfw_conf; - - if ($vmfw_conf->{options}->{enable}) { - foreach my $netid (sort keys %$conf) { - next if $netid !~ m/^net(\d+)$/; - my $net = PVE::LXC::Config->parse_lxc_network($conf->{$netid}); - next if !$net->{firewall}; - my $iface = "veth${vmid}i$1"; - my $macaddr = $net->{hwaddr}; - generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, - $vmfw_conf, $vmid, 'IN', $ipversion); - generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, - $vmfw_conf, $vmid, 'OUT', $ipversion); - } - } - }; - warn $@ if $@; # just to be sure - should not happen + eval { + my $conf = $vmdata->{lxc}->{$vmid}; + my $vmfw_conf = $vmfw_configs->{$vmid}; + return if !$vmfw_conf || !$vmfw_conf->{options}->{enable}; + + foreach my $netid (sort keys %$conf) { + next if $netid !~ m/^net(\d+)$/; + my $net = PVE::LXC::Config->parse_lxc_network($conf->{$netid}); + next if !$net->{firewall}; + + my $iface = "veth${vmid}i$1"; + my $macaddr = $net->{hwaddr}; + generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, + $vmfw_conf, $vmid, 'IN', $ipversion); + generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr, + $vmfw_conf, $vmid, 'OUT', $ipversion); + } + }; + warn $@ if $@; # just to be sure - should not happen } - if(ruleset_chain_exist($ruleset, "PVEFW-IPS")){ + if (ruleset_chain_exist($ruleset, "PVEFW-IPS")){ ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED", "-j PVEFW-IPS"); } @@ -3645,7 +3767,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 }; } @@ -3812,7 +3934,14 @@ sub compile_ebtables_filter { push(@$arpfilter, $ip); } } - push(@$arpfilter, $net->{ip}) if $net->{ip} && $vmfw_conf->{options}->{ipfilter}; + if (defined(my $ip = $net->{ip}) && $vmfw_conf->{options}->{ipfilter}) { + # ebtables changes this to a .0/MASK network but we just + # want the address here, no network - see #2193 + $ip =~ s|/(\d+)$||; + if ($ip ne 'dhcp') { + push @$arpfilter, $ip; + } + } generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter); } }; @@ -3928,11 +4057,13 @@ sub print_sig_rule { } sub get_ruleset_cmdlist { - my ($ruleset, $iptablescmd) = @_; + my ($ruleset, $iptablescmd, $table) = @_; + + $table = 'filter' if !$table; - my $cmdlist = "*filter\n"; # we pass this to iptables-restore; + my $cmdlist = "*$table\n"; # we pass this to iptables-restore; - my ($active_chains, $hooks) = iptables_get_chains($iptablescmd); + my ($active_chains, $hooks) = iptables_get_chains($iptablescmd, $table); my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest); # create missing chains first @@ -3944,7 +4075,7 @@ sub get_ruleset_cmdlist { $cmdlist .= ":$chain - [0:0]\n"; } - foreach my $h (qw(INPUT OUTPUT FORWARD)) { + foreach my $h (qw(INPUT OUTPUT FORWARD PREROUTING)) { my $chain = "PVEFW-$h"; if ($ruleset->{$chain} && !$hooks->{$h}) { $cmdlist .= "-A $h -j $chain\n"; @@ -3979,10 +4110,11 @@ sub get_ruleset_cmdlist { next if $chain eq 'PVEFW-INPUT'; next if $chain eq 'PVEFW-OUTPUT'; next if $chain eq 'PVEFW-FORWARD'; + next if $chain eq 'PVEFW-PREROUTING'; $cmdlist .= "-X $chain\n"; } - my $changes = $cmdlist ne "*filter\n" ? 1 : 0; + my $changes = $cmdlist ne "*$table\n" ? 1 : 0; $cmdlist .= "COMMIT\n"; @@ -4013,8 +4145,8 @@ sub get_ebtables_cmdlist { foreach my $chain (sort keys %$statushash) { my $stat = $statushash->{$chain}; - next if ($stat->{action} eq 'delete'); $changes = 1 if ($stat->{action} !~ 'ignore|exists'); + next if ($stat->{action} eq 'delete'); foreach my $cmd (@{$statushash->{$chain}->{'rules'}}) { if ($chain eq 'FORWARD' && $cmd eq $append_pve_to_forward) { @@ -4095,9 +4227,11 @@ sub apply_ruleset { my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) = get_ipset_cmdlist($ipset_ruleset); - my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset); - my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, "ip6tables"); + my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset->{filter}); + my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables"); my ($ebtables_cmdlist, $ebtables_changes) = get_ebtables_cmdlist($ebtables_ruleset); + my ($cmdlist_raw, $changes_raw) = get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw'); + my ($cmdlistv6_raw, $changesv6_raw) = get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw'); if ($verbose) { if ($ipset_changes) { @@ -4116,6 +4250,16 @@ sub apply_ruleset { print $cmdlistv6; } + if ($changes_raw) { + print "iptables table raw changes:\n"; + print $cmdlist_raw; + } + + if ($changesv6_raw) { + print "ip6tables table raw changes:\n"; + print $cmdlistv6_raw; + } + if ($ebtables_changes) { print "ebtables changes:\n"; print $ebtables_cmdlist; @@ -4132,11 +4276,21 @@ sub apply_ruleset { iptables_restore_cmdlist($cmdlist); + $tmpfile = "$pve_fw_status_dir/ip4cmdlistraw"; + PVE::Tools::file_set_contents($tmpfile, $cmdlist_raw || ''); + + iptables_restore_cmdlist($cmdlist_raw, 'raw'); + $tmpfile = "$pve_fw_status_dir/ip6cmdlist"; PVE::Tools::file_set_contents($tmpfile, $cmdlistv6 || ''); ip6tables_restore_cmdlist($cmdlistv6); + $tmpfile = "$pve_fw_status_dir/ip6cmdlistraw"; + PVE::Tools::file_set_contents($tmpfile, $cmdlistv6_raw || ''); + + ip6tables_restore_cmdlist($cmdlistv6_raw, 'raw'); + $tmpfile = "$pve_fw_status_dir/ipsetcmdlist2"; PVE::Tools::file_set_contents($tmpfile, $ipset_delete_cmdlist || ''); @@ -4148,11 +4302,12 @@ sub apply_ruleset { PVE::Tools::file_set_contents($tmpfile, $ebtables_cmdlist || ''); # test: re-read status and check if everything is up to date + my $ruleset_filter = $ruleset->{filter}; my $active_chains = iptables_get_chains(); - my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest); + my $statushash = get_ruleset_status($ruleset_filter, $active_chains, \&iptables_chain_digest); my $errors; - foreach my $chain (sort keys %$ruleset) { + foreach my $chain (sort keys %$ruleset_filter) { my $stat = $statushash->{$chain}; if ($stat->{action} ne 'exists') { warn "unable to update chain '$chain'\n"; @@ -4160,10 +4315,11 @@ sub apply_ruleset { } } + my $rulesetv6_filter = $rulesetv6->{filter}; my $active_chainsv6 = iptables_get_chains("ip6tables"); - my $statushashv6 = get_ruleset_status($rulesetv6, $active_chainsv6, \&iptables_chain_digest); + my $statushashv6 = get_ruleset_status($rulesetv6_filter, $active_chainsv6, \&iptables_chain_digest); - foreach my $chain (sort keys %$rulesetv6) { + foreach my $chain (sort keys %$rulesetv6_filter) { my $stat = $statushashv6->{$chain}; if ($stat->{action} ne 'exists') { warn "unable to update chain '$chain'\n"; @@ -4171,6 +4327,30 @@ sub apply_ruleset { } } + my $ruleset_raw = $ruleset->{raw}; + my $active_chains_raw = iptables_get_chains(undef, 'raw'); + my $statushash_raw = get_ruleset_status($ruleset_raw, $active_chains_raw, \&iptables_chain_digest); + + foreach my $chain (sort keys %$ruleset_raw) { + my $stat = $statushash_raw->{$chain}; + if ($stat->{action} ne 'exists') { + warn "unable to update chain '$chain'\n"; + $errors = 1; + } + } + + my $rulesetv6_raw = $rulesetv6->{raw}; + my $active_chainsv6_raw = iptables_get_chains("ip6tables", 'raw'); + my $statushashv6_raw = get_ruleset_status($rulesetv6_raw, $active_chainsv6_raw, \&iptables_chain_digest); + + foreach my $chain (sort keys %$rulesetv6_raw) { + my $stat = $statushashv6_raw->{$chain}; + if ($stat->{action} ne 'exists') { + warn "unable to update chain '$chain'\n"; + $errors = 1; + } + } + my $active_ebtables_chains = ebtables_get_chains(); my $ebtables_statushash = get_ruleset_status($ebtables_ruleset, $active_ebtables_chains, \&iptables_chain_digest, @@ -4190,13 +4370,15 @@ sub apply_ruleset { update_nf_conntrack_tcp_timeout_established($hostfw_conf); + update_nf_conntrack_tcp_timeout_syn_recv($hostfw_conf); + update_nf_conntrack_logging($hostfw_conf); } sub update_nf_conntrack_max { my ($hostfw_conf) = @_; - my $max = 65536; # reasonable default + my $max = 262144; # reasonable default (2^16 * 4), see nf_conntrack-sysctl docs my $options = $hostfw_conf->{options} || {}; @@ -4227,6 +4409,16 @@ sub update_nf_conntrack_tcp_timeout_established { PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established", $value); } +sub update_nf_conntrack_tcp_timeout_syn_recv { + my ($hostfw_conf) = @_; + + my $options = $hostfw_conf->{options} || {}; + + my $value = defined($options->{nf_conntrack_tcp_timeout_syn_recv}) ? $options->{nf_conntrack_tcp_timeout_syn_recev} : 60; + + PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv", $value); +} + my $log_nf_conntrack_enabled = undef; sub update_nf_conntrack_logging { my ($hostfw_conf) = @_; @@ -4239,7 +4431,7 @@ sub update_nf_conntrack_logging { my $tmpfile = "$pve_fw_status_dir/log_nf_conntrack"; PVE::Tools::file_set_contents($tmpfile, $value); - PVE::Tools::run_command([qw(systemctl try-reload-or-restart pvefw-logger.service)]); + run_command([qw(systemctl try-reload-or-restart pvefw-logger.service)]); $log_nf_conntrack_enabled = $value; } } @@ -4248,17 +4440,22 @@ sub remove_pvefw_chains { PVE::Firewall::remove_pvefw_chains_iptables("iptables"); PVE::Firewall::remove_pvefw_chains_iptables("ip6tables"); + PVE::Firewall::remove_pvefw_chains_iptables("iptables", "raw"); + PVE::Firewall::remove_pvefw_chains_iptables("ip6tables", "raw"); PVE::Firewall::remove_pvefw_chains_ipset(); + PVE::Firewall::remove_pvefw_chains_ebtables(); } sub remove_pvefw_chains_iptables { - my ($iptablescmd) = @_; + my ($iptablescmd, $table) = @_; - my ($chash, $hooks) = iptables_get_chains($iptablescmd); - my $cmdlist = "*filter\n"; + $table = 'filter' if !$table; + + my ($chash, $hooks) = iptables_get_chains($iptablescmd, $table); + my $cmdlist = "*$table\n"; - foreach my $h (qw(INPUT OUTPUT FORWARD)) { + foreach my $h (qw(INPUT OUTPUT FORWARD PREROUTING)) { if ($hooks->{$h}) { $cmdlist .= "-D $h -j PVEFW-$h\n"; } @@ -4274,9 +4471,9 @@ sub remove_pvefw_chains_iptables { $cmdlist .= "COMMIT\n"; if($iptablescmd eq "ip6tables") { - ip6tables_restore_cmdlist($cmdlist); + ip6tables_restore_cmdlist($cmdlist, $table); } else { - iptables_restore_cmdlist($cmdlist); + iptables_restore_cmdlist($cmdlist, $table); } } @@ -4285,7 +4482,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"; @@ -4294,6 +4491,11 @@ sub remove_pvefw_chains_ipset { ipset_restore_cmdlist($cmdlist) if $cmdlist; } +sub remove_pvefw_chains_ebtables { + # apply empty ruleset = remove all our chains + ebtables_restore_cmdlist(get_ebtables_cmdlist({})); +} + sub init { my $cluster_conf = load_clusterfw_conf(); my $cluster_options = $cluster_conf->{options};