]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
add ip6tables standard chains
[pve-firewall.git] / src / PVE / Firewall.pm
index 03e75f99538064e9836bfb898329c137c47b7f57..7e346687037ab3bc61a821b0039217ab007a093c 100644 (file)
@@ -5,6 +5,7 @@ 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);
@@ -571,6 +572,99 @@ $pve_std_chains->{4} = {
     ],
 };
 
+$pve_std_chains->{6} = {
+    'PVEFW-SET-ACCEPT-MARK' => [
+        "-j MARK --set-mark 1",
+    ],
+    'PVEFW-DropBroadcast' => [
+        # same as shorewall 'Broadcast'
+        # simply DROP BROADCAST/MULTICAST/ANYCAST
+        # we can use this to reduce logging
+        #{ action => 'DROP', dsttype => 'BROADCAST' }, #no broadcast in ipv6
+        { action => 'DROP', dsttype => 'MULTICAST' },
+        { action => 'DROP', dsttype => 'ANYCAST' },
+        #{ 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' },
+        "-p tcp -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",
+    ],
+    'PVEFW-Drop' => [
+        # 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
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'destination-unreachable' },
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'time-exceeded' },
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'packet-too-big' },
+
+        # Drop packets with INVALID state
+        "-m conntrack --ctstate INVALID -j DROP",
+        # Drop Microsoft SMB noise
+        { action => 'DROP', proto => 'udp', dport => '135,445', nbdport => 2 },
+        { action => 'DROP', proto => 'udp', dport => '137:139'},
+        { action => 'DROP', proto => 'udp', dport => '1024:65535', sport => 137 },
+        { action => 'DROP', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
+        { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
+        # Drop new/NotSyn traffic so that it doesn't get logged
+        "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
+        # Drop DNS replies
+        { action => 'DROP', proto => 'udp', sport => 53 },
+    ],
+    'PVEFW-Reject' => [
+        # 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
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'destination-unreachable' },
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'time-exceeded' },
+        { action => 'ACCEPT', proto => 'icmpv6', dport => 'packet-too-big' },
+
+        # Drop packets with INVALID state
+        "-m conntrack --ctstate INVALID -j DROP",
+        # Drop Microsoft SMB noise
+        { action => 'PVEFW-reject', proto => 'udp', dport => '135,445', nbdport => 2 },
+        { action => 'PVEFW-reject', proto => 'udp', dport => '137:139'},
+        { action => 'PVEFW-reject', proto => 'udp', dport => '1024:65535', sport => 137 },
+        { action => 'PVEFW-reject', proto => 'tcp', dport => '135,139,445', nbdport => 3 },
+        { action => 'DROP', proto => 'udp', dport => 1900 }, # UPnP
+        # Drop new/NotSyn traffic so that it doesn't get logged
+        "-p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -j DROP",
+        # Drop DNS replies
+        { action => 'DROP', proto => 'udp', sport => 53 },
+    ],
+    'PVEFW-tcpflags' => [
+        # same as shorewall tcpflags action.
+        # Packets arriving on this interface are checked for som illegal combinations of TCP flags
+        "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG -g PVEFW-logflags",
+        "-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE -g PVEFW-logflags",
+        "-p tcp -m tcp --tcp-flags SYN,RST SYN,RST -g PVEFW-logflags",
+        "-p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -g PVEFW-logflags",
+        "-p tcp -m tcp --sport 0 --tcp-flags FIN,SYN,RST,ACK SYN -g PVEFW-logflags",
+    ],
+    'PVEFW-smurfs' => [
+        #does smurf attack works with ipv6, as broadcast not exist ???
+
+        # same as shorewall smurfs action
+        # Filter packets for smurfs (packets with a broadcast address as the source).
+        #"-s 0.0.0.0/32 -j RETURN",
+        #"-m addrtype --src-type BROADCAST -g PVEFW-smurflog",
+        #"-s 224.0.0.0/4 -g PVEFW-smurflog",
+    ],
+};
+
 # iptables -p icmp -h
 my $icmp_type_names = {
     any => 1,
@@ -612,6 +706,32 @@ my $icmp_type_names = {
     'address-mask-reply' => 1,
 };
 
+# ip6tables -p icmpv6 -h
+
+my $icmpv6_type_names = {
+    'any' => 1,
+    'destination-unreachable' => 1,
+    'no-route' => 1,
+    'communication-prohibited' => 1,
+    'address-unreachable' => 1,
+    'port-unreachable' => 1,
+    'packet-too-big' => 1,
+    'time-exceeded' => 1,
+    'ttl-zero-during-transit' => 1,
+    'ttl-zero-during-reassembly' => 1,
+    'parameter-problem' => 1,
+    'bad-header' => 1,
+    'unknown-header-type' => 1,
+    'unknown-option' => 1,
+    'echo-request' => 1,
+    'echo-reply' => 1,
+    'router-solicitation' => 1,
+    'router-advertisement' => 1,
+    'neighbour-solicitation' => 1,
+    'neighbour-advertisement' => 1,
+    'redirect' => 1,
+};
+
 sub init_firewall_macros {
 
     $pve_fw_parsed_macros = {};
@@ -698,6 +818,10 @@ sub get_etc_protocols {
 
     close($fh);
 
+    # add special case for ICMP v6
+    $protocols->{byid}->{icmpv6}->{name} = "icmpv6";
+    $protocols->{byname}->{icmpv6} = $protocols->{byid}->{icmpv6};
+
     $etc_protocols = $protocols;
 
     return $etc_protocols;
@@ -755,10 +879,12 @@ sub local_network {
     return $__local_network;
 }
 
-# ipset names are limited to 31 characters, and we use '_swap' 
-# suffix for atomic update, for example PVEFW-${VMID}-${ipset_name}_swap
+# 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, 
+# for example PVEFW-${VMID}-${ipset_name}_swap
 
-my $max_iptables_ipset_name_length = 31 - length("_swap");
+my $max_iptables_ipset_name_length = 31 - length("_swap") - length("-v4");
 
 sub compute_ipset_chain_name {
     my ($vmid, $ipset_name) = @_;
@@ -767,7 +893,6 @@ sub compute_ipset_chain_name {
 
     my $id = "$vmid-${ipset_name}";
 
-   
     if ((length($id) + 6) > $max_iptables_ipset_name_length) {
        $id = PVE::Tools::fnv31a_hex($id);
     }
@@ -839,6 +964,8 @@ sub parse_port_name_number_or_range {
        } else {
            if ($icmp_type_names->{$item}) {
                $icmp_port = 1;
+           } elsif ($icmpv6_type_names->{$item}) {
+               $icmp_port = 1;
            } else {
                die "invalid port '$item'\n" if !$services->{byname}->{$item};
            }
@@ -1161,9 +1288,21 @@ sub verify_rule {
        }
     }
 
+    my $ipversion;
+    my $set_ip_version = sub {
+       my $vers = shift;
+       if ($vers) {
+           die "detected mixed ipv4/ipv6 adresses in rule\n"
+               if $ipversion && ($vers != $ipversion);
+           $ipversion = $vers;
+       }
+    };
+
     if ($rule->{proto}) {
        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';
     }
 
     if ($rule->{dport}) {
@@ -1180,10 +1319,11 @@ sub verify_rule {
            if !$rule->{proto};
     }
 
-    my $ipversion;
-
     if ($rule->{source}) {
-       eval { $ipversion = parse_address_list($rule->{source}); };
+       eval { 
+           my $source_ipversion = parse_address_list($rule->{source});
+           &$set_ip_version($source_ipversion);
+       };
        &$add_error('source', $@) if $@;
        &$check_ipset_or_alias_property('source', $ipversion);
     }
@@ -1191,9 +1331,7 @@ sub verify_rule {
     if ($rule->{dest}) {
        eval { 
            my $dest_ipversion = parse_address_list($rule->{dest}); 
-           die "detected mixed ipv4/ipv6 adresses in rule\n"
-               if $ipversion && $dest_ipversion && ($dest_ipversion != $ipversion);
-           $ipversion = $dest_ipversion if $dest_ipversion;
+           &$set_ip_version($dest_ipversion);
        };
        &$add_error('dest', $@) if $@;
        &$check_ipset_or_alias_property('dest', $ipversion);
@@ -1508,6 +1646,10 @@ sub ruleset_generate_cmdstr {
                # Note: we use dport to store --icmp-type
                die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
                push @cmd, "-m icmp --icmp-type $rule->{dport}";
+           } elsif ($rule->{proto} && $rule->{proto} eq 'icmpv6') {
+               # Note: we use dport to store --icmpv6-type
+               die "unknown icmpv6-type '$rule->{dport}'\n" if !defined($icmpv6_type_names->{$rule->{dport}});
+               push @cmd, "-m icmpv6 --icmpv6-type $rule->{dport}";
            } else {
                if ($nbdport > 1) {
                    if ($multiport == 2) {
@@ -2202,16 +2344,13 @@ sub parse_clusterfw_option {
 sub resolve_alias {
     my ($clusterfw_conf, $fw_conf, $cidr) = @_;
 
-    if ($cidr !~ m/^\d/) {
-       my $alias = lc($cidr);
-       my $e = $fw_conf->{aliases}->{$alias} if $fw_conf;
-       $e = $clusterfw_conf->{aliases}->{$alias} if !$e && $clusterfw_conf;
-       return $e->{cidr} if $e;
-       
-       die "no such alias '$cidr'\n";
-    }
+    my $alias = lc($cidr);
+    my $e = $fw_conf->{aliases}->{$alias} if $fw_conf;
+    $e = $clusterfw_conf->{aliases}->{$alias} if !$e && $clusterfw_conf;
+
+    die "no such alias '$cidr'\n" if !$e;;
 
-    return $cidr;
+    return wantarray ? ($e->{cidr}, $e->{ipversion}) : $e->{cidr};
 }
 
 sub parse_ip_or_cidr {
@@ -2738,39 +2877,64 @@ sub generate_ipset {
 
     die "duplicate ipset chain '$name'\n" if defined($ipset_ruleset->{$name});
 
-    my $hashsize = scalar(@$options);
-    if ($hashsize <= 64) {
-       $hashsize = 64;
-    } else {
-       $hashsize = round_powerof2($hashsize);
-    }
-
-    $ipset_ruleset->{$name} = ["create $name hash:net family inet hashsize $hashsize maxelem $hashsize"];
+    $ipset_ruleset->{$name} = ["create $name list:set size 4"];
 
     # remove duplicates
     my $nethash = {};
     foreach my $entry (@$options) {
        next if $entry->{errors}; # skip entries with errors
        eval {
-           my $cidr = resolve_alias($clusterfw_conf, $fw_conf, $entry->{cidr});
-           $nethash->{$cidr} = { cidr => $cidr, nomatch => $entry->{nomatch} };
+           my ($cidr, $ipversion);
+           if ($entry->{cidr} =~ m/^${ip_alias_pattern}$/) {
+               ($cidr, $ipversion) = resolve_alias($clusterfw_conf, $fw_conf, $entry->{cidr});
+            } else {
+               ($cidr, $ipversion) = parse_ip_or_cidr($entry->{cidr});
+           }
+           #http://backreference.org/2013/03/01/ipv6-address-normalization/
+           if ($ipversion == 6) {
+               my $ipv6 = inet_pton(AF_INET6, lc($cidr));
+               $cidr = inet_ntop(AF_INET6, $ipv6);
+               $cidr =~ s|/128$||;
+           } else {
+               $cidr =~ s|/32$||;
+           }
+
+           $nethash->{$ipversion}->{$cidr} = { cidr => $cidr, nomatch => $entry->{nomatch} };
        };
        warn $@ if $@;
     }
 
-    foreach my $cidr (sort keys %$nethash) {
-       my $entry = $nethash->{$cidr};
+    foreach my $ipversion (sort keys %$nethash) {
+       my $data = $nethash->{$ipversion};
+       my $subname = "$name-v$ipversion";
 
-       my $cmd = "add $name $cidr";
-       if ($entry->{nomatch}) {
-           if ($feature_ipset_nomatch) {
-               push @{$ipset_ruleset->{$name}}, "$cmd nomatch";
+       my $hashsize = scalar(@$options);
+       if ($hashsize <= 64) {
+           $hashsize = 64;
+       } else {
+           $hashsize = round_powerof2($hashsize);
+       }
+
+       my $family = $ipversion == "6" ? "inet6" : "inet";
+
+       $ipset_ruleset->{$subname} = ["create $subname hash:net family $family hashsize $hashsize maxelem $hashsize"];
+
+       foreach my $cidr (sort keys %$data) {
+           my $entry = $data->{$cidr};
+
+           my $cmd = "add $subname $cidr";
+           if ($entry->{nomatch}) {
+               if ($feature_ipset_nomatch) {
+                   push @{$ipset_ruleset->{$subname}}, "$cmd nomatch";
+               } else {
+                   warn "ignore !$cidr - nomatch not supported by kernel\n";
+               }
            } else {
-               warn "ignore !$cidr - nomatch not supported by kernel\n";
+               push @{$ipset_ruleset->{$subname}}, $cmd;
            }
-       } else {
-           push @{$ipset_ruleset->{$name}}, $cmd;
        }
+
+       push @{$ipset_ruleset->{$name}}, "add $name $subname";
     }
 }
 
@@ -3171,6 +3335,11 @@ sub get_ipset_cmdlist {
                $cmdlist .= "$cmd\n";
            }
        }
+    }
+
+    foreach my $chain (sort keys %$ruleset) {
+       my $stat = $statushash->{$chain};
+       die "internal error" if !$stat;
 
        if ($stat->{action} eq 'update') {
            my $chain_swap = $chain."_swap";
@@ -3183,10 +3352,9 @@ sub get_ipset_cmdlist {
            $cmdlist .= "flush $chain_swap\n";
            $cmdlist .= "destroy $chain_swap\n";
        }
-
     }
 
-    foreach my $chain (keys %$statushash) {
+    foreach my $chain (sort keys %$statushash) {
        next if $statushash->{$chain}->{action} ne 'delete';
 
        $delete_cmdlist .= "flush $chain\n";