]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
add aliases feature
[pve-firewall.git] / src / PVE / Firewall.pm
index 9054648a40ff03c4c1f0c28a1be96960a8332b83..7ef38fa514be9b0c35cf2ba3425845ef95472649 100644 (file)
@@ -7,10 +7,10 @@ use Data::Dumper;
 use Digest::SHA;
 use PVE::INotify;
 use PVE::Exception qw(raise raise_param_exc);
-use PVE::JSONSchema qw(get_standard_option);
+use PVE::JSONSchema qw(register_standard_option get_standard_option);
 use PVE::Cluster;
 use PVE::ProcFSTools;
-use PVE::Tools;
+use PVE::Tools qw($IPV4RE);
 use File::Basename;
 use File::Path;
 use IO::File;
@@ -35,6 +35,44 @@ eval {
     $have_pve_manager = 1;
 };
 
+PVE::JSONSchema::register_format('IPv4orCIDR', \&pve_verify_ipv4_or_cidr);
+sub pve_verify_ipv4_or_cidr {
+    my ($cidr, $noerr) = @_;
+
+    if ($cidr =~ m!^(?:$IPV4RE)(/(\d+))?$!) {
+       return $cidr if Net::IP->new($cidr); 
+       return undef if $noerr;
+       die Net::IP::Error() . "\n";
+    }
+    return undef if $noerr;
+    die "value does not look like a valid IP address or CIDR network\n";
+}
+
+PVE::JSONSchema::register_standard_option('ipset-name', {
+    description => "IP set name.",
+    type => 'string',
+    pattern => '[A-Za-z][A-Za-z0-9\-\_]+',
+    minLength => 2,
+    maxLength => 20,                     
+});
+
+PVE::JSONSchema::register_standard_option('pve-fw-loglevel' => {
+    description => "Log level.",
+    type => 'string', 
+    enum => ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug', 'nolog'],
+    optional => 1,
+});
+
+my $security_group_pattern = '[A-Za-z][A-Za-z0-9\-\_]+';
+
+PVE::JSONSchema::register_standard_option('pve-security-group-name', {
+    description => "Security Group name.",
+    type => 'string',
+    pattern => $security_group_pattern,
+    minLength => 2,
+    maxLength => 20,                             
+});
+
 my $feature_ipset_nomatch = 0;
 eval  {
     my (undef, undef, $release) = POSIX::uname();
@@ -673,7 +711,8 @@ sub get_etc_protocols {
 sub parse_address_list {
     my ($str) = @_;
 
-    return if $str !~ m/^(\+)(\S+)$/; # ipset ref
+    return if $str =~ m/^(\+)(\S+)$/; # ipset ref
+    return if $str =~ m/^${security_group_pattern}$/; # aliases
 
     my $count = 0;
     my $iprange = 0;
@@ -753,6 +792,52 @@ sub pve_fw_verify_protocol_spec {
 
 # helper function for API
 
+sub copy_opject_with_digest {
+    my ($object) = @_;
+
+    my $sha = Digest::SHA->new('sha1');
+
+    my $res = {};
+    foreach my $k (sort keys %$object) {
+       my $v = $object->{$k};
+       next if !defined($v);
+       $res->{$k} = $v;
+       $sha->add($k, ':', $v, "\n");
+    }
+
+    my $digest = $sha->b64digest;
+
+    $res->{digest} = $digest;
+
+    return wantarray ? ($res, $digest) : $res;
+}
+
+sub copy_list_with_digest {
+    my ($list) = @_;
+
+    my $sha = Digest::SHA->new('sha1');
+
+    my $res = [];
+    foreach my $entry (@$list) {
+       my $data = {};
+       foreach my $k (sort keys %$entry) {
+           my $v = $entry->{$k};
+           next if !defined($v);
+           $data->{$k} = $v;
+           $sha->add($k, ':', $v, "\n");
+       }
+       push @$res, $data;
+    }
+
+    my $digest = $sha->b64digest;
+
+    foreach my $entry (@$res) {
+       $entry->{digest} = $digest;
+    }
+
+    return wantarray ? ($res, $digest) : $res;
+}
+
 my $rule_properties = {
     pos => {
        description => "Update rule at position <pos>.",
@@ -760,21 +845,19 @@ my $rule_properties = {
        minimum => 0,
        optional => 1,
     },
-    digest => {
-       type => 'string',
-       optional => 1,
-       maxLength => 27,
-       minLength => 27,
-    },
+    digest => get_standard_option('pve-config-digest'),
     type => {
        type => 'string',
        optional => 1,
        enum => ['in', 'out', 'group'],
     },
     action => {
+       description => "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name.",
        type => 'string',
        optional => 1,
-       enum => ['ACCEPT', 'DROP', 'REJECT'],
+       pattern => $security_group_pattern,
+       maxLength => 20,
+       minLength => 2,
     },
     macro => {
        type => 'string',
@@ -812,23 +895,6 @@ my $rule_properties = {
     },
 };
 
-sub cleanup_fw_rule {
-    my ($rule, $digest, $pos) = @_;
-
-    my $r = {};
-
-    foreach my $k (keys %$rule) {
-       next if !$rule_properties->{$k};
-       my $v = $rule->{$k};
-       next if !defined($v);
-       $r->{$k} = $v;
-       $r->{digest} = $digest;
-       $r->{pos} = $pos;
-    }
-
-    return $r;
-}
-
 sub add_rule_properties {
     my ($properties) = @_;
 
@@ -921,7 +987,7 @@ sub verify_rule {
        raise_param_exc({ type => "security groups not allowed"}) 
            if !$allow_groups;
        raise_param_exc({ action => "invalid characters in security group name"}) 
-           if $rule->{action} !~ m/^[A-Za-z0-9_\-]+$/;
+           if $rule->{action} !~ m/^${security_group_pattern}$/;
     } else {
        raise_param_exc({ type => "unknown rule type '$type'"});
     }
@@ -1143,6 +1209,10 @@ sub ruleset_generate_cmdstr {
            die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
            push @cmd, "-m set --match-set PVEFW-$2 src";
 
+       } elsif ($source =~ m/^${security_group_pattern}$/){
+           die "no such alias $source" if !$cluster_conf->{aliases}->{$source};
+           push @cmd, "-s $cluster_conf->{aliases}->{$source}";
+
         } elsif ($source =~ m/\-/){
            push @cmd, "-m iprange --src-range $source";
 
@@ -1156,11 +1226,15 @@ sub ruleset_generate_cmdstr {
            die "no such ipset $2" if !$cluster_conf->{ipset}->{$2};
            push @cmd, "-m set --match-set PVEFW-$2 dst";
 
+       } elsif ($dest =~ m/^${security_group_pattern}$/){
+           die "no such alias $dest" if !$cluster_conf->{aliases}->{$dest};
+           push @cmd, "-d $cluster_conf->{aliases}->{$dest}";
+
         } elsif ($dest =~ m/^(\d+)\.(\d+).(\d+).(\d+)\-(\d+)\.(\d+).(\d+).(\d+)$/){
            push @cmd, "-m iprange --dst-range $dest";
 
        } else {
-           push @cmd, "-s $dest";
+           push @cmd, "-d $dest";
         }
     }
 
@@ -1360,12 +1434,12 @@ sub ruleset_add_chain_policy {
 }
 
 sub ruleset_create_vm_chain {
-    my ($ruleset, $chain, $options, $macaddr, $direction) = @_;
+    my ($ruleset, $chain, $options, $host_options, $macaddr, $direction) = @_;
 
     ruleset_create_chain($ruleset, $chain);
     my $accept = generate_nfqueue($options);
 
-    if (!(defined($options->{nosmurfs}) && $options->{nosmurfs} == 0)) {
+    if (!(defined($host_options->{nosmurfs}) && $host_options->{nosmurfs} == 0)) {
        ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
     }
 
@@ -1379,7 +1453,7 @@ sub ruleset_create_vm_chain {
        }
     }
 
-    if ($options->{tcpflags}) {
+    if ($host_options->{tcpflags}) {
        ruleset_addrule($ruleset, $chain, "-p tcp -j PVEFW-tcpflags");
     }
 
@@ -1445,7 +1519,7 @@ sub generate_nfqueue {
                $action .= " --queue-num $1";
            }
        }
-       $action .= " --queue-bypass";
+       $action .= " --queue-bypass" if $feature_ipset_nomatch; #need kernel 3.10
     }else{
        $action = "ACCEPT";
     }
@@ -1473,7 +1547,7 @@ sub ruleset_generate_vm_ipsrules {
 }
 
 sub generate_venet_rules_direction {
-    my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
+    my ($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
 
     parse_address_list($ip); # make sure we have a valid $ip list
 
@@ -1482,11 +1556,12 @@ sub generate_venet_rules_direction {
     my $rules = $vmfw_conf->{rules};
 
     my $options = $vmfw_conf->{options};
+    my $hostfw_options = $vmfw_conf->{options};
     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
 
     my $chain = "venet0-$vmid-$direction";
 
-    ruleset_create_vm_chain($ruleset, $chain, $options, undef, $direction);
+    ruleset_create_vm_chain($ruleset, $chain, $options, $hostfw_options, undef, $direction);
 
     ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $chain, 'venet', $direction);
 
@@ -1528,18 +1603,19 @@ sub generate_venet_rules_direction {
 }
 
 sub generate_tap_rules_direction {
-    my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
+    my ($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $bridge, $direction) = @_;
 
     my $lc_direction = lc($direction);
 
     my $rules = $vmfw_conf->{rules};
 
     my $options = $vmfw_conf->{options};
+    my $hostfw_options = $hostfw_conf->{options};
     my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
 
     my $tapchain = "$iface-$direction";
 
-    ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $direction);
+    ruleset_create_vm_chain($ruleset, $tapchain, $options, $hostfw_options, $macaddr, $direction);
 
     ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $tapchain, $netid, $direction, $options);
 
@@ -1571,10 +1647,10 @@ sub generate_tap_rules_direction {
 sub enable_host_firewall {
     my ($ruleset, $hostfw_conf, $cluster_conf) = @_;
 
-    # fixme: allow security groups
-
     my $options = $hostfw_conf->{options};
+    my $cluster_options = $cluster_conf->{options};
     my $rules = $hostfw_conf->{rules};
+    my $cluster_rules = $cluster_conf->{rules};
 
     # host inbound firewall
     my $chain = "PVEFW-HOST-IN";
@@ -1600,13 +1676,14 @@ sub enable_host_firewall {
     # we use RETURN because we need to check also tap rules
     my $accept_action = 'RETURN';
 
-    foreach my $rule (@$rules) {
+    # add host rules first, so that cluster wide rules can be overwritten
+    foreach my $rule (@$rules, @$cluster_rules) {
        next if $rule->{type} ne 'in';
        ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf);
     }
 
     # implement input policy
-    my $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
+    my $policy = $cluster_options->{policy_in} || 'DROP'; # allow nothing by default
     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
 
     # host outbound firewall
@@ -1625,13 +1702,14 @@ sub enable_host_firewall {
     # we use RETURN because we may want to check other thigs later
     $accept_action = 'RETURN';
 
-    foreach my $rule (@$rules) {
+    # add host rules first, so that cluster wide rules can be overwritten
+    foreach my $rule (@$rules, @$cluster_rules) {
        next if $rule->{type} ne 'out';
        ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" }, undef, $cluster_conf);
     }
 
     # implement output policy
-    $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
+    $policy = $cluster_options->{policy_out} || 'ACCEPT'; # allow everything by default
     ruleset_add_chain_policy($ruleset, $chain, 0, $policy, $loglevel, $accept_action);
 
     ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
@@ -1719,7 +1797,7 @@ sub parse_fw_rule {
        die "wrong number of rule elements\n" if scalar(@data) != 3;
        die "groups disabled\n" if !$allow_groups;
 
-       die "invalid characters in group name\n" if $action !~ m/^[A-Za-z0-9_\-]+$/;
+       die "invalid characters in group name\n" if $action !~ m/^${security_group_pattern}$/;
     } else {
        die "unknown rule type '$type'\n";
     }
@@ -1765,7 +1843,7 @@ sub parse_vmfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags|ips):\s*(0|1)\s*$/i) {
+    if ($line =~ m/^(enable|dhcp|macfilter|ips):\s*(0|1)\s*$/i) {
        $opt = lc($1);
        $value = int($2);
     } elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
@@ -1792,16 +1870,13 @@ sub parse_hostfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|dhcp|nosmurfs|tcpflags|allow_bridge_route|optimize):\s*(0|1)\s*$/i) {
+    if ($line =~ m/^(enable|nosmurfs|tcpflags|allow_bridge_route|optimize):\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);
-    } elsif ($line =~ m/^(nf_conntrack_max):\s*(\d+)\s*$/i) {
+    } elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established):\s*(\d+)\s*$/i) {
        $opt = lc($1);
        $value = int($2);
     } else {
@@ -1820,6 +1895,9 @@ sub parse_clusterfw_option {
     if ($line =~ m/^(enable):\s*(0|1)\s*$/i) {
        $opt = lc($1);
        $value = int($2);
+    } 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"
@@ -1828,6 +1906,25 @@ sub parse_clusterfw_option {
     return ($opt, $value);
 }
 
+sub parse_clusterfw_alias {
+    my ($line) = @_;
+
+    my ($opt, $value);
+    if ($line =~ m/^(\S+)\s(\S+)$/) {
+       $opt = lc($1);
+       if($2){
+           $2 =~ s|/32$||;
+           pve_verify_ipv4_or_cidr($2) if $2;
+           $value = $2;
+       }
+    } else {
+       chomp $line;
+       die "can't parse option '$line'\n";
+    }
+
+    return ($opt, $value);
+}
+
 sub parse_vm_fw_rules {
     my ($filename, $fh) = @_;
 
@@ -1835,11 +1932,7 @@ sub parse_vm_fw_rules {
 
     my $section;
 
-    my $digest = Digest::SHA->new('sha1');
-
     while (defined(my $line = <$fh>)) {
-       $digest->add($line);
-
        next if $line =~ m/^#/;
        next if $line =~ m/^\s*$/;
 
@@ -1877,8 +1970,6 @@ sub parse_vm_fw_rules {
        push @{$res->{$section}}, $rule;
     }
 
-    $res->{digest} = $digest->b64digest;
-
     return $res;
 }
 
@@ -1889,11 +1980,7 @@ sub parse_host_fw_rules {
 
     my $section;
 
-    my $digest = Digest::SHA->new('sha1');
-
     while (defined(my $line = <$fh>)) {
-       $digest->add($line);
-
        next if $line =~ m/^#/;
        next if $line =~ m/^\s*$/;
 
@@ -1931,8 +2018,6 @@ sub parse_host_fw_rules {
        push @{$res->{$section}}, $rule;
     }
 
-$res->{digest} = $digest->b64digest;
-
     return $res;
 }
 
@@ -1942,13 +2027,16 @@ sub parse_cluster_fw_rules {
     my $section;
     my $group;
 
-    my $res = { rules => [], options => {}, groups => {}, ipset => {} };
-
-    my $digest = Digest::SHA->new('sha1');
+    my $res = { 
+       rules => [], 
+       options => {}, 
+       groups => {}, 
+       group_comments => {}, 
+       ipset => {} ,
+       ipset_comments => {}, 
+    };
 
     while (defined(my $line = <$fh>)) {
-       $digest->add($line);
-
        next if $line =~ m/^#/;
        next if $line =~ m/^\s*$/;
 
@@ -1960,9 +2048,18 @@ sub parse_cluster_fw_rules {
            next;
        }
 
-       if ($line =~ m/^\[group\s+(\S+)\]\s*$/i) {
+       if ($line =~ m/^\[aliases\]$/i) {
+           $section = 'aliases';
+           next;
+       }
+
+       if ($line =~ m/^\[group\s+(\S+)\]\s*(?:#\s*(.*?)\s*)?$/i) {
            $section = 'groups';
            $group = lc($1);
+           my $comment = $2;
+           $res->{$section}->{$group} = [];
+           $res->{group_comments}->{$group} =  decode('utf8', $comment)
+               if $comment;
            next;
        }
 
@@ -1971,9 +2068,13 @@ sub parse_cluster_fw_rules {
            next;
        }
 
-       if ($line =~ m/^\[ipset\s+(\S+)\]\s*$/i) {
+       if ($line =~ m/^\[ipset\s+(\S+)\]\s*(?:#\s*(.*?)\s*)?$/i) {
            $section = 'ipset';
            $group = lc($1);
+           my $comment = $2;
+           $res->{$section}->{$group} = [];
+           $res->{ipset_comments}->{$group} = decode('utf8', $comment) 
+               if $comment;
            next;
        }
 
@@ -1988,6 +2089,12 @@ sub parse_cluster_fw_rules {
                $res->{options}->{$opt} = $value;
            };
            warn "$prefix: $@" if $@;
+       } elsif ($section eq 'aliases') {
+           eval {
+               my ($opt, $value) = parse_clusterfw_alias($line);
+               $res->{aliases}->{$opt} = $value;
+           };
+           warn "$prefix: $@" if $@;
        } elsif ($section eq 'rules') {
            my $rule;
            eval { $rule = parse_fw_rule($line, 1, 1); };
@@ -2005,33 +2112,31 @@ sub parse_cluster_fw_rules {
            }
            push @{$res->{$section}->{$group}}, $rule;
        } elsif ($section eq 'ipset') {
-           chomp $line;
-           $line =~ m/^(\!)?\s*((\d+)\.(\d+)\.(\d+)\.(\d+)(\/(\d+))?)/;
-           my $nomatch = $1;
-       my $ip = $2;
+           # we can add single line comments to the end of the rule
+           my $comment = decode('utf8', $1) if $line =~ s/#\s*(.*?)\s*$//;
 
-           if(!$ip){
-               warn "$prefix: $line is not an valid ip address\n";
-               next;
-           }
-           if (!Net::IP->new($ip)) {
-               warn "$prefix: $line is not an valid ip address\n";
-               next;
-           }
-
-           if ($nomatch) {
-               if ($feature_ipset_nomatch) {
-                   push @{$res->{$section}->{$group}}, "$ip nomatch";
-               } else {
-                   warn "$prefix: ignore $line - nomatch not supported by kernel\n";
+           $line =~ m/^(\!)?\s*(\S+)\s*$/;
+           my $nomatch = $1;
+           my $cidr = $2;
+
+           if($cidr !~ m/^${security_group_pattern}$/) {
+               $cidr =~ s|/32$||;
+           
+               eval { pve_verify_ipv4_or_cidr($cidr); };
+               if (my $err = $@) {
+                   warn "$prefix: $cidr - $err";
+                   next;
                }
-           } else {
-               push @{$res->{$section}->{$group}}, $ip;
            }
+
+           my $entry = { cidr => $cidr }; 
+           $entry->{nomatch} = 1 if $nomatch;
+           $entry->{comment} = $comment if $comment;
+           
+           push @{$res->{$section}->{$group}}, $entry;
        }
     }
 
-    $res->{digest} = $digest->b64digest;
     return $res;
 }
 
@@ -2102,7 +2207,7 @@ my $format_rules = sub {
     my $raw = '';
 
     foreach my $rule (@$rules) {
-       if ($rule->{type} eq  'in' || $rule->{type} eq 'out') {
+       if ($rule->{type} eq  'in' || $rule->{type} eq 'out' || $rule->{type} eq 'group') {
            $raw .= '|' if defined($rule->{enable}) && !$rule->{enable};
            $raw .= uc($rule->{type});
            if ($rule->{macro}) {
@@ -2111,16 +2216,20 @@ my $format_rules = sub {
                $raw .= " " . $rule->{action};
            }
            $raw .= " " . ($rule->{iface} || '-') if $need_iface;
-           $raw .= " " . ($rule->{source} || '-');
-           $raw .= " " . ($rule->{dest} || '-');
-           $raw .= " " . ($rule->{proto} || '-');
-           $raw .= " " . ($rule->{dport} || '-');
-           $raw .= " " . ($rule->{sport} || '-');
+
+           if ($rule->{type} ne  'group')  {
+               $raw .= " " . ($rule->{source} || '-');
+               $raw .= " " . ($rule->{dest} || '-');
+               $raw .= " " . ($rule->{proto} || '-');
+               $raw .= " " . ($rule->{dport} || '-');
+               $raw .= " " . ($rule->{sport} || '-');
+           }
+
            $raw .= " # " . encode('utf8', $rule->{comment})
                if $rule->{comment} && $rule->{comment} !~ m/^\s*$/;
            $raw .= "\n";
        } else {
-           die "implement me '$rule->{type}'";
+           die "unknown rule type '$rule->{type}'";
        }
     }
 
@@ -2141,6 +2250,28 @@ my $format_options = sub {
     return $raw;
 };
 
+my $format_ipset = sub {
+    my ($options) = @_;
+
+    my $raw = '';
+
+    my $nethash = {};
+    foreach my $entry (@$options) {
+       $nethash->{$entry->{cidr}} = $entry;
+    }
+
+    foreach my $cidr (sort keys %$nethash) {
+       my $entry = $nethash->{$cidr};
+       my $line = $entry->{nomatch} ? '!' : '';
+       $line .= $entry->{cidr};
+       $line .= " # " . encode('utf8', $entry->{comment})
+           if $entry->{comment} && $entry->{comment} !~ m/^\s*$/;
+       $raw .= "$line\n";
+    }
+    return $raw;
+};
+
 sub save_vmfw_conf {
     my ($vmid, $vmfw_conf) = @_;
 
@@ -2197,6 +2328,7 @@ sub generate_std_chains {
 
     # same as shorewall smurflog.
     my $chain = 'PVEFW-smurflog';
+    $pve_std_chains->{$chain} = [];
 
     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
     push @{$pve_std_chains->{$chain}}, "-j DROP";
@@ -2204,6 +2336,8 @@ sub generate_std_chains {
     # same as shorewall logflags action.
     $loglevel = get_option_log_level($options, 'tcp_flags_log_level');
     $chain = 'PVEFW-logflags';
+    $pve_std_chains->{$chain} = [];
+
     # fixme: is this correctly logged by pvewf-logger? (ther is no --log-ip-options for NFLOG)
     push @{$pve_std_chains->{$chain}}, get_log_rule_base($chain, 0, "DROP: ", $loglevel) if $loglevel;
     push @{$pve_std_chains->{$chain}}, "-j DROP";
@@ -2224,12 +2358,12 @@ sub generate_ipset_chains {
     my ($ipset_ruleset, $fw_conf) = @_;
 
     foreach my $ipset (keys %{$fw_conf->{ipset}}) {
-       generate_ipset($ipset_ruleset, "PVEFW-$ipset", $fw_conf->{ipset}->{$ipset});
+       generate_ipset($ipset_ruleset, "PVEFW-$ipset", $fw_conf->{ipset}->{$ipset}, $fw_conf->{aliases});
     }
 }
 
 sub generate_ipset {
-    my ($ipset_ruleset, $name, $options) = @_;
+    my ($ipset_ruleset, $name, $options, $aliases) = @_;
 
     my $hashsize = scalar(@$options);
     if ($hashsize <= 64) {
@@ -2240,8 +2374,31 @@ sub generate_ipset {
 
     push @{$ipset_ruleset->{$name}}, "create $name hash:net family inet hashsize $hashsize maxelem $hashsize";
 
-    foreach my $ip (@$options) {
-       push @{$ipset_ruleset->{$name}}, "add $name $ip";
+    # remove duplicates
+    my $nethash = {};
+    foreach my $entry (@$options) {
+       my $cidr = $entry->{cidr};
+       #check aliases
+       if ($cidr =~ m/^${security_group_pattern}$/){
+           die "no such alias $cidr" if !$aliases->{$cidr};
+           $entry->{cidr} = $aliases->{$cidr};
+       }
+       $nethash->{$entry->{cidr}} = $entry;
+    }
+
+    foreach my $cidr (sort keys %$nethash) {
+       my $entry = $nethash->{$cidr};
+
+       my $cmd = "add $name $cidr";
+       if ($entry->{nomatch}) {
+           if ($feature_ipset_nomatch) {
+               push @{$ipset_ruleset->{$name}}, "$cmd nomatch";
+           } else {
+               warn "ignore !$cidr - nomatch not supported by kernel\n";
+           }
+       } else {
+           push @{$ipset_ruleset->{$name}}, $cmd;
+       }
     }
 }
 
@@ -2323,7 +2480,17 @@ sub save_clusterfw_conf {
     my $options = $cluster_conf->{options};
     $raw .= &$format_options($options) if scalar(keys %$options);
 
-    # fixme: save ipset
+    foreach my $ipset (sort keys %{$cluster_conf->{ipset}}) {
+       if (my $comment = $cluster_conf->{ipset_comments}->{$ipset}) {
+           my $utf8comment = encode('utf8', $comment);
+           $raw .= "[IPSET $ipset] # $utf8comment\n\n";
+       } else {
+           $raw .= "[IPSET $ipset]\n\n";
+       }
+       my $options = $cluster_conf->{ipset}->{$ipset};
+       $raw .= &$format_ipset($options);
+       $raw .= "\n";
+    }
 
     my $rules = $cluster_conf->{rules};
     if (scalar(@$rules)) {
@@ -2334,7 +2501,13 @@ sub save_clusterfw_conf {
 
     foreach my $group (sort keys %{$cluster_conf->{groups}}) {
        my $rules = $cluster_conf->{groups}->{$group};
-       $raw .= "[group $group]\n\n";
+       if (my $comment = $cluster_conf->{group_comments}->{$group}) {
+           my $utf8comment = encode('utf8', $comment);
+           $raw .= "[group $group] # $utf8comment\n\n";
+       } else {
+           $raw .= "[group $group]\n\n";
+       }
+
        $raw .= &$format_rules($rules, 0);
        $raw .= "\n";
     }
@@ -2370,13 +2543,16 @@ sub save_hostfw_conf {
 }
 
 sub compile {
+    my ($cluster_conf, $hostfw_conf) = @_;
+
+    $cluster_conf = load_clusterfw_conf() if !$cluster_conf;
+    $hostfw_conf = load_hostfw_conf() if !$hostfw_conf;
+
     my $vmdata = read_local_vm_config();
     my $vmfw_configs = read_vm_firewall_configs($vmdata);
 
     my $routing_table = read_proc_net_route();
 
-    my $cluster_conf = load_clusterfw_conf();
-
     my $ipset_ruleset = {};
     generate_ipset_chains($ipset_ruleset, $cluster_conf);
 
@@ -2387,7 +2563,6 @@ sub compile {
 
     ruleset_create_chain($ruleset, "PVEFW-FORWARD");
 
-    my $hostfw_conf = load_hostfw_conf();
     my $hostfw_options = $hostfw_conf->{options} || {};
 
     generate_std_chains($ruleset, $hostfw_options);
@@ -2417,9 +2592,9 @@ sub compile {
            generate_bridge_chains($ruleset, $hostfw_conf, $bridge, $routing_table);
 
            my $macaddr = $net->{macaddr};
-           generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+           generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
                                         $vmfw_conf, $vmid, $bridge, 'IN');
-           generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+           generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
                                         $vmfw_conf, $vmid, $bridge, 'OUT');
        }
     }
@@ -2434,8 +2609,8 @@ sub compile {
 
        if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
            my $ip = $conf->{ip_address}->{value};
-           generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'IN');
-           generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'OUT');
+           generate_venet_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'IN');
+           generate_venet_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $vmfw_conf, $vmid, $ip, 'OUT');
        }
 
        if ($conf->{netif} && $conf->{netif}->{value}) {
@@ -2452,9 +2627,9 @@ sub compile {
 
                my $macaddr = $d->{mac};
                my $iface = $d->{host_ifname};
-               generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+               generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
                                             $vmfw_conf, $vmid, $bridge, 'IN');
-               generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
+               generate_tap_rules_direction($ruleset, $cluster_conf, $hostfw_conf, $iface, $netid, $macaddr,
                                             $vmfw_conf, $vmid, $bridge, 'OUT');
            }
        }
@@ -2483,7 +2658,7 @@ sub compile {
     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o vmbr+ -j DROP");
     ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i vmbr+ -j DROP");
 
-    return wantarray ? ($ruleset, $hostfw_conf, $ipset_ruleset) : $ruleset;
+    return ($ruleset, $ipset_ruleset);
 }
 
 sub get_ruleset_status {
@@ -2602,6 +2777,13 @@ sub get_ipset_cmdlist {
     my $active_chains = ipset_get_chains();
     my $statushash = get_ruleset_status($ruleset, $active_chains, \&ipset_chain_digest, $verbose);
 
+    # remove stale _swap chains 
+    foreach my $chain (keys %$active_chains) {
+       if ($chain =~ m/^PVEFW-\S+_swap$/) {
+           $cmdlist .= "destroy $chain\n";
+       }
+    }
+
     foreach my $chain (sort keys %$ruleset) {
        my $stat = $statushash->{$chain};
        die "internal error" if !$stat;
@@ -2645,6 +2827,8 @@ sub apply_ruleset {
 
     update_nf_conntrack_max($hostfw_conf);
 
+    update_nf_conntrack_tcp_timeout_established($hostfw_conf);
+
     my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
        get_ipset_cmdlist($ipset_ruleset, undef, $verbose);
 
@@ -2709,6 +2893,16 @@ sub update_nf_conntrack_max {
     }
 }
 
+sub update_nf_conntrack_tcp_timeout_established {
+    my ($hostfw_conf) = @_;
+
+    my $options = $hostfw_conf->{options} || {};
+
+    my $value = defined($options->{nf_conntrack_tcp_timeout_established}) ? $options->{nf_conntrack_tcp_timeout_established} : 432000;
+
+    PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established", $value);
+}
+
 sub remove_pvefw_chains {
 
     my ($chash, $hooks) = iptables_get_chains();
@@ -2736,9 +2930,25 @@ sub update {
     my ($start, $verbose) = @_;
 
     my $code = sub {
+
+       my $cluster_conf = load_clusterfw_conf();
+       my $cluster_options = $cluster_conf->{options};
+
+       my $enable = $cluster_options->{enable};
+
        my $status = read_pvefw_status();
 
-       my ($ruleset, $hostfw_conf, $ipset_ruleset) = compile();
+       die "Firewall is disabled - cannot start\n" if !$enable && $start;
+
+       if (!$enable) {
+           PVE::Firewall::remove_pvefw_chains();
+           print "Firewall disabled\n" if $verbose;
+           return;
+       }
+
+       my $hostfw_conf = load_hostfw_conf();
+
+       my ($ruleset, $ipset_ruleset) = compile($cluster_conf, $hostfw_conf);
 
        if ($start || $status eq 'active') {