X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=d40a9b17328144fefd9c6ef308a460d9566a6664;hb=21d5ba9c34d729f2b1d3a895a4703b3079309e51;hp=e349ee7769a6d529a3414075bf04520085e31d06;hpb=644b5fc95ad95e370893f902e3296b948f182f9a;p=pve-firewall.git diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index e349ee7..d40a9b1 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -24,6 +24,8 @@ use PVE::SafeSyslog; use PVE::Tools qw($IPV4RE $IPV6RE); use PVE::Tools qw(run_command lock_file dir_glob_foreach); +use PVE::Firewall::Helpers; + my $pvefw_conf_dir = "/etc/pve/firewall"; my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw"; @@ -213,7 +215,7 @@ my $pve_fw_macros = { { action => 'PARAM', proto => 'udp', dport => '6881' }, ], 'Ceph' => [ - "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Deamons)", + "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)", # Legacy port for protocol v1 { action => 'PARAM', proto => 'tcp', dport => '6789' }, # New port for protocol v2 @@ -276,7 +278,7 @@ my $pve_fw_macros = { { action => 'PARAM', proto => 'tcp', dport => '9418' }, ], 'HKP' => [ - "OpenPGP HTTP keyserver protocol traffic", + "OpenPGP HTTP key server protocol traffic", { action => 'PARAM', proto => 'tcp', dport => '11371' }, ], 'HTTP' => [ @@ -494,6 +496,10 @@ my $pve_fw_macros = { { action => 'PARAM', proto => '41' }, { action => 'PARAM', proto => 'udp', dport => '5072,8374' }, ], + 'SPICEproxy' => [ + "Proxmox VE SPICE display proxy traffic", + { action => 'PARAM', proto => 'tcp', dport => '3128' }, + ], 'Squid' => [ "Squid web proxy traffic", { action => 'PARAM', proto => 'tcp', dport => '3128' }, @@ -592,7 +598,6 @@ $pve_std_chains_conf->{4} = { # same as shorewall 'Drop', which is equal to DROP, # but REJECT/DROP some packages to reduce logging, # and ACCEPT critical ICMP types - { action => 'PVEFW-reject', proto => 'tcp', dport => '43' }, # REJECT 'auth' # we are not interested in BROADCAST/MULTICAST/ANYCAST { action => 'PVEFW-DropBroadcast' }, # ACCEPT critical ICMP types @@ -615,7 +620,6 @@ $pve_std_chains_conf->{4} = { # same as shorewall 'Reject', which is equal to Reject, # but REJECT/DROP some packages to reduce logging, # and ACCEPT critical ICMP types - { action => 'PVEFW-reject', proto => 'tcp', dport => '43' }, # REJECT 'auth' # we are not interested in BROADCAST/MULTICAST/ANYCAST { action => 'PVEFW-DropBroadcast' }, # ACCEPT critical ICMP types @@ -636,7 +640,7 @@ $pve_std_chains_conf->{4} = { ], 'PVEFW-tcpflags' => [ # same as shorewall tcpflags action. - # Packets arriving on this interface are checked for som illegal combinations of TCP flags + # Packets arriving on this interface are checked for some illegal combinations of TCP flags { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' }, { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' }, { match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' }, @@ -729,7 +733,7 @@ $pve_std_chains_conf->{6} = { ], 'PVEFW-tcpflags' => [ # same as shorewall tcpflags action. - # Packets arriving on this interface are checked for som illegal combinations of TCP flags + # Packets arriving on this interface are checked for some illegal combinations of TCP flags { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' }, { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' }, { match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' }, @@ -785,12 +789,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, @@ -810,6 +816,17 @@ my $icmpv6_type_names = { 'redirect' => 1, }; +my $is_valid_icmp_type = sub { + my ($type, $valid_types) = @_; + + if ($type =~ m/^\d+$/) { + # values for icmp-type range between 0 and 255 (8 bit field) + die "invalid icmp-type '$type'\n" if $type > 255; + } else { + die "unknown icmp-type '$type'\n" if !defined($valid_types->{$type}); + } +}; + sub init_firewall_macros { $pve_fw_parsed_macros = {}; @@ -1080,7 +1097,7 @@ sub parse_port_name_number_or_range { } } - die "ICPM ports not allowed in port range\n" if $icmp_port && $count > 0; + die "ICMP ports not allowed in port range\n" if $icmp_port && $count > 0; # I really don't like to use the word number here, but it's the only thing # that makes sense in a literal way. The range 1:100 counts as 2, not as @@ -1131,6 +1148,19 @@ sub pve_fw_verify_protocol_spec { return $proto; } +PVE::JSONSchema::register_format('pve-fw-icmp-type-spec', \&pve_fw_verify_icmp_type_spec); +sub pve_fw_verify_icmp_type_spec { + my ($icmp_type) = @_; + + if ($icmp_type_names->{$icmp_type} || $icmpv6_type_names->{$icmp_type}) { + return $icmp_type; + } + + die "invalid icmp-type value '$icmp_type'\n" if $icmp_type ne ''; + + return $icmp_type; +} + # helper function for API @@ -1230,7 +1260,7 @@ our $cluster_option_properties = { type => 'integer', minimum => 0, optional => 1, - description => 'Inital burst of packages which will get logged', + description => 'Initial burst of packages which will always get logged before the rate is applied', default => 5, }, }, @@ -1333,7 +1363,7 @@ our $vm_option_properties = { macfilter => { description => "Enable/disable MAC address filter.", type => 'boolean', - default => 0, + default => 1, optional => 1, }, dhcp => { @@ -1423,11 +1453,13 @@ my $rule_properties = { description => "Restrict packet source address. $addr_list_descr", type => 'string', format => 'pve-fw-addr-spec', optional => 1, + maxLength => 512, }, dest => { description => "Restrict packet destination address. $addr_list_descr", type => 'string', format => 'pve-fw-addr-spec', optional => 1, + maxLength => 512, }, proto => { description => "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.", @@ -1458,6 +1490,11 @@ my $rule_properties = { type => 'string', optional => 1, }, + 'icmp-type' => { + description => "Specify icmp-type. Only valid if proto equals 'icmp'.", + type => 'string', format => 'pve-fw-icmp-type-spec', + optional => 1, + }, }; sub add_rule_properties { @@ -1578,7 +1615,7 @@ sub verify_rule { my $set_ip_version = sub { my $vers = shift; if ($vers) { - die "detected mixed ipv4/ipv6 adresses in rule\n" + die "detected mixed ipv4/ipv6 addresses in rule\n" if $ipversion && ($vers != $ipversion); $ipversion = $vers; } @@ -1651,7 +1688,8 @@ sub verify_rule { eval { pve_fw_verify_protocol_spec($rule->{proto}); }; &$add_error('proto', $@) if $@; &$set_ip_version(4) if $rule->{proto} eq 'icmp'; - &$set_ip_version(6) if $rule->{proto} eq 'icmpv6'; + &$set_ip_version(6) if $rule->{proto} eq 'icmpv6'; + &$set_ip_version(6) if $rule->{proto} eq 'ipv6-icmp'; } if ($rule->{dport}) { @@ -1665,6 +1703,19 @@ sub verify_rule { $proto ne 'icmp' && $proto ne 'icmpv6'; # special cases } + if (my $icmp_type = $rule ->{'icmp-type'}) { + my $proto = $rule->{proto}; + &$add_error('proto', "missing property - 'icmp-type' requires this property") + if $proto ne 'icmp' && $proto ne 'icmpv6' && $proto ne 'ipv6-icmp'; + &$add_error('icmp-type', "'icmp-type' cannot be specified together with 'dport'") + if $rule->{dport}; + if ($proto eq 'icmp' && !$icmp_type_names->{$icmp_type}) { + &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto 'icmp'"); + } elsif (($proto eq 'icmpv6' || $proto eq 'ipv6-icmp') && !$icmpv6_type_names->{$icmp_type}) { + &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto '$proto'"); + } + } + if ($rule->{sport}) { eval { parse_port_name_number_or_range($rule->{sport}, 0); }; &$add_error('sport', $@) if $@; @@ -1771,11 +1822,9 @@ sub rules_audit_permissions { } # core functions -my $bridge_firewall_enabled = 0; sub enable_bridge_firewall { - return if $bridge_firewall_enabled; # only once PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1"); PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1"); @@ -1783,7 +1832,6 @@ sub enable_bridge_firewall { # make sure syncookies are enabled (which is default on newer 3.X kernels anyways) PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1"); - $bridge_firewall_enabled = 1; } sub iptables_restore_cmdlist { @@ -1905,6 +1953,8 @@ sub ipset_get_chains { return if $line =~ m/^\s*$/; if ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) { my $chain = $1; + # ignore initval from ipset v7.7+, won't set that yet so it'd mess up change detection + $line =~ s/\binitval 0x[0-9a-f]+//; $line =~ s/\s+$//; # delete trailing white space push @{$chains->{$chain}}, $line; } else { @@ -1927,13 +1977,22 @@ sub ebtables_get_chains { my $res = {}; my $chains = {}; + my $table; my $parser = sub { my $line = shift; return if $line =~ m/^#/; return if $line =~ m/^\s*$/; - if ($line =~ m/^:(\S+)\s\S+$/) { + if ($line =~ m/^\*(\S+)$/) { + $table = $1; + return; + } + + return if $table ne "filter"; + + if ($line =~ m/^:(\S+)\s(ACCEPT|DROP|RETURN)$/) { # Make sure we know chains exist even if they're empty. $chains->{$1} //= []; + $res->{$1}->{policy} = $2; } elsif ($line =~ m/^(?:\S+)\s(\S+)\s(?:\S+).*/) { my $chain = $1; $line =~ s/\s+$//; @@ -1953,7 +2012,7 @@ sub ebtables_get_chains { return $res; } -# substitude action of rule according to action hash +# substitute action of rule according to action hash sub rule_substitude_action { my ($rule, $actions) = @_; @@ -2035,23 +2094,21 @@ sub ipt_rule_to_cmds { my $multisport = defined($rule->{sport}) && parse_port_name_number_or_range($rule->{sport}, 0); my $add_dport = sub { - return if !$rule->{dport}; + return if !defined($rule->{dport}); + # NOTE: we re-use dport to store --icmp-type for icmp* protocol if ($proto eq 'icmp') { - # 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}}); + $is_valid_icmp_type->($rule->{dport}, $icmp_type_names); 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}}); + $is_valid_icmp_type->($rule->{dport}, $icmpv6_type_names); push @match, "-m icmpv6 --icmpv6-type $rule->{dport}"; } elsif (!$PROTOCOLS_WITH_PORTS->{$proto}) { die "protocol $proto does not have ports\n"; } elsif ($multidport) { push @match, "--match multiport", "--dports $rule->{dport}"; } else { + return if !$rule->{dport}; push @match, "--dport $rule->{dport}"; } }; @@ -2068,7 +2125,18 @@ sub ipt_rule_to_cmds { } }; + my $add_icmp_type = sub { + return if !defined($rule->{'icmp-type'}) || $rule->{'icmp-type'} eq ''; + + die "'icmp-type' can only be set if 'icmp', 'icmpv6' or 'ipv6-icmp' is specified\n" + if ($proto ne 'icmp') && ($proto ne 'icmpv6') && ($proto ne 'ipv6-icmp'); + my $type = $proto eq 'icmp' ? 'icmp-type' : 'icmpv6-type'; + + push @match, "-m $proto --$type $rule->{'icmp-type'}"; + }; + # order matters - single port before multiport! + $add_icmp_type->(); $add_dport->() if $multisport; $add_sport->(); $add_dport->() if !$multisport; @@ -2692,32 +2760,36 @@ sub parse_fw_rule { last if $rule->{type} eq 'group'; - if ($line =~ s/^-p (\S+)\s*//) { + if ($line =~ s/^(?:-p|--?proto) (\S+)\s*//) { $rule->{proto} = $1; next; } - if ($line =~ s/^-dport (\S+)\s*//) { + if ($line =~ s/^--?dport (\S+)\s*//) { $rule->{dport} = $1; next; } - if ($line =~ s/^-sport (\S+)\s*//) { + if ($line =~ s/^--?sport (\S+)\s*//) { $rule->{sport} = $1; next; } - if ($line =~ s/^-source (\S+)\s*//) { + if ($line =~ s/^--?source (\S+)\s*//) { $rule->{source} = $1; next; } - if ($line =~ s/^-dest (\S+)\s*//) { + if ($line =~ s/^--?dest (\S+)\s*//) { $rule->{dest} = $1; next; } - if ($line =~ s/^-log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) { + if ($line =~ s/^--?log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) { $rule->{log} = $1; next; } + if ($line =~ s/^--?icmp-type (\S+)\s*//) { + $rule->{'icmp-type'} = $1; + next; + } last; } @@ -3098,28 +3170,21 @@ sub read_local_vm_config { } } } elsif ($d->{type} eq 'lxc') { - if ($have_lxc) { - my $cfspath = PVE::LXC::Config->cfs_config_path($vmid); - if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) { - $lxc->{$vmid} = $conf; - } - } - } + if ($have_lxc) { + my $cfspath = PVE::LXC::Config->cfs_config_path($vmid); + if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) { + $lxc->{$vmid} = $conf; + } + } + } } return $vmdata; }; +# FIXME: move use sites over to moved helper and break older packages, then remove this here 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; + return PVE::Firewall::Helpers::lock_vmfw_conf(@_); } sub load_vmfw_conf { @@ -3167,6 +3232,7 @@ my $format_rules = sub { $raw .= " -dport $rule->{dport}" if $rule->{dport}; $raw .= " -sport $rule->{sport}" if $rule->{sport}; $raw .= " -log $rule->{log}" if $rule->{log}; + $raw .= " -icmp-type $rule->{'icmp-type'}" if defined($rule->{'icmp-type'}) && $rule->{'icmp-type'} ne ''; } $raw .= " # " . encode('utf8', $rule->{comment}) @@ -3281,29 +3347,14 @@ sub save_vmfw_conf { } } +# FIXME: remove with 8.0 and break older qemu-server/pve-container sub remove_vmfw_conf { - my ($vmid) = @_; - - my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw"; - - unlink $vmfw_conffile; + return PVE::Firewall::Helpers::remove_vmfw_conf(@_); } +# FIXME: remove with 8.0 and break older qemu-server/pve-container sub clone_vmfw_conf { - my ($vmid, $newid) = @_; - - my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw"; - my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw"; - - 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); - } - }); + return PVE::Firewall::Helpers::clone_vmfw_conf(@_); } sub read_vm_firewall_configs { @@ -3426,9 +3477,13 @@ sub generate_ipset_chains { $hashsize = round_powerof2($hashsize); } + my $bucketsize = 12; # lower than the default of 14, faster but slightly more memory use + my $family = $ipversion == "6" ? "inet6" : "inet"; - $ipset_ruleset->{$name} = ["create $name hash:net family $family hashsize $hashsize maxelem $hashsize"]; + $ipset_ruleset->{$name} = [ + "create $name hash:net family $family hashsize $hashsize maxelem $hashsize bucketsize $bucketsize" + ]; foreach my $cidr (sort keys %$data) { my $entry = $data->{$cidr}; @@ -3549,10 +3604,12 @@ sub save_clusterfw_conf { } } -sub lock_hostfw_conf { - my ($timeout, $code, @param) = @_; +sub lock_hostfw_conf : prototype($$$@) { + my ($node, $timeout, $code, @param) = @_; + + $node = $nodename if !defined($node); - my $res = PVE::Cluster::cfs_lock_firewall("host-$nodename", $timeout, $code, @param); + my $res = PVE::Cluster::cfs_lock_firewall("host-$node", $timeout, $code, @param); die $@ if $@; return $res; @@ -3568,7 +3625,9 @@ sub load_hostfw_conf { } sub save_hostfw_conf { - my ($hostfw_conf) = @_; + my ($hostfw_conf, $filename) = @_; + + $filename = $hostfw_conf_filename if !defined($filename); my $raw = ''; @@ -3583,9 +3642,9 @@ sub save_hostfw_conf { } if ($raw) { - PVE::Tools::file_set_contents($hostfw_conf_filename, $raw); + PVE::Tools::file_set_contents($filename, $raw); } else { - unlink $hostfw_conf_filename; + unlink $filename; } } @@ -3797,7 +3856,7 @@ sub compile_ipsets { return if !$vmfw_conf; # When the 'ipfilter' option is enabled every device for which there - # is no 'ipfilter-netX' ipset defiend gets an implicit empty default + # is no 'ipfilter-netX' ipset defined gets an implicit empty default # ipset. # The reason is that ipfilter ipsets are always filled with standard # IPv6 link-local filters. @@ -3836,7 +3895,7 @@ sub compile_ipsets { return if !$vmfw_conf; # When the 'ipfilter' option is enabled every device for which there - # is no 'ipfilter-netX' ipset defiend gets an implicit empty default + # is no 'ipfilter-netX' ipset defined gets an implicit empty default # ipset. # The reason is that ipfilter ipsets are always filled with standard # IPv6 link-local filters, as well as the IP addresses configured @@ -3901,7 +3960,7 @@ sub compile_ebtables_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}; my $ipsets = $vmfw_conf->{ipset}; foreach my $netid (sort keys %$conf) { @@ -3977,7 +4036,7 @@ sub generate_tap_layer2filter { ruleset_create_chain($ruleset, $tapchain); if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) { - ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP'); + ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP'); } if (@$arpfilter){ @@ -4052,6 +4111,7 @@ sub get_ruleset_status { if (defined($change_only_regex)) { $action = 'ignore' if ($chain !~ m/$change_only_regex/); $statushash->{$chain}->{rules} = $active_chains->{$chain}->{rules}; + $statushash->{$chain}->{policy} = $active_chains->{$chain}->{policy}; $sig = $sig->{sig}; } $statushash->{$chain}->{action} = $action; @@ -4152,7 +4212,8 @@ sub get_ebtables_cmdlist { my $pve_include = 0; foreach my $chain (sort keys %$statushash) { next if ($statushash->{$chain}->{action} eq 'delete'); - $cmdlist .= ":$chain ACCEPT\n"; + my $policy = $statushash->{$chain}->{policy} // 'ACCEPT'; + $cmdlist .= ":$chain $policy\n"; $pve_include = 1 if ($chain eq 'PVEFW-FORWARD'); }