sub pve_verify_ip_or_cidr {
my ($cidr, $noerr) = @_;
- if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(/(\d+))?$!) {
- return $cidr if Net::IP->new($cidr);
+ if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(?:/\d+)?$!) {
+ # Net::IP throws an error if the masked CIDR part isn't zero, e.g., `192.168.1.155/24`
+ # fails but `192.168.1.0/24` succeeds. clean_cidr removes the non zero bits from the CIDR.
+ my $clean_cidr = clean_cidr($cidr);
+ return $cidr if Net::IP->new($clean_cidr);
return undef if $noerr;
+
die Net::IP::Error() . "\n";
}
return undef if $noerr;
return pve_verify_ip_or_cidr($cidr, $noerr);
}
+sub clean_cidr {
+ my ($cidr) = @_;
+ my ($ip, $len) = split('/', $cidr);
+ return $cidr if !$len;
+ my $ver = ($ip =~ m!^$IPV4RE$!) ? 4 : 6;
+
+ my $bin_ip = Net::IP::ip_iptobin( Net::IP::ip_expand_address($ip, $ver), $ver);
+ my $bin_mask = Net::IP::ip_get_mask($len, $ver);
+ my $clean_ip = Net::IP::ip_compress_address( Net::IP::ip_bintoip($bin_ip & $bin_mask, $ver), $ver);
+
+ return "${clean_ip}/$len";
+}
+
PVE::JSONSchema::register_standard_option('ipset-name', {
description => "IP set name.",
type => 'string',
],
};
+my $pve_fw_helpers = {
+ 'amanda' => { proto => 'udp', dport => '10080', 'v4' => 1, 'v6' => 1 },
+ 'ftp' => { proto => 'tcp', dport => '21', 'v4' => 1, 'v6' => 1},
+ 'irc' => { proto => 'tcp', dport => '6667', 'v4' => 1 },
+ 'netbios-ns' => { proto => 'udp', dport => '137', 'v4' => 1 },
+ 'pptp' => { proto => 'tcp', dport => '1723', 'v4' => 1, },
+ 'sane' => { proto => 'tcp', dport => '6566', 'v4' => 1, 'v6' => 1 },
+ 'sip' => { proto => 'udp', dport => '5060', 'v4' => 1, 'v6' => 1 },
+ 'snmp' => { proto => 'udp', dport => '161', 'v4' => 1 },
+ 'tftp' => { proto => 'udp', dport => '69', 'v4' => 1, 'v6' => 1},
+};
+
my $pve_fw_parsed_macros;
my $pve_fw_macro_descr;
my $pve_fw_macro_ipversion = {};
return (scalar(@elements) > 1);
}
+PVE::JSONSchema::register_format('pve-fw-conntrack-helper', \&pve_fw_verify_conntrack_helper);
+sub pve_fw_verify_conntrack_helper {
+ my ($list) = @_;
+
+ my @helpers = split(/,/, $list);
+ die "extraneous commas in list\n" if $list ne join(',', @helpers);
+ foreach my $helper (@helpers) {
+ die "unknown helper $helper" if !$pve_fw_helpers->{$helper};
+ }
+
+ return $list;
+}
+
PVE::JSONSchema::register_format('pve-fw-sport-spec', \&pve_fw_verify_sport_spec);
sub pve_fw_verify_sport_spec {
my ($portstr) = @_;
default => 0,
optional => 1,
},
+ nf_conntrack_helpers => {
+ type => 'string', format => 'pve-fw-conntrack-helper',
+ description => "Enable conntrack helpers for specific protocols. ".
+ "Supported protocols: amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp",
+ default => '',
+ optional => 1,
+ },
protection_synflood => {
description => "Enable synflood protection",
type => 'boolean',
$targetstr = $rule->{target};
} else {
my $action = (defined $rule->{action}) ? $rule->{action} : "";
- my $goto = 1 if $action eq 'PVEFW-SET-ACCEPT-MARK';
- $targetstr = ($goto) ? "-g $action" : "-j $action";
+ $targetstr = $action eq 'PVEFW-SET-ACCEPT-MARK' ? "-g $action" : "-j $action";
}
my @iptcmds;
my $tapchain = "$iface-$direction";
my $ipfilter_name = compute_ipfilter_ipset_name($netid);
- my $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion)
+ my $ipfilter_ipset;
+ $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion)
if $options->{ipfilter} || $vmfw_conf->{ipset}->{$ipfilter_name};
if ($options->{enable}) {
} 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/^(nf_conntrack_helpers):\s*(((\S+)[,]?)+)\s*$/i) {
+ $opt = lc($1);
+ $value = lc($2);
+ pve_fw_verify_conntrack_helper($value);
} elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established|nf_conntrack_tcp_timeout_syn_recv|protection_synflood_rate|protection_synflood_burst|protection_limit):\s*(\d+)\s*$/i) {
$opt = lc($1);
$value = int($2);
my ($line) = @_;
# we can add single line comments to the end of the line
- my $comment = decode('utf8', $1) if $line =~ s/\s*#\s*(.*?)\s*$//;
+ my $comment = $line =~ s/\s*#\s*(.*?)\s*$// ? decode('utf8', $1) : undef;
if ($line =~ m/^(\S+)\s(\S+)$/) {
my ($name, $cidr) = ($1, $2);
push @{$res->{$section}->{$group}}, $rule;
} elsif ($section eq 'ipset') {
# we can add single line comments to the end of the rule
- my $comment = decode('utf8', $1) if $line =~ s/#\s*(.*?)\s*$//;
+ my $comment = $line =~ s/#\s*(.*?)\s*$// ? decode('utf8', $1) : undef;
$line =~ m/^(\!)?\s*(\S+)\s*$/;
my $nomatch = $1;
my $hostfw_options = $hostfw_conf->{options} || {};
my $protection_synflood = $hostfw_options->{protection_synflood} || 0;
+ my $conntrack_helpers = $hostfw_options->{nf_conntrack_helpers} || '';
+
+ ruleset_create_chain($ruleset, "PVEFW-PREROUTING") if $protection_synflood != 0 || $conntrack_helpers ne '';
if($protection_synflood) {
$protection_synflood_expire = $protection_synflood_expire * 1000;
my $protection_synflood_mask = $ipversion == 4 ? 32 : 64;
- ruleset_create_chain($ruleset, "PVEFW-PREROUTING");
ruleset_addrule($ruleset, "PVEFW-PREROUTING", "-p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m hashlimit --hashlimit-above $protection_synflood_rate/sec --hashlimit-burst $protection_synflood_burst --hashlimit-mode srcip --hashlimit-name syn --hashlimit-htable-size 2097152 --hashlimit-srcmask $protection_synflood_mask --hashlimit-htable-expire $protection_synflood_expire", "-j DROP");
}
+ foreach my $conntrack_helper (split(/,/, $conntrack_helpers)) {
+ my $helper = $pve_fw_helpers->{$conntrack_helper};
+ ruleset_addrule($ruleset, "PVEFW-PREROUTING", "-p $helper->{proto} -m $helper->{proto} --dport $helper->{dport} -j CT", "--helper $conntrack_helper") if $helper && $helper->{"v$ipversion"};
+ }
+
return $ruleset;
}