X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=c946040404a2636812407013a684d6bea12fe2d7;hp=72d5a69ae50d2e19bd3fe037da44a502000c194b;hb=c1031ab16cda7208eb161c891eceac31976a74b9;hpb=585ede439c205271353a17b64ad89da717df87fc diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index 72d5a69..c946040 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -3,27 +3,27 @@ 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"; @@ -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"; @@ -666,14 +667,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, @@ -1751,25 +1748,25 @@ sub enable_bridge_firewall { sub iptables_restore_cmdlist { my ($cmdlist) = @_; - run_command("/sbin/iptables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist"); + run_command(['iptables-restore', '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist"); } sub ip6tables_restore_cmdlist { my ($cmdlist) = @_; - run_command("/sbin/ip6tables-restore -n", input => $cmdlist, errmsg => "iptables_restore_cmdlist"); + run_command(['ip6tables-restore', '-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 { @@ -1828,7 +1825,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 +1869,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 +1900,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}; @@ -2391,21 +2388,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); @@ -2450,14 +2466,20 @@ sub enable_host_firewall { 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 - 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 +2522,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; + + PVE::Corosync::for_all_corosync_addresses($corosync_conf, $ipversion, sub { + my ($node_name, $node_ip, $node_ipversion, $key) = @_; + my $source = $corosync_local_addresses->{$key}; - 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"); + 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 @@ -2801,21 +2838,27 @@ sub parse_alias { sub generic_fw_config_parser { my ($filename, $cluster_conf, $empty_conf, $rule_env) = @_; - my $fh = IO::File->new($filename, O_RDONLY); - return {} if !$fh; - 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)) { @@ -3454,7 +3497,7 @@ sub save_hostfw_conf { } sub compile { - my ($cluster_conf, $hostfw_conf, $vmdata) = @_; + my ($cluster_conf, $hostfw_conf, $vmdata, $corosync_conf) = @_; my $vmfw_configs; @@ -3475,6 +3518,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); } @@ -3494,8 +3541,8 @@ 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 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4); + my $rulesetv6 = compile_iptables_filter($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); @@ -3503,7 +3550,7 @@ sub compile { } 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 = {}; @@ -3533,7 +3580,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 } @@ -3780,7 +3827,12 @@ 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+)$||; + push @$arpfilter, $ip; + } generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter); } }; @@ -3981,8 +4033,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) { @@ -4207,7 +4259,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; } } @@ -4217,6 +4269,7 @@ sub remove_pvefw_chains { PVE::Firewall::remove_pvefw_chains_iptables("iptables"); PVE::Firewall::remove_pvefw_chains_iptables("ip6tables"); PVE::Firewall::remove_pvefw_chains_ipset(); + PVE::Firewall::remove_pvefw_chains_ebtables(); } @@ -4262,6 +4315,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};