]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
add ipv6 ipset support
[pve-firewall.git] / src / PVE / Firewall.pm
index 76684f9676ddc5a49bfcd3ab61b4725d72e8bf75..e2cf5f8be125fb2bc215379857b8766d10ce6327 100644 (file)
@@ -5,12 +5,13 @@ 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);
 use PVE::Cluster;
 use PVE::ProcFSTools;
-use PVE::Tools qw($IPV4RE);
+use PVE::Tools qw($IPV4RE $IPV6RE);
 use File::Basename;
 use File::Path;
 use IO::File;
@@ -44,11 +45,11 @@ my $max_alias_name_length = 64;
 my $max_ipset_name_length = 64;
 my $max_group_name_length = 20;
 
-PVE::JSONSchema::register_format('IPv4orCIDR', \&pve_verify_ipv4_or_cidr);
-sub pve_verify_ipv4_or_cidr {
+PVE::JSONSchema::register_format('IPorCIDR', \&pve_verify_ip_or_cidr);
+sub pve_verify_ip_or_cidr {
     my ($cidr, $noerr) = @_;
 
-    if ($cidr =~ m!^(?:$IPV4RE)(/(\d+))?$!) {
+    if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(/(\d+))?$!) {
        return $cidr if Net::IP->new($cidr);
        return undef if $noerr;
        die Net::IP::Error() . "\n";
@@ -57,19 +58,13 @@ sub pve_verify_ipv4_or_cidr {
     die "value does not look like a valid IP address or CIDR network\n";
 }
 
-PVE::JSONSchema::register_format('IPv4orCIDRorAlias', \&pve_verify_ipv4_or_cidr_or_alias);
-sub pve_verify_ipv4_or_cidr_or_alias {
+PVE::JSONSchema::register_format('IPorCIDRorAlias', \&pve_verify_ip_or_cidr_or_alias);
+sub pve_verify_ip_or_cidr_or_alias {
     my ($cidr, $noerr) = @_;
 
     return if $cidr =~ m/^(?:$ip_alias_pattern)$/;
 
-    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";
+    return pve_verify_ip_or_cidr($cidr, $noerr);
 }
 
 PVE::JSONSchema::register_standard_option('ipset-name', {
@@ -761,10 +756,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) = @_;
@@ -773,7 +770,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);
     }
@@ -802,19 +798,27 @@ sub parse_address_list {
 
     my $count = 0;
     my $iprange = 0;
-    my $ipversion = undef;
+    my $ipversion;
 
     foreach my $elem (split(/,/, $str)) {
        $count++;
-       if (!Net::IP->new($elem)) {
+       my $ip = Net::IP->new($elem);
+       if (!$ip) {
            my $err = Net::IP::Error();
            die "invalid IP address: $err\n";
        }
        $iprange = 1 if $elem =~ m/-/;
-       $ipversion = Net::IP::ip_get_version($elem); #fixme : don't work with range
+
+       my $new_ipversion = Net::IP::ip_is_ipv6($ip->ip()) ? 6 : 4;
+
+       die "detected mixed ipv4/ipv6 addresses in address list '$str'\n"
+           if $ipversion && ($new_ipversion != $ipversion);
+
+       $ipversion = $new_ipversion;
     }
 
-    die "you can use a range in a list\n" if $iprange && $count > 1;
+    die "you can't use a range in a list\n" if $iprange && $count > 1;
+
     return $ipversion;
 }
 
@@ -1075,7 +1079,6 @@ sub verify_rule {
     my ($rule, $cluster_conf, $fw_conf, $rule_env, $noerr) = @_;
 
     my $allow_groups = $rule_env eq 'group' ? 0 : 1;
-    my $ipversion = undef;
 
     my $allow_iface = $rule_env_iface_lookup->{$rule_env};
     die "unknown rule_env '$rule_env'\n" if !defined($allow_iface); # should not happen
@@ -1093,7 +1096,7 @@ sub verify_rule {
     };
 
     my $check_ipset_or_alias_property = sub {
-       my ($name) = @_;
+       my ($name, $expected_ipversion) = @_;
 
        if (my $value = $rule->{$name}) {
            if ($value =~ m/^\+/) {
@@ -1107,7 +1110,13 @@ sub verify_rule {
            } elsif ($value =~ m/^${ip_alias_pattern}$/){
                my $alias = lc($value);
                &$add_error($name, "no such alias '$value'")
-                   if !($cluster_conf->{aliases}->{$alias} || ($fw_conf && $fw_conf->{aliases}->{$alias}))
+                   if !($cluster_conf->{aliases}->{$alias} || ($fw_conf && $fw_conf->{aliases}->{$alias}));
+
+               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});
            }
        }
     };
@@ -1173,16 +1182,23 @@ sub verify_rule {
            if !$rule->{proto};
     }
 
+    my $ipversion;
+
     if ($rule->{source}) {
        eval { $ipversion = parse_address_list($rule->{source}); };
        &$add_error('source', $@) if $@;
-       &$check_ipset_or_alias_property('source');
+       &$check_ipset_or_alias_property('source', $ipversion);
     }
 
     if ($rule->{dest}) {
-       eval { $ipversion = parse_address_list($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;
+       };
        &$add_error('dest', $@) if $@;
-       &$check_ipset_or_alias_property('dest');
+       &$check_ipset_or_alias_property('dest', $ipversion);
     }
 
     if ($rule->{macro} && !$error_count) {
@@ -1707,12 +1723,12 @@ sub ruleset_create_vm_chain {
 }
 
 sub ruleset_add_group_rule {
-    my ($ruleset, $cluster_conf, $chain, $rule, $direction, $action) = @_;
+    my ($ruleset, $cluster_conf, $chain, $rule, $direction, $action, $ipversion) = @_;
 
     my $group = $rule->{action};
     my $group_chain = "GROUP-$group-$direction";
     if(!ruleset_chain_exist($ruleset, $group_chain)){
-       generate_group_rules($ruleset, $cluster_conf, $group);
+       generate_group_rules($ruleset, $cluster_conf, $group, $ipversion);
     }
 
     if ($direction eq 'OUT' && $rule->{iface_out}) {
@@ -1727,7 +1743,7 @@ sub ruleset_add_group_rule {
 }
 
 sub ruleset_generate_vm_rules {
-    my ($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, $netid, $direction, $options) = @_;
+    my ($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, $netid, $direction, $options, $ipversion) = @_;
 
     my $lc_direction = lc($direction);
 
@@ -1736,9 +1752,11 @@ sub ruleset_generate_vm_rules {
     foreach my $rule (@$rules) {
        next if $rule->{iface} && $rule->{iface} ne $netid;
        next if !$rule->{enable} || $rule->{errors};
+       next if $rule->{ipversion} && ($rule->{ipversion} != $ipversion);
+
        if ($rule->{type} eq 'group') {
            ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, $direction,
-                                  $direction eq 'OUT' ? 'RETURN' : $in_accept);
+                                  $direction eq 'OUT' ? 'RETURN' : $in_accept, $ipversion);
        } else {
            next if $rule->{type} ne $lc_direction;
            eval {
@@ -1791,7 +1809,7 @@ sub ruleset_generate_vm_ipsrules {
 }
 
 sub generate_venet_rules_direction {
-    my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $direction) = @_;
+    my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $direction, $ipversion) = @_;
 
     my $lc_direction = lc($direction);
 
@@ -1804,7 +1822,7 @@ sub generate_venet_rules_direction {
 
     ruleset_create_vm_chain($ruleset, $chain, $options, undef, undef, $direction);
 
-    ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, 'venet', $direction);
+    ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, 'venet', $direction, undef, $ipversion);
 
     # implement policy
     my $policy;
@@ -1833,7 +1851,7 @@ sub generate_venet_rules_direction {
 }
 
 sub generate_tap_rules_direction {
-    my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $direction) = @_;
+    my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $direction, $ipversion) = @_;
 
     my $lc_direction = lc($direction);
 
@@ -1852,7 +1870,7 @@ sub generate_tap_rules_direction {
     ruleset_create_vm_chain($ruleset, $tapchain, $options, $macaddr, $ipfilter_ipset, $direction);
 
     if ($options->{enable}) {
-       ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $tapchain, $netid, $direction, $options);
+       ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $tapchain, $netid, $direction, $options, $ipversion);
 
        ruleset_generate_vm_ipsrules($ruleset, $options, $direction, $iface);
 
@@ -1884,7 +1902,7 @@ sub generate_tap_rules_direction {
 }
 
 sub enable_host_firewall {
-    my ($ruleset, $hostfw_conf, $cluster_conf) = @_;
+    my ($ruleset, $hostfw_conf, $cluster_conf, $ipversion) = @_;
 
     my $options = $hostfw_conf->{options};
     my $cluster_options = $cluster_conf->{options};
@@ -1915,7 +1933,7 @@ sub enable_host_firewall {
 
        eval {
            if ($rule->{type} eq 'group') {
-               ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'IN', $accept_action);
+               ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'IN', $accept_action, $ipversion);
            } elsif ($rule->{type} eq 'in') {
                ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" },
                                      undef, $cluster_conf, $hostfw_conf);
@@ -1968,7 +1986,7 @@ sub enable_host_firewall {
        $rule->{iface_out} = $rule->{iface} if $rule->{iface};
        eval {
            if ($rule->{type} eq 'group') {
-               ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'OUT', $accept_action);
+               ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'OUT', $accept_action, $ipversion);
            } elsif ($rule->{type} eq 'out') {
                ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => $accept_action, REJECT => "PVEFW-reject" },
                                      undef, $cluster_conf, $hostfw_conf);
@@ -1999,7 +2017,7 @@ sub enable_host_firewall {
 }
 
 sub generate_group_rules {
-    my ($ruleset, $cluster_conf, $group) = @_;
+    my ($ruleset, $cluster_conf, $group, $ipversion) = @_;
 
     my $rules = $cluster_conf->{groups}->{$group};
 
@@ -2015,6 +2033,7 @@ sub generate_group_rules {
 
     foreach my $rule (@$rules) {
        next if $rule->{type} ne 'in';
+       next if $rule->{ipversion} && $rule->{ipversion} ne $ipversion;
        ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" }, undef, $cluster_conf);
     }
 
@@ -2025,6 +2044,7 @@ sub generate_group_rules {
 
     foreach my $rule (@$rules) {
        next if $rule->{type} ne 'out';
+       next if $rule->{ipversion} && $rule->{ipversion} ne $ipversion;
        # 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,
@@ -2184,16 +2204,31 @@ 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 wantarray ? ($e->{cidr}, $e->{ipversion}) : $e->{cidr};
+}
+
+sub parse_ip_or_cidr {
+    my ($cidr) = @_;
+
+    my $ipversion;
+    
+    if ($cidr =~ m!^(?:$IPV6RE)(/(\d+))?$!) {
+       $cidr =~ s|/128$||;
+       $ipversion = 6;
+    } elsif ($cidr =~ m!^(?:$IPV4RE)(/(\d+))?$!) {
+       $cidr =~ s|/32$||;
+       $ipversion = 4;
+    } else {
+       die "value does not look like a valid IP address or CIDR network\n";
     }
 
-    return $cidr;
+    return wantarray ? ($cidr, $ipversion) : $cidr;
 }
 
 sub parse_alias {
@@ -2204,11 +2239,14 @@ sub parse_alias {
 
     if ($line =~ m/^(\S+)\s(\S+)$/) {
        my ($name, $cidr) = ($1, $2);
-       $cidr =~ s|/32$||;
-       pve_verify_ipv4_or_cidr($cidr);
+       my $ipversion;
+
+       ($cidr, $ipversion) = parse_ip_or_cidr($cidr);
+
        my $data = {
            name => $name,
            cidr => $cidr,
+           ipversion => $ipversion,
        };
        $data->{comment} = $comment  if $comment;
        return $data;
@@ -2346,8 +2384,7 @@ sub generic_fw_config_parser {
                if ($cidr =~ m/^${ip_alias_pattern}$/) {
                    resolve_alias($cluster_conf, $res, $cidr); # make sure alias exists
                } else {
-                   $cidr =~ s|/32$||;
-                   pve_verify_ipv4_or_cidr_or_alias($cidr);
+                   $cidr = parse_ip_or_cidr($cidr);
                }
            };
            if (my $err = $@) {
@@ -2700,39 +2737,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 $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"];
 
-       my $cmd = "add $name $cidr";
-       if ($entry->{nomatch}) {
-           if ($feature_ipset_nomatch) {
-               push @{$ipset_ruleset->{$name}}, "$cmd nomatch";
+       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";
     }
 }
 
@@ -2909,8 +2971,9 @@ sub compile_iptables_filter {
 
     my $ipset_ruleset = {};
 
-    if ($hostfw_enable) {
-       eval { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf); };
+    # currently pveproxy don't works with ipv6, so let's generate host fw ipv4 only for the moment
+    if ($hostfw_enable && ($ipversion == 4)) {
+       eval { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf, $ipversion); };
        warn $@ if $@; # just to be sure - should not happen
     }
 
@@ -2933,9 +2996,9 @@ sub compile_iptables_filter {
 
                my $macaddr = $net->{macaddr};
                generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
-                                            $vmfw_conf, $vmid, 'IN');
+                                            $vmfw_conf, $vmid, 'IN', $ipversion);
                generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
-                                            $vmfw_conf, $vmid, 'OUT');
+                                            $vmfw_conf, $vmid, 'OUT', $ipversion);
            }
        };
        warn $@ if $@; # just to be sure - should not happen
@@ -2955,18 +3018,20 @@ sub compile_iptables_filter {
                if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
                    my $ip = $conf->{ip_address}->{value};
                    $ip =~ s/\s+/,/g;
-                   parse_address_list($ip); # make sure we have a valid $ip list
 
-                   my @ips = split(',', $ip);
+                   my @ips = ();
 
-                   foreach my $singleip (@ips) {
-                       my $venet0ipset = {};
-                       $venet0ipset->{cidr} = $singleip;
-                       push @{$cluster_conf->{ipset}->{venet0}}, $venet0ipset;
+                   foreach my $singleip (split(',', $ip)) {
+                       my $singleip_ver = parse_address_list($singleip); # make sure we have a valid $ip list
+                       push @{$cluster_conf->{ipset}->{venet0}}, { cidr => $singleip };
+                       push @ips, $singleip if $singleip_ver == $ipversion;
                    }
 
-                   generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'IN');
-                   generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, 'OUT');
+                   if (scalar(@ips)) {
+                       my $ip_list = join(',', @ips);
+                       generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip_list, 'IN', $ipversion);
+                       generate_venet_rules_direction($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip_list, 'OUT', $ipversion);
+                   }
                }
            }
 
@@ -2979,9 +3044,9 @@ sub compile_iptables_filter {
                    my $macaddr = $d->{mac};
                    my $iface = $d->{host_ifname};
                    generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
-                                                $vmfw_conf, $vmid, 'IN');
+                                                $vmfw_conf, $vmid, 'IN', $ipversion);
                    generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
-                                                $vmfw_conf, $vmid, 'OUT');
+                                                $vmfw_conf, $vmid, 'OUT', $ipversion);
                }
            }
        };
@@ -3130,6 +3195,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";
@@ -3142,10 +3212,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";