]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
verify_rule: correctly set ipversion for aliases
[pve-firewall.git] / src / PVE / Firewall.pm
index e2cf5f8be125fb2bc215379857b8766d10ce6327..f4b199bdb7281f8ab894cdfd26b194f295e8a2bc 100644 (file)
@@ -37,9 +37,13 @@ eval {
     $have_pve_manager = 1;
 };
 
+my $pve_fw_status_dir = "/var/lib/pve-firewall";
+
+mkdir $pve_fw_status_dir; # make sure this exists
+
 my $security_group_name_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
 my $ipset_name_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
-my $ip_alias_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
+our $ip_alias_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
 
 my $max_alias_name_length = 64;
 my $max_ipset_name_length = 64;
@@ -572,6 +576,92 @@ $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
+       # ipv6 addrtype does not work with kernel 2.6.32
+       #{ action => 'DROP', dsttype => 'MULTICAST' },
+        #{ action => 'DROP', dsttype => 'ANYCAST' },
+        { action => 'DROP', dest => 'ff00::/8' },
+        #{ 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",
+    ],
+};
+
 # iptables -p icmp -h
 my $icmp_type_names = {
     any => 1,
@@ -613,6 +703,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 = {};
@@ -699,6 +815,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;
@@ -841,6 +961,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};
            }
@@ -1095,6 +1217,16 @@ sub verify_rule {
        $errors->{$param} = $msg if !$errors->{$param};
     };
 
+    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;
+       }
+    };
+
     my $check_ipset_or_alias_property = sub {
        my ($name, $expected_ipversion) = @_;
 
@@ -1115,8 +1247,7 @@ sub verify_rule {
                my $e = $fw_conf->{aliases}->{$alias} if $fw_conf;
                $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
 
-               die "detected mixed ipv4/ipv6 adresses in rule\n"
-                   if $expected_ipversion && ($expected_ipversion != $e->{ipversion});
+               &$set_ip_version($e->{ipversion});
            }
        }
     };
@@ -1166,6 +1297,8 @@ sub verify_rule {
     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}) {
@@ -1182,10 +1315,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);
     }
@@ -1193,9 +1327,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);
@@ -1298,16 +1430,25 @@ my $rule_format = "%-15s %-30s %-30s %-15s %-15s %-15s\n";
 sub iptables_restore_cmdlist {
     my ($cmdlist) = @_;
 
-    run_command("/sbin/iptables-restore -n", input => $cmdlist);
+    run_command("/sbin/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");
 }
 
 sub ipset_restore_cmdlist {
     my ($cmdlist) = @_;
 
-    run_command("/usr/sbin/ipset restore", input => $cmdlist);
+    run_command("/usr/sbin/ipset restore", input => $cmdlist, errmsg => "ipset_restore_cmdlist");
 }
 
 sub iptables_get_chains {
+    my ($iptablescmd) = @_;
+
+    $iptablescmd = "iptables" if !$iptablescmd;
 
     my $res = {};
 
@@ -1362,7 +1503,7 @@ sub iptables_get_chains {
        }
     };
 
-    run_command("/sbin/iptables-save", outfunc => $parser);
+    run_command("/sbin/$iptablescmd-save", outfunc => $parser);
 
     return wantarray ? ($res, $hooks) : $res;
 }
@@ -1510,6 +1651,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) {
@@ -1674,7 +1819,7 @@ sub ruleset_chain_add_conn_filters {
 }
 
 sub ruleset_chain_add_input_filters {
-    my ($ruleset, $chain, $options, $cluster_conf, $loglevel) = @_;
+    my ($ruleset, $chain, $options, $cluster_conf, $ipversion, $loglevel) = @_;
 
     if ($cluster_conf->{ipset}->{blacklist}){
        if (!ruleset_chain_exist($ruleset, "PVEFW-blacklist")) {
@@ -1687,7 +1832,9 @@ sub ruleset_chain_add_input_filters {
     }
 
     if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
-       ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
+       if ($ipversion == 4) {
+           ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
+       }
     }
 
     if ($options->{tcpflags}) {
@@ -1918,7 +2065,7 @@ sub enable_host_firewall {
     ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
 
     ruleset_chain_add_conn_filters($ruleset, $chain, 'ACCEPT');
-    ruleset_chain_add_input_filters($ruleset, $chain, $options, $cluster_conf, $loglevel);
+    ruleset_chain_add_input_filters($ruleset, $chain, $options, $cluster_conf, $ipversion, $loglevel);
 
     # we use RETURN because we need to check also tap rules
     my $accept_action = 'RETURN';
@@ -2912,7 +3059,9 @@ sub compile {
     }
 
     my ($ruleset, $ipset_ruleset) = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4, $verbose);
-    return ($ruleset, $ipset_ruleset);
+    my ($rulesetv6) = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6, $verbose);
+
+    return ($ruleset, $ipset_ruleset, $rulesetv6);
 }
 
 sub compile_iptables_filter {
@@ -2953,7 +3102,7 @@ sub compile_iptables_filter {
     ruleset_addrule($ruleset, "PVEFW-INPUT", "-i venet0 -m set --match-set ${venet0_ipset_chain} src -j PVEFW-VENET-OUT");
 
     ruleset_create_chain($ruleset, "PVEFW-FWBR-IN");
-    ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $hostfw_options, $cluster_conf, $loglevel);
+    ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $hostfw_options, $cluster_conf, $ipversion, $loglevel);
 
     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-in fwln+ -j PVEFW-FWBR-IN");
 
@@ -2961,7 +3110,7 @@ sub compile_iptables_filter {
     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-out fwln+ -j PVEFW-FWBR-OUT");
 
     ruleset_create_chain($ruleset, "PVEFW-VENET-IN");
-    ruleset_chain_add_input_filters($ruleset, "PVEFW-VENET-IN", $hostfw_options, $cluster_conf, $loglevel);
+    ruleset_chain_add_input_filters($ruleset, "PVEFW-VENET-IN", $hostfw_options, $cluster_conf, $ipversion, $loglevel);
 
     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o venet0 -m set --match-set ${venet0_ipset_chain} dst -j PVEFW-VENET-IN");
 
@@ -3108,11 +3257,11 @@ sub print_sig_rule {
 }
 
 sub get_ruleset_cmdlist {
-    my ($ruleset, $verbose) = @_;
+    my ($ruleset, $verbose, $iptablescmd) = @_;
 
     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
 
-    my ($active_chains, $hooks) = iptables_get_chains();
+    my ($active_chains, $hooks) = iptables_get_chains($iptablescmd);
     my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, $verbose);
 
     # create missing chains first
@@ -3186,7 +3335,9 @@ sub get_ipset_cmdlist {
        }
     }
 
-    foreach my $chain (sort keys %$ruleset) {
+    # create -v4 and -v6 chains first
+    foreach my $chain (keys %$ruleset) {
+       next if $chain !~ m/-v[46]$/;
        my $stat = $statushash->{$chain};
        die "internal error" if !$stat;
 
@@ -3197,7 +3348,20 @@ sub get_ipset_cmdlist {
        }
     }
 
-    foreach my $chain (sort keys %$ruleset) {
+    # then create list chains which use above -v4 and -v6 chains
+    foreach my $chain (keys %$ruleset) {
+       next if $chain =~ m/-v[46]$/;
+       my $stat = $statushash->{$chain};
+       die "internal error" if !$stat;
+
+       if ($stat->{action} eq 'create') {
+           foreach my $cmd (@{$ruleset->{$chain}}) {
+               $cmdlist .= "$cmd\n";
+           }
+       }
+    }
+
+    foreach my $chain (keys %$ruleset) {
        my $stat = $statushash->{$chain};
        die "internal error" if !$stat;
 
@@ -3214,8 +3378,19 @@ sub get_ipset_cmdlist {
        }
     }
 
-    foreach my $chain (sort keys %$statushash) {
+    # remove unused list chains first
+    foreach my $chain (keys %$statushash) {
+       next if $statushash->{$chain}->{action} ne 'delete';
+       next if $chain !~ m/-v[46]$/;
+
+       $delete_cmdlist .= "flush $chain\n";
+       $delete_cmdlist .= "destroy $chain\n";
+    }
+
+    # the remove unused -v4 -v6 chains
+    foreach my $chain (keys %$statushash) {
        next if $statushash->{$chain}->{action} ne 'delete';
+       next if $chain =~ m/-v[46]$/;
 
        $delete_cmdlist .= "flush $chain\n";
        $delete_cmdlist .= "destroy $chain\n";
@@ -3227,7 +3402,7 @@ sub get_ipset_cmdlist {
 }
 
 sub apply_ruleset {
-    my ($ruleset, $hostfw_conf, $ipset_ruleset, $verbose) = @_;
+    my ($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $verbose) = @_;
 
     enable_bridge_firewall();
 
@@ -3235,6 +3410,7 @@ sub apply_ruleset {
        get_ipset_cmdlist($ipset_ruleset, undef, $verbose);
 
     my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset, $verbose);
+    my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");
 
     if ($verbose) {
        if ($ipset_changes) {
@@ -3247,12 +3423,31 @@ sub apply_ruleset {
            print "iptables changes:\n";
            print $cmdlist;
        }
+
+       if ($changesv6) {
+           print "ip6tables changes:\n";
+           print $cmdlistv6;
+       }
     }
 
+    my $tmpfile = "$pve_fw_status_dir/ipsetcmdlist1";
+    PVE::Tools::file_set_contents($tmpfile, $ipset_create_cmdlist || '');
+
     ipset_restore_cmdlist($ipset_create_cmdlist);
 
+    $tmpfile = "$pve_fw_status_dir/ip4cmdlist";
+    PVE::Tools::file_set_contents($tmpfile, $cmdlist || '');
+
     iptables_restore_cmdlist($cmdlist);
 
+    $tmpfile = "$pve_fw_status_dir/ip6cmdlist";
+    PVE::Tools::file_set_contents($tmpfile, $cmdlistv6 || '');
+
+    ip6tables_restore_cmdlist($cmdlistv6);
+
+    $tmpfile = "$pve_fw_status_dir/ipsetcmdlist2";
+    PVE::Tools::file_set_contents($tmpfile, $ipset_delete_cmdlist || '');
+
     ipset_restore_cmdlist($ipset_delete_cmdlist) if $ipset_delete_cmdlist;
 
     # test: re-read status and check if everything is up to date
@@ -3268,6 +3463,17 @@ sub apply_ruleset {
        }
     }
 
+    my $active_chainsv6 = iptables_get_chains("ip6tables");
+    my $statushashv6 = get_ruleset_status($rulesetv6, $active_chainsv6, \&iptables_chain_digest, 0);
+
+    foreach my $chain (sort keys %$rulesetv6) {
+       my $stat = $statushashv6->{$chain};
+       if ($stat->{action} ne 'exists') {
+           warn "unable to update chain '$chain'\n";
+           $errors = 1;
+       }
+    }
+
     die "unable to apply firewall changes\n" if $errors;
 
     update_nf_conntrack_max($hostfw_conf);
@@ -3312,7 +3518,16 @@ sub update_nf_conntrack_tcp_timeout_established {
 
 sub remove_pvefw_chains {
 
-    my ($chash, $hooks) = iptables_get_chains();
+    PVE::Firewall::remove_pvefw_chains_iptables("iptables");
+    PVE::Firewall::remove_pvefw_chains_iptables("ip6tables");
+    PVE::Firewall::remove_pvefw_chains_ipset();
+
+}
+
+sub remove_pvefw_chains_iptables {
+    my ($iptablescmd) = @_;
+
+    my ($chash, $hooks) = iptables_get_chains($iptablescmd);
     my $cmdlist = "*filter\n";
 
     foreach my $h (qw(INPUT OUTPUT FORWARD)) {
@@ -3330,18 +3545,33 @@ sub remove_pvefw_chains {
     }
     $cmdlist .= "COMMIT\n";
 
-    iptables_restore_cmdlist($cmdlist);
+    if($iptablescmd eq "ip6tables") {
+       ip6tables_restore_cmdlist($cmdlist);
+    } else {
+       iptables_restore_cmdlist($cmdlist);
+    }
+}
+
+sub remove_pvefw_chains_ipset {
 
     my $ipset_chains = ipset_get_chains();
 
-    $cmdlist = "";
+    my $sub_cmdlist = "";
+    my $cmdlist = "";
  
     foreach my $chain (keys %$ipset_chains) {
-       $cmdlist .= "flush $chain\n";
-       $cmdlist .= "destroy $chain\n";
+       if ($chain =~ m/^PVEFW-\S+\-(v4|v6)$/) {
+          $sub_cmdlist .= "flush $chain\n";
+          $sub_cmdlist .= "destroy $chain\n";
+       }else{
+          $cmdlist .= "flush $chain\n";
+          $cmdlist .= "destroy $chain\n";
+        }
     }
 
-    ipset_restore_cmdlist($cmdlist) if $cmdlist; 
+    $cmdlist .= $sub_cmdlist;
+
+    ipset_restore_cmdlist($cmdlist) if $cmdlist;
 }
 
 sub init {
@@ -3367,9 +3597,9 @@ sub update {
 
        my $hostfw_conf = load_hostfw_conf();
 
-       my ($ruleset, $ipset_ruleset) = compile($cluster_conf, $hostfw_conf);
+       my ($ruleset, $ipset_ruleset, $rulesetv6) = compile($cluster_conf, $hostfw_conf);
 
-       apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset);
+       apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6);
     };
 
     run_locked($code);