]> git.proxmox.com Git - pve-firewall.git/blobdiff - PVE/Firewall.pm
implement log_level_in and log_level_out options
[pve-firewall.git] / PVE / Firewall.pm
index 081b35098b3b7c4947391962fb9ff472f3d9f248..d18809a2ef8a417c276b87ccb5d27e60bd2894ef 100644 (file)
@@ -6,6 +6,7 @@ use Data::Dumper;
 use Digest::SHA;
 use PVE::Tools;
 use PVE::QemuServer;
+use File::Basename;
 use File::Path;
 use IO::File;
 use Net::IP;
@@ -14,6 +15,20 @@ use PVE::Tools qw(run_command lock_file);
 use Data::Dumper;
 
 my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
+my $pve_fw_status_filename = "/var/lib/pve-firewall/pvefw.status";
+
+my $default_log_level = 'info';
+
+my $log_level_hash = {
+    debug => 7,
+    info => 6,
+    notice => 5,
+    warning => 4,
+    err => 3,
+    crit => 2,
+    alert => 1,
+    emerg => 0,
+};
 
 # imported/converted from: /usr/share/shorewall/macro.*
 my $pve_fw_macros = {
@@ -224,7 +239,7 @@ my $pve_fw_macros = {
        { action => 'PARAM', proto => 'tcp', dport => '1723' },
     ],
     'Ping' => [
-       { action => 'PARAM', proto => 'icmp', dport => '8' },
+       { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
     ],
     'PostgreSQL' => [
        { action => 'PARAM', proto => 'tcp', dport => '5432' },
@@ -322,7 +337,7 @@ my $pve_fw_macros = {
     ],
     'Trcrt' => [
        { action => 'PARAM', proto => 'udp', dport => '33434:33524' },
-       { action => 'PARAM', proto => 'icmp', dport => '8' },
+       { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
     ],
     'VNC' => [
        { action => 'PARAM', proto => 'tcp', dport => '5900:5909' },
@@ -417,6 +432,22 @@ my $pve_std_chains = {
        # 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' => [
+       # 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
@@ -686,7 +717,9 @@ sub iptables_rule_exist {
 }
 
 sub ruleset_generate_rule {
-    my ($ruleset, $chain, $rule, $goto) = @_;
+    my ($ruleset, $chain, $rule, $actions, $goto) = @_;
+
+    return if $rule->{disable};
 
     my $cmd = '';
 
@@ -694,41 +727,56 @@ sub ruleset_generate_rule {
     $cmd .= " -s $rule->{source}" if $rule->{source};
     $cmd .= " -m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
     $cmd .= " -d $rule->{dest}" if $rule->{dest};
-    $cmd .= " -p $rule->{proto}" if $rule->{proto};
 
-    if (($rule->{nbdport} && $rule->{nbdport} > 1) ||
-       ($rule->{nbsport} && $rule->{nbsport} > 1)) {
-       $cmd .= " --match multiport" 
-    }
+    if ($rule->{proto}) {
+       $cmd .= " -p $rule->{proto}";
 
-    if ($rule->{dport}) {
-       if ($rule->{proto} && $rule->{proto} eq 'icmp') {
-           # Note: we use dport to store --icmp-type
-           die "unknown icmp-type\n" if !$icmp_type_names->{$rule->{dport}};
-           $cmd .= " -m icmp --icmp-type $rule->{dport}";
-       } else {
-           if ($rule->{nbdport} && $rule->{nbdport} > 1) {
-               $cmd .= " --dports $rule->{dport}";
+       my $multiport = 0;
+       $multiport++ if $rule->{nbdport} && ($rule->{nbdport} > 1);
+       $multiport++ if $rule->{nbsport} && ($rule->{nbsport} > 1);
+
+       $cmd .= " --match multiport" if $multiport;
+
+       die "multiport: option '--sports' cannot be used together with '--dports'\n" 
+           if ($multiport == 2) && ($rule->{dport} ne $rule->{sport});
+
+       if ($rule->{dport}) {
+           if ($rule->{proto} && $rule->{proto} eq 'icmp') {
+               # Note: we use dport to store --icmp-type
+               die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
+               $cmd .= " -m icmp --icmp-type $rule->{dport}";
            } else {
-               $cmd .= " --dport $rule->{dport}";
+               if ($rule->{nbdport} && $rule->{nbdport} > 1) {
+                   if ($multiport == 2) {
+                       $cmd .= " --ports $rule->{dport}";
+                   } else {
+                       $cmd .= " --dports $rule->{dport}";
+                   }
+               } else {
+                   $cmd .= " --dport $rule->{dport}";
+               }
            }
        }
-    }
 
-    if ($rule->{sport}) {
-       if ($rule->{nbsport} && $rule->{nbsport} > 1) {
-           $cmd .= " --sports $rule->{sport}";
-       } else {
-           $cmd .= " --sport $rule->{sport}";
+       if ($rule->{sport}) {
+           if ($rule->{nbsport} && $rule->{nbsport} > 1) {
+               $cmd .= " --sports $rule->{sport}" if $multiport != 2;
+           } else {
+               $cmd .= " --sport $rule->{sport}";
+           }
        }
+    } elsif ($rule->{dport} || $rule->{sport}) {
+       warn "ignoring destination port '$rule->{dport}' - no protocol specified\n" if $rule->{dport};
+       warn "ignoring source port '$rule->{sport}' - no protocol specified\n" if $rule->{sport};
     }
 
     $cmd .= " -m addrtype --dst-type $rule->{dsttype}" if $rule->{dsttype};
 
     if (my $action = $rule->{action}) {
+       $action = $actions->{$action} if defined($actions->{$action}); 
        $goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
        $cmd .= $goto ? " -g $action" : " -j $action";
-    };
+    }
 
     ruleset_addrule($ruleset, $chain, $cmd) if $cmd;
 }
@@ -790,23 +838,41 @@ sub generate_bridge_chains {
        ruleset_create_chain($ruleset, "$bridge-IN");
        ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-bridged --physdev-is-out -j $bridge-IN");
        ruleset_addrule($ruleset, "$bridge-FW", "-m mark --mark 1 -j ACCEPT");
+       # accept traffic to unmanaged bridge ports
+       ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-bridged --physdev-is-out -j ACCEPT ");
     }
 }
 
 sub generate_tap_rules_direction {
     my ($ruleset, $group_rules, $iface, $netid, $macaddr, $vmfw_conf, $bridge, $direction) = @_;
 
-    my $rules = $vmfw_conf->{lc($direction)};
+    my $lc_direction = lc($direction);
+    my $rules = $vmfw_conf->{$lc_direction};
+
     my $options = $vmfw_conf->{options};
+    my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
 
     my $tapchain = "$iface-$direction";
 
     ruleset_create_chain($ruleset, $tapchain);
 
+    if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
+       ruleset_addrule($ruleset, $tapchain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
+    }
+
+    if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
+       ruleset_addrule($ruleset, $tapchain, "-p udp -m udp --dport 67:68 -j ACCEPT");
+    }
+
+    if ($options->{tcpflags}) {
+       ruleset_addrule($ruleset, $tapchain, "-p tcp -j PVEFW-tcpflags");
+    }
+
     ruleset_addrule($ruleset, $tapchain, "-m conntrack --ctstate INVALID -j DROP");
     ruleset_addrule($ruleset, $tapchain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
 
-    if ($direction eq 'OUT' && defined($macaddr)) {
+    if ($direction eq 'OUT' && defined($macaddr) && 
+       !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
        ruleset_addrule($ruleset, $tapchain, "-m mac ! --mac-source $macaddr -j DROP");
     }
 
@@ -824,8 +890,12 @@ sub generate_tap_rules_direction {
                ruleset_addrule($ruleset, $tapchain, "-m mark --mark 1 -j RETURN")
                    if $direction eq 'OUT';
            } else {
-               $rule->{action} = "PVEFW-SET-ACCEPT-MARK" if $rule->{action} eq 'ACCEPT' && $direction eq 'OUT';
-               ruleset_generate_rule($ruleset, $tapchain, $rule);
+               if ($direction eq 'OUT') {
+                   ruleset_generate_rule($ruleset, $tapchain, $rule, 
+                                         { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" });
+               } else {
+                   ruleset_generate_rule($ruleset, $tapchain, $rule, { REJECT => "PVEFW-reject" });
+               }
            }
        }
     }
@@ -846,11 +916,20 @@ sub generate_tap_rules_direction {
            ruleset_addrule($ruleset, $tapchain, "-j ACCEPT");
        }
     } elsif ($policy eq 'DROP') {
-       ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-dropped: \" --log-level 4");
+
+       ruleset_addrule($ruleset, $tapchain, "-j PVEFW-Drop");
+
+       ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-dropped: \" --log-level $loglevel")
+           if defined($loglevel);
+
        ruleset_addrule($ruleset, $tapchain, "-j DROP");
     } elsif ($policy eq 'REJECT') {
-       ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-reject: \" --log-level 4");
-       ruleset_addrule($ruleset, $tapchain, "-j REJECT");
+       ruleset_addrule($ruleset, $tapchain, "-j PVEFW-Reject");
+
+       ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-reject: \" --log-level $loglevel")
+           if defined($loglevel);
+
+       ruleset_addrule($ruleset, $tapchain, "-g PVEFW-reject");
     } else {
        # should not happen
        die "internal error: unknown policy '$policy'";
@@ -873,10 +952,14 @@ sub enablehostfw {
 
     # fixme: allow security groups
 
+    my $options = $rules->{options};
+
     # host inbound firewall
     my $chain = "PVEFW-HOST-IN";
     ruleset_create_chain($ruleset, $chain);
 
+    my $loglevel = get_option_log_level($options, "log_level_in");
+
     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
     ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
@@ -887,18 +970,21 @@ sub enablehostfw {
     if ($rules->{in}) {
         foreach my $rule (@{$rules->{in}}) {
             # we use RETURN because we need to check also tap rules
-            $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
-            ruleset_generate_rule($ruleset, $chain, $rule);
+            ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => 'RETURN', REJECT => "PVEFW-reject" });
         }
     }
 
-    ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level 4");
+    ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level $loglevel")
+       if defined($loglevel);
+
     ruleset_addrule($ruleset, $chain, "-j DROP");
 
     # host outbound firewall
     $chain = "PVEFW-HOST-OUT";
     ruleset_create_chain($ruleset, $chain);
 
+    $loglevel = get_option_log_level($options, "log_level_out");
+
     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
     ruleset_addrule($ruleset, $chain, "-o lo -j ACCEPT");
@@ -909,12 +995,13 @@ sub enablehostfw {
     if ($rules->{out}) {
         foreach my $rule (@{$rules->{out}}) {
             # we use RETURN because we need to check also tap rules
-            $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
-            ruleset_generate_rule($ruleset, $chain, $rule);
+            ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => 'RETURN', REJECT => "PVEFW-reject" });
         }
     }
 
-    ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level 4");
+    ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level $loglevel")
+       if defined($loglevel);
+
     ruleset_addrule($ruleset, $chain, "-j DROP");
 
     ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
@@ -934,7 +1021,7 @@ sub generate_group_rules {
 
     if ($rules->{in}) {
         foreach my $rule (@{$rules->{in}}) {
-           ruleset_generate_rule($ruleset, $chain, $rule);
+           ruleset_generate_rule($ruleset, $chain, $rule, { REJECT => "PVEFW-reject" });
         }
     }
 
@@ -945,11 +1032,10 @@ sub generate_group_rules {
 
     if ($rules->{out}) {
         foreach my $rule (@{$rules->{out}}) {
-            # we go the PVEFW-SET-ACCEPT-MARK Instead of ACCEPT) because we need to
-           # check also other tap rules (and group rules can be set on any bridge,
-           # so we can't go to VMBRXX-IN)
-            $rule->{action} = 'PVEFW-SET-ACCEPT-MARK' if $rule->{action} eq 'ACCEPT';
-            ruleset_generate_rule($ruleset, $chain, $rule);
+            # we use PVEFW-SET-ACCEPT-MARK (Instead of ACCEPT) because we need to
+           # check also other tap rules later
+            ruleset_generate_rule($ruleset, $chain, $rule, 
+                                 { ACCEPT => 'PVEFW-SET-ACCEPT-MARK', REJECT => "PVEFW-reject" });
         }
     }
 }
@@ -968,7 +1054,11 @@ sub parse_fw_rule {
 
     my ($action, $iface, $source, $dest, $proto, $dport, $sport);
 
-    $line =~ s/#.*$//;
+    # we can add single line comments to the end of the rule
+    my $comment = $1 if $line =~ s/#\s*(.*?)\s*$//;
+
+    # we can disable a rule when prefixed with '|'
+    my $disable = 1 if  $line =~ s/^\|//;
 
     my @data = split(/\s+/, $line);
     my $expected_elements = $need_iface ? 7 : 6;
@@ -1027,6 +1117,8 @@ sub parse_fw_rule {
     my $rules = [];
 
     my $param = {
+       disable => $disable,
+       comment => $comment,
        action => $action,
        iface => $iface,
        source => $source,
@@ -1041,19 +1133,33 @@ sub parse_fw_rule {
     if ($macro) {
        foreach my $templ (@$macro) {
            my $rule = {};
+           my $param_used = {};
            foreach my $k (keys %$templ) {
                my $v = $templ->{$k};
                if ($v eq 'PARAM') {
                    $v = $param->{$k};
+                   $param_used->{$k} = 1;
                } elsif ($v eq 'DEST') {
                    $v = $param->{dest};
+                   $param_used->{dest} = 1;
                } elsif ($v eq 'SOURCE') {
                    $v = $param->{source};
+                   $param_used->{source} = 1;
                }
 
                die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
                $rule->{$k} = $v;
            }
+           foreach my $k (keys %$param) {
+               next if !defined($param->{$k});
+               next if $param_used->{$k};
+               if (defined($rule->{$k})) {
+                   die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
+                       if $rule->{$k} ne $param->{$k};
+               } else {
+                   $rule->{$k} = $param->{$k};
+               }
+           }
            push @$rules, $rule;
        }
     } else {
@@ -1070,18 +1176,47 @@ sub parse_fw_rule {
     return $rules;
 }
 
-sub parse_fw_option {
+sub parse_vmfw_option {
     my ($line) = @_;
 
     my ($opt, $value);
 
-    if ($line =~ m/^enable:\s*(0|1)\s*$/i) {
-       $opt = 'enable';
-       $value = int($1);
+    my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
+
+    if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
+       $opt = lc($1);
+       $value = int($2);
+    } elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
+       $opt = lc($1);
+       $value = $2 ? lc($3) : '';
     } elsif ($line =~ m/^(policy-(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
        $opt = lc($1);
        $value = uc($3);
-     } else {
+    } else {
+       chomp $line;
+       die "can't parse option '$line'\n"
+    }
+
+    return ($opt, $value);
+}
+
+sub parse_hostfw_option {
+    my ($line) = @_;
+
+    my ($opt, $value);
+
+    my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
+
+    if ($line =~ m/^(enable|dhcp|nosmurfs|tcpflags):\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/^(policy-(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
+       $opt = lc($1);
+       $value = uc($3);
+    } else {
        chomp $line;
        die "can't parse option '$line'\n"
     }
@@ -1117,7 +1252,7 @@ sub parse_vm_fw_rules {
 
        if ($section eq 'options') {
            eval {
-               my ($opt, $value) = parse_fw_option($line);
+               my ($opt, $value) = parse_vmfw_option($line);
                $res->{options}->{$opt} = $value;
            };
            warn "$prefix: $@" if $@;
@@ -1140,7 +1275,7 @@ sub parse_vm_fw_rules {
 sub parse_host_fw_rules {
     my ($filename, $fh) = @_;
 
-    my $res = { in => [], out => [] };
+    my $res = { in => [], out => [], options => {}};
 
     my $section;
 
@@ -1151,8 +1286,9 @@ sub parse_host_fw_rules {
        my $linenr = $fh->input_line_number();
        my $prefix = "$filename (line $linenr)";
 
-       if ($line =~ m/^\[(in|out)\]\s*$/i) {
+       if ($line =~ m/^\[(\S+)\]\s*$/i) {
            $section = lc($1);
+           warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
            next;
        }
        if (!$section) {
@@ -1160,6 +1296,18 @@ sub parse_host_fw_rules {
            next;
        }
 
+       next if !$res->{$section}; # skip undefined section
+
+       if ($section eq 'options') {
+           eval {
+               print "PARSE:$line\n";
+               my ($opt, $value) = parse_hostfw_option($line);
+               $res->{options}->{$opt} = $value;
+           };
+           warn "$prefix: $@" if $@;
+           next;
+       }
+
        my $rules;
        eval { $rules = parse_fw_rule($line, 1, 1); };
        if (my $err = $@) {
@@ -1257,8 +1405,48 @@ sub read_vm_firewall_rules {
     return $rules;
 }
 
+sub get_option_log_level {
+    my ($options, $k) = @_;
+
+    my $v = $options->{$k};
+    $v = $default_log_level if !defined($v);
+
+    return undef if $v eq '' || $v eq 'nolog';
+
+    $v = $log_level_hash->{$v} if defined($log_level_hash->{$v});
+
+    return $v if ($v >= 0) && ($v <= 7);
+
+    warn "unknown log level ($k = '$v')\n";
+
+    return undef;
+}
+
 sub generate_std_chains {
-    my ($ruleset) = @_;
+    my ($ruleset, $options) = @_;
+    
+    my $loglevel = get_option_log_level($options, 'smurf_log_level');
+
+    # same as shorewall smurflog.
+    if (defined($loglevel)) {
+       $pve_std_chains-> {'PVEFW-smurflog'} = [
+           "-j LOG --log-prefix \"smurfs-dropped\" --log-level $loglevel",
+           "-j DROP",
+           ];
+    } else {
+       $pve_std_chains-> {'PVEFW-smurflog'} = [ "-j DROP" ];
+    }
+
+    # same as shorewall logflags action.
+    $loglevel = get_option_log_level($options, 'tcp_flags_log_level');
+    if (defined($loglevel)) {
+       $pve_std_chains-> {'PVEFW-logflags'} = [
+           "-j LOG --log-prefix \"logflags-dropped:\" --log-level $loglevel --log-ip-options",
+           "-j DROP",
+           ];
+    } else {
+       $pve_std_chains-> {'PVEFW-logflags'} = [ "-j DROP" ];
+    }
 
     foreach my $chain (keys %$pve_std_chains) {
        ruleset_create_chain($ruleset, $chain);
@@ -1272,6 +1460,30 @@ sub generate_std_chains {
     }
 }
 
+sub save_pvefw_status {
+    my ($status) = @_;
+
+    die "unknown status '$status' - internal error"
+       if $status !~ m/^(stopped|active)$/;
+
+    mkdir dirname($pve_fw_status_filename);
+    PVE::Tools::file_set_contents($pve_fw_status_filename, $status);
+}
+
+sub read_pvefw_status {
+
+    my $status = 'unknown';
+
+    return 'stopped' if ! -f $pve_fw_status_filename;
+
+    eval {
+       $status = PVE::Tools::file_get_contents($pve_fw_status_filename);
+    };
+    warn $@ if $@;
+
+    return $status;
+}
+
 sub compile {
     my $vmdata = read_local_vm_config();
     my $rules = read_vm_firewall_rules($vmdata);
@@ -1290,17 +1502,20 @@ sub compile {
     ruleset_create_chain($ruleset, "PVEFW-OUTPUT");
     ruleset_create_chain($ruleset, "PVEFW-FORWARD");
 
-    generate_std_chains($ruleset);
+    my $host_options = {};
+    my $host_rules;
 
-    my $enable_hostfw = 0;
     $filename = "/etc/pve/local/host.fw";
     if (my $fh = IO::File->new($filename, O_RDONLY)) {
-       my $host_rules = parse_host_fw_rules($filename, $fh);
+       $host_rules = parse_host_fw_rules($filename, $fh);
+       $host_options = $host_rules->{options};
+    }
 
-       $enable_hostfw = 1;
+    generate_std_chains($ruleset, $host_options);
 
-       enablehostfw($ruleset, $host_rules, $group_rules);
-    }
+    my $hotsfw_enable = $host_rules && !(defined($host_options->{enable}) && ($host_options->{enable} == 0));
+
+    enablehostfw($ruleset, $host_rules, $group_rules) if $hotsfw_enable;
 
     # generate firewall rules for QEMU VMs
     foreach my $vmid (keys %{$vmdata->{qemu}}) {
@@ -1329,7 +1544,7 @@ sub compile {
        }
     }
 
-    if ($enable_hostfw) {
+    if ($hotsfw_enable) {
        # allow traffic from lo (ourself)
        ruleset_addrule($ruleset, "PVEFW-INPUT", "-i lo -j ACCEPT");
     }
@@ -1393,11 +1608,9 @@ sub print_sig_rule {
     return "-A $chain -m comment --comment \"PVESIG:$sig\"\n";
 }
 
-sub apply_ruleset {
+sub get_rulset_cmdlist {
     my ($ruleset, $verbose) = @_;
 
-    enable_bridge_firewall();
-
     my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
 
     my $statushash = get_ruleset_status($ruleset, $verbose);
@@ -1458,12 +1671,22 @@ sub apply_ruleset {
 
     $cmdlist .= "COMMIT\n";
 
+    return $cmdlist;
+}
+
+sub apply_ruleset {
+    my ($ruleset, $verbose) = @_;
+
+    enable_bridge_firewall();
+
+    my $cmdlist = get_rulset_cmdlist($ruleset, $verbose);
+
     print $cmdlist if $verbose;
 
     iptables_restore_cmdlist($cmdlist);
 
     # test: re-read status and check if everything is up to date
-    $statushash = get_ruleset_status($ruleset);
+    my $statushash = get_ruleset_status($ruleset);
 
     my $errors;
     foreach my $chain (sort keys %$ruleset) {
@@ -1477,4 +1700,26 @@ sub apply_ruleset {
     die "unable to apply firewall changes\n" if $errors;
 }
 
+sub update {
+    my ($start, $verbose) = @_;
+
+    my $code = sub {
+       my $status = read_pvefw_status();
+
+       my $ruleset = PVE::Firewall::compile();
+
+       if ($start || $status eq 'active') {
+
+           save_pvefw_status('active') if ($status ne 'active');
+
+           PVE::Firewall::apply_ruleset($ruleset, $verbose);
+       } else {
+           print "Firewall not active (status = $status)\n" if $verbose;
+       }
+    };
+
+    run_locked($code);
+}
+
+
 1;