implement ipset ip/net groups
authorAlexandre Derumier <aderumier@odiso.com>
Thu, 27 Mar 2014 10:22:06 +0000 (11:22 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Fri, 28 Mar 2014 09:11:42 +0000 (10:11 +0100)
This implement ipset groups of ips or network in groups.fw.

groups.fw
---------
[ipgroup ipgroup1]

192.168.0.1
192.168.0.2
192.168.0.3

[ipgroup ipgroup2]

192.168.0.3
192.168.0.4

[netgroup netgroup1]

192.168.0.0/24
10.0.0.0/8

Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
example/groups.fw
src/PVE/Firewall.pm

index eab4b2d..b9c088f 100644 (file)
@@ -10,3 +10,23 @@ IN  ACCEPT 10.0.0.1
 IN  ACCEPT 10.0.0.2
 IN  ACCEPT 10.0.0.2 
 
+
+#ipset hash:ip
+[ipgroup ipgroup1]
+
+192.168.0.1
+192.168.0.2
+192.168.0.3
+
+
+[ipgroup ipgroup2]
+
+192.168.0.3
+192.168.0.4
+
+#ipset hash:net
+[netgroup netgroup1]
+
+192.168.0.0/24
+10.0.0.0/8
+
index 630e5c4..b0bcd94 100644 (file)
@@ -801,6 +801,12 @@ sub iptables_restore_cmdlist {
     run_command("/sbin/iptables-restore -n", input => $cmdlist);
 }
 
+sub ipset_restore_cmdlist {
+    my ($cmdlist) = @_;
+
+    run_command(" /usr/sbin/ipset restore", input => $cmdlist);
+}
+
 sub iptables_get_chains {
 
     my $res = {};
@@ -857,6 +863,38 @@ sub iptables_get_chains {
     return $res;
 }
 
+sub ipset_get_chains {
+
+    my $res = {};
+    my $chains = {};
+
+    my $parser = sub {
+       my $line = shift;
+
+       return if $line =~ m/^#/;
+       return if $line =~ m/^\s*$/;
+       if ($line =~ m/^(\S+)\s(\S+)\s(\S+)/) {
+          push @{$chains->{$2}}, $line;
+       } else {
+           # simply ignore the rest
+           return;
+       }
+    };
+
+    run_command(" /usr/sbin/ipset save", outfunc => $parser);
+
+    #comptute sig for each chain
+    foreach my $chain (keys %$chains){
+       my $digest = Digest::SHA->new('sha1');
+       foreach my $rule (@{$chains->{$chain}}) {
+           $digest->add($rule);
+       }
+       $res->{$chain} = $digest->b64digest;
+    }
+
+    return $res;
+}
+
 sub iptables_chain_exist {
     my ($chain) = @_;
 
@@ -1709,7 +1747,7 @@ sub parse_group_fw_rules {
     my $section;
     my $group;
 
-    my $res = { rules => {} };
+    my $res = { rules => {}, ipset => {} };
 
     my $digest = Digest::SHA->new('sha1');
 
@@ -1727,19 +1765,46 @@ sub parse_group_fw_rules {
            $group = lc($1);
            next;
        }
+
+       if ($line =~ m/^\[ipgroup\s+(\S+)\]\s*$/i) {
+           $section = 'ipset';
+           $group = lc($1);
+           $res->{$section}->{$group}->{type} = 'hash:ip family inet hashsize 1024 maxelem 65536 ';
+
+           next;
+       }
+       if ($line =~ m/^\[netgroup\s+(\S+)\]\s*$/i) {
+           $section = 'ipset';
+           $group = lc($1);
+           $res->{$section}->{$group}->{type} = 'hash:net family inet hashsize 1024 maxelem 65536 ';
+
+           next;
+       }
+
        if (!$section || !$group) {
            warn "$prefix: skip line - no section";
            next;
        }
 
-       my $rule;
-       eval { $rule = parse_fw_rule($line, 0, 0); };
-       if (my $err = $@) {
-           warn "$prefix: $err";
-           next;
+       if($section eq 'rules'){
+           my $rule;
+           eval { $rule = parse_fw_rule($line, 0, 0); };
+           if (my $err = $@) {
+               warn "$prefix: $err";
+               next;
+           }
+       push @{$res->{$section}->{$group}}, $rule;
+
+       }elsif($section eq 'ipset'){
+           my $ip;
+           if (!Net::IP->new($line)) {
+               warn "$prefix: $line is not an valid ip address";
+               next;
+           }
+           push @{$res->{$section}->{$group}->{ip}}, $line;
        }
+
        
-       push @{$res->{$section}->{$group}}, $rule;
     }
 
     $res->{digest} = $digest->b64digest;
@@ -1868,6 +1933,24 @@ sub generate_std_chains {
     }
 }
 
+sub generate_ipset_chains {
+    my ($ipset_ruleset, $options) = @_;
+
+    foreach my $ipset (keys %{$options->{ipset}}) {
+       generate_ipset($ipset_ruleset, $ipset, $options->{ipset}->{$ipset});
+    }
+}
+
+sub generate_ipset {
+    my ($ipset_ruleset, $name, $options) = @_;
+
+    push @{$ipset_ruleset->{$name}}, "create $name $options->{type}";
+
+    foreach my $ip (@{$options->{ip}}) {
+       push @{$ipset_ruleset->{$name}}, "add $name $ip";
+    }
+}
+
 sub save_pvefw_status {
     my ($status) = @_;
 
@@ -1928,7 +2011,7 @@ sub load_security_groups {
        $groups_conf = parse_group_fw_rules($filename, $fh);
     }
 
-    return $groups_conf;
+    return ($groups_conf);
 }
 
 sub save_security_groups {
@@ -1983,6 +2066,9 @@ sub compile {
 
     my $groups_conf = load_security_groups();
 
+    my $ipset_ruleset = {};
+    generate_ipset_chains($ipset_ruleset, $groups_conf);
+
     my $ruleset = {};
 
     ruleset_create_chain($ruleset, "PVEFW-INPUT");
@@ -2086,13 +2172,18 @@ 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) : $ruleset;
+    return wantarray ? ($ruleset, $hostfw_conf, $ipset_ruleset) : $ruleset;
 }
 
 sub get_ruleset_status {
-    my ($ruleset, $verbose) = @_;
+    my ($ruleset, $verbose, $ipset) = @_;
 
-    my $active_chains = iptables_get_chains();
+    my $active_chains = undef;
+    if($ipset){
+       $active_chains = ipset_get_chains();
+    }else{
+       $active_chains = iptables_get_chains();
+    }
 
     my $statushash = {};
 
@@ -2211,17 +2302,63 @@ sub get_rulset_cmdlist {
     return $cmdlist;
 }
 
+sub get_ipset_cmdlist {
+    my ($ruleset, $verbose) = @_;
+
+    my $cmdlist = "";
+
+    my $statushash = get_ruleset_status($ruleset, $verbose, 1);
+
+    foreach my $chain (sort keys %$ruleset) {
+       my $stat = $statushash->{$chain};
+       die "internal error" if !$stat;
+
+       if ($stat->{action} eq 'create') {
+           foreach my $cmd (@{$ruleset->{$chain}}) {
+               $cmdlist .= "$cmd\n";
+           }
+        }
+
+       if ($stat->{action} eq 'update') {
+           my $chain_swap = $chain."_swap";
+
+           foreach my $cmd (@{$ruleset->{$chain}}) {
+               $cmd =~ s/$chain/$chain_swap/;
+               $cmdlist .= "$cmd\n";
+
+           }
+           $cmdlist .= "swap $chain_swap $chain\n";
+           $cmdlist .= "flush $chain_swap\n";
+           $cmdlist .= "destroy $chain_swap\n";
+
+        }
+
+    }
+
+    foreach my $chain (keys %$statushash) {
+       next if $statushash->{$chain}->{action} ne 'delete';
+       $cmdlist .= "flush $chain\n";
+       $cmdlist .= "destroy $chain\n";
+    }
+
+    return $cmdlist;
+}
+
 sub apply_ruleset {
-    my ($ruleset, $hostfw_conf, $verbose) = @_;
+    my ($ruleset, $hostfw_conf, $ipset_ruleset, $verbose) = @_;
 
     enable_bridge_firewall();
 
     update_nf_conntrack_max($hostfw_conf);
 
+    my $ipsetcmdlist = get_ipset_cmdlist($ipset_ruleset, $verbose);
+
     my $cmdlist = get_rulset_cmdlist($ruleset, $verbose);
 
     print $cmdlist if $verbose;
 
+    ipset_restore_cmdlist($ipsetcmdlist);
+
     iptables_restore_cmdlist($cmdlist);
 
     # test: re-read status and check if everything is up to date
@@ -2269,13 +2406,13 @@ sub update {
     my $code = sub {
        my $status = read_pvefw_status();
 
-       my ($ruleset, $hostfw_conf) = PVE::Firewall::compile();
+       my ($ruleset, $hostfw_conf, $ipset_ruleset) = PVE::Firewall::compile();
 
        if ($start || $status eq 'active') {
 
            save_pvefw_status('active') if ($status ne 'active');
 
-           apply_ruleset($ruleset, $hostfw_conf, $verbose);
+           apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $verbose);
        } else {
            print "Firewall not active (status = $status)\n" if $verbose;
        }