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;
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";
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', {
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) = @_;
my $id = "$vmid-${ipset_name}";
-
if ((length($id) + 6) > $max_iptables_ipset_name_length) {
$id = PVE::Tools::fnv31a_hex($id);
}
};
my $check_ipset_or_alias_property = sub {
- my ($name) = @_;
+ my ($name, $expected_ipversion) = @_;
if (my $value = $rule->{$name}) {
if ($value =~ m/^\+/) {
} 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});
}
}
};
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 {
my $dest_ipversion = parse_address_list($rule->{dest});
die "detected mixed ipv4/ipv6 adresses in rule\n"
- if defined($ipversion) && ($dest_ipversion != $ipversion);
- $ipversion = $dest_ipversion;
+ 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) {
}
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}) {
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 {
}
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};
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);
$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);
}
sub generate_group_rules {
- my ($ruleset, $cluster_conf, $group) = @_;
+ my ($ruleset, $cluster_conf, $group, $ipversion) = @_;
my $rules = $cluster_conf->{groups}->{$group};
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);
}
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,
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 {
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;
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 = $@) {
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"];
+
+ foreach my $cidr (sort keys %$data) {
+ my $entry = $data->{$cidr};
- my $cmd = "add $name $cidr";
- if ($entry->{nomatch}) {
- if ($feature_ipset_nomatch) {
- push @{$ipset_ruleset->{$name}}, "$cmd nomatch";
+ 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";
}
}
my $ipset_ruleset = {};
- if ($hostfw_enable && $ipversion eq 4) {
- 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
}
$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";
$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";