use PVE::Tools qw($IPV4RE $IPV6RE);
use PVE::Tools qw(run_command lock_file dir_glob_foreach);
+use PVE::Firewall::Helpers;
+
my $pvefw_conf_dir = "/etc/pve/firewall";
my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
my ($cidr, $noerr) = @_;
if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(/(\d+))?$!) {
- return $cidr if Net::IP->new($cidr);
+ # 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',
{ action => 'PARAM', proto => 'udp', dport => '6881' },
],
'Ceph' => [
- "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Deamons)",
+ "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)",
# Legacy port for protocol v1
{ action => 'PARAM', proto => 'tcp', dport => '6789' },
# New port for protocol v2
{ action => 'PARAM', proto => 'tcp', dport => '9418' },
],
'HKP' => [
- "OpenPGP HTTP keyserver protocol traffic",
+ "OpenPGP HTTP key server protocol traffic",
{ action => 'PARAM', proto => 'tcp', dport => '11371' },
],
'HTTP' => [
{ action => 'PARAM', proto => '41' },
{ action => 'PARAM', proto => 'udp', dport => '5072,8374' },
],
+ 'SPICEproxy' => [
+ "Proxmox VE SPICE display proxy traffic",
+ { action => 'PARAM', proto => 'tcp', dport => '3128' },
+ ],
'Squid' => [
"Squid web proxy traffic",
{ action => 'PARAM', proto => 'tcp', dport => '3128' },
# same as shorewall 'Drop', which is equal to DROP,
# but REJECT/DROP some packages to reduce logging,
# and ACCEPT critical ICMP types
- { action => 'PVEFW-reject', proto => 'tcp', dport => '43' }, # REJECT 'auth'
# we are not interested in BROADCAST/MULTICAST/ANYCAST
{ action => 'PVEFW-DropBroadcast' },
# ACCEPT critical ICMP types
# same as shorewall 'Reject', which is equal to Reject,
# but REJECT/DROP some packages to reduce logging,
# and ACCEPT critical ICMP types
- { action => 'PVEFW-reject', proto => 'tcp', dport => '43' }, # REJECT 'auth'
# we are not interested in BROADCAST/MULTICAST/ANYCAST
{ action => 'PVEFW-DropBroadcast' },
# ACCEPT critical ICMP types
],
'PVEFW-tcpflags' => [
# same as shorewall tcpflags action.
- # Packets arriving on this interface are checked for som illegal combinations of TCP flags
+ # Packets arriving on this interface are checked for some illegal combinations of TCP flags
{ match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' },
{ match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' },
{ match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' },
],
'PVEFW-tcpflags' => [
# same as shorewall tcpflags action.
- # Packets arriving on this interface are checked for som illegal combinations of TCP flags
+ # Packets arriving on this interface are checked for some illegal combinations of TCP flags
{ match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' },
{ match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' },
{ match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' },
return $proto;
}
+PVE::JSONSchema::register_format('pve-fw-icmp-type-spec', \&pve_fw_verify_icmp_type_spec);
+sub pve_fw_verify_icmp_type_spec {
+ my ($icmp_type) = @_;
+
+ if ($icmp_type_names->{$icmp_type} || $icmpv6_type_names->{$icmp_type}) {
+ return $icmp_type;
+ }
+
+ die "invalid icmp-type value '$icmp_type'\n" if $icmp_type ne '';
+
+ return $icmp_type;
+}
+
# helper function for API
type => 'integer',
minimum => 0,
optional => 1,
- description => 'Inital burst of packages which will get logged',
+ description => 'Initial burst of packages which will always get logged before the rate is applied',
default => 5,
},
},
macfilter => {
description => "Enable/disable MAC address filter.",
type => 'boolean',
- default => 0,
+ default => 1,
optional => 1,
},
dhcp => {
description => "Restrict packet source address. $addr_list_descr",
type => 'string', format => 'pve-fw-addr-spec',
optional => 1,
+ maxLength => 512,
},
dest => {
description => "Restrict packet destination address. $addr_list_descr",
type => 'string', format => 'pve-fw-addr-spec',
optional => 1,
+ maxLength => 512,
},
proto => {
description => "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.",
type => 'string',
optional => 1,
},
+ 'icmp-type' => {
+ description => "Specify icmp-type. Only valid if proto equals 'icmp'.",
+ type => 'string', format => 'pve-fw-icmp-type-spec',
+ optional => 1,
+ },
};
sub add_rule_properties {
my $set_ip_version = sub {
my $vers = shift;
if ($vers) {
- die "detected mixed ipv4/ipv6 adresses in rule\n"
+ die "detected mixed ipv4/ipv6 addresses in rule\n"
if $ipversion && ($vers != $ipversion);
$ipversion = $vers;
}
eval { pve_fw_verify_protocol_spec($rule->{proto}); };
&$add_error('proto', $@) if $@;
&$set_ip_version(4) if $rule->{proto} eq 'icmp';
- &$set_ip_version(6) if $rule->{proto} eq 'icmpv6';
+ &$set_ip_version(6) if $rule->{proto} eq 'icmpv6';
+ &$set_ip_version(6) if $rule->{proto} eq 'ipv6-icmp';
}
if ($rule->{dport}) {
$proto ne 'icmp' && $proto ne 'icmpv6'; # special cases
}
+ if (my $icmp_type = $rule ->{'icmp-type'}) {
+ my $proto = $rule->{proto};
+ &$add_error('proto', "missing property - 'icmp-type' requires this property")
+ if $proto ne 'icmp' && $proto ne 'icmpv6' && $proto ne 'ipv6-icmp';
+ &$add_error('icmp-type', "'icmp-type' cannot be specified together with 'dport'")
+ if $rule->{dport};
+ if ($proto eq 'icmp' && !$icmp_type_names->{$icmp_type}) {
+ &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto 'icmp'");
+ } elsif (($proto eq 'icmpv6' || $proto eq 'ipv6-icmp') && !$icmpv6_type_names->{$icmp_type}) {
+ &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto '$proto'");
+ }
+ }
+
if ($rule->{sport}) {
eval { parse_port_name_number_or_range($rule->{sport}, 0); };
&$add_error('sport', $@) if $@;
}
# core functions
-my $bridge_firewall_enabled = 0;
sub enable_bridge_firewall {
- return if $bridge_firewall_enabled; # only once
PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1");
PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1");
# make sure syncookies are enabled (which is default on newer 3.X kernels anyways)
PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1");
- $bridge_firewall_enabled = 1;
}
sub iptables_restore_cmdlist {
return if $line =~ m/^\s*$/;
if ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) {
my $chain = $1;
+ # ignore initval from ipset v7.7+, won't set that yet so it'd mess up change detection
+ $line =~ s/\binitval 0x[0-9a-f]+//;
$line =~ s/\s+$//; # delete trailing white space
push @{$chains->{$chain}}, $line;
} else {
my $res = {};
my $chains = {};
+ my $table;
my $parser = sub {
my $line = shift;
return if $line =~ m/^#/;
return if $line =~ m/^\s*$/;
- if ($line =~ m/^:(\S+)\s\S+$/) {
+ if ($line =~ m/^\*(\S+)$/) {
+ $table = $1;
+ return;
+ }
+
+ return if $table ne "filter";
+
+ if ($line =~ m/^:(\S+)\s(ACCEPT|DROP|RETURN)$/) {
# Make sure we know chains exist even if they're empty.
$chains->{$1} //= [];
+ $res->{$1}->{policy} = $2;
} elsif ($line =~ m/^(?:\S+)\s(\S+)\s(?:\S+).*/) {
my $chain = $1;
$line =~ s/\s+$//;
return $res;
}
-# substitude action of rule according to action hash
+# substitute action of rule according to action hash
sub rule_substitude_action {
my ($rule, $actions) = @_;
}
};
+ my $add_icmp_type = sub {
+ return if !defined($rule->{'icmp-type'}) || $rule->{'icmp-type'} eq '';
+
+ die "'icmp-type' can only be set if 'icmp', 'icmpv6' or 'ipv6-icmp' is specified\n"
+ if ($proto ne 'icmp') && ($proto ne 'icmpv6') && ($proto ne 'ipv6-icmp');
+ my $type = $proto eq 'icmp' ? 'icmp-type' : 'icmpv6-type';
+
+ push @match, "-m $proto --$type $rule->{'icmp-type'}";
+ };
+
# order matters - single port before multiport!
+ $add_icmp_type->();
$add_dport->() if $multisport;
$add_sport->();
$add_dport->() if !$multisport;
last if $rule->{type} eq 'group';
- if ($line =~ s/^-p (\S+)\s*//) {
+ if ($line =~ s/^(?:-p|--?proto) (\S+)\s*//) {
$rule->{proto} = $1;
next;
}
- if ($line =~ s/^-dport (\S+)\s*//) {
+ if ($line =~ s/^--?dport (\S+)\s*//) {
$rule->{dport} = $1;
next;
}
- if ($line =~ s/^-sport (\S+)\s*//) {
+ if ($line =~ s/^--?sport (\S+)\s*//) {
$rule->{sport} = $1;
next;
}
- if ($line =~ s/^-source (\S+)\s*//) {
+ if ($line =~ s/^--?source (\S+)\s*//) {
$rule->{source} = $1;
next;
}
- if ($line =~ s/^-dest (\S+)\s*//) {
+ if ($line =~ s/^--?dest (\S+)\s*//) {
$rule->{dest} = $1;
next;
}
- if ($line =~ s/^-log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) {
+ if ($line =~ s/^--?log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) {
$rule->{log} = $1;
next;
}
+ if ($line =~ s/^--?icmp-type (\S+)\s*//) {
+ $rule->{'icmp-type'} = $1;
+ next;
+ }
last;
}
}
}
} elsif ($d->{type} eq 'lxc') {
- if ($have_lxc) {
- my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
- if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
- $lxc->{$vmid} = $conf;
- }
- }
- }
+ if ($have_lxc) {
+ my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
+ if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
+ $lxc->{$vmid} = $conf;
+ }
+ }
+ }
}
return $vmdata;
};
+# FIXME: move use sites over to moved helper and break older packages, then remove this here
sub lock_vmfw_conf {
- my ($vmid, $timeout, $code, @param) = @_;
-
- die "can't lock VM firewall config for undefined VMID\n"
- if !defined($vmid);
-
- my $res = PVE::Cluster::cfs_lock_firewall("vm-$vmid", $timeout, $code, @param);
- die $@ if $@;
-
- return $res;
+ return PVE::Firewall::Helpers::lock_vmfw_conf(@_);
}
sub load_vmfw_conf {
$raw .= " -dport $rule->{dport}" if $rule->{dport};
$raw .= " -sport $rule->{sport}" if $rule->{sport};
$raw .= " -log $rule->{log}" if $rule->{log};
+ $raw .= " -icmp-type $rule->{'icmp-type'}" if defined($rule->{'icmp-type'}) && $rule->{'icmp-type'} ne '';
}
$raw .= " # " . encode('utf8', $rule->{comment})
}
}
+# FIXME: remove with 8.0 and break older qemu-server/pve-container
sub remove_vmfw_conf {
- my ($vmid) = @_;
-
- my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw";
-
- unlink $vmfw_conffile;
+ return PVE::Firewall::Helpers::remove_vmfw_conf(@_);
}
+# FIXME: remove with 8.0 and break older qemu-server/pve-container
sub clone_vmfw_conf {
- my ($vmid, $newid) = @_;
-
- my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw";
- my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw";
-
- lock_vmfw_conf($newid, 10, sub {
- if (-f $clonevm_conffile) {
- unlink $clonevm_conffile;
- }
- if (-f $sourcevm_conffile) {
- my $data = PVE::Tools::file_get_contents($sourcevm_conffile);
- PVE::Tools::file_set_contents($clonevm_conffile, $data);
- }
- });
+ return PVE::Firewall::Helpers::clone_vmfw_conf(@_);
}
sub read_vm_firewall_configs {
$hashsize = round_powerof2($hashsize);
}
+ my $bucketsize = 12; # lower than the default of 14, faster but slightly more memory use
+
my $family = $ipversion == "6" ? "inet6" : "inet";
- $ipset_ruleset->{$name} = ["create $name hash:net family $family hashsize $hashsize maxelem $hashsize"];
+ $ipset_ruleset->{$name} = [
+ "create $name hash:net family $family hashsize $hashsize maxelem $hashsize bucketsize $bucketsize"
+ ];
foreach my $cidr (sort keys %$data) {
my $entry = $data->{$cidr};
}
}
-sub lock_hostfw_conf {
- my ($timeout, $code, @param) = @_;
+sub lock_hostfw_conf : prototype($$$@) {
+ my ($node, $timeout, $code, @param) = @_;
+
+ $node = $nodename if !defined($node);
- my $res = PVE::Cluster::cfs_lock_firewall("host-$nodename", $timeout, $code, @param);
+ my $res = PVE::Cluster::cfs_lock_firewall("host-$node", $timeout, $code, @param);
die $@ if $@;
return $res;
}
sub save_hostfw_conf {
- my ($hostfw_conf) = @_;
+ my ($hostfw_conf, $filename) = @_;
+
+ $filename = $hostfw_conf_filename if !defined($filename);
my $raw = '';
}
if ($raw) {
- PVE::Tools::file_set_contents($hostfw_conf_filename, $raw);
+ PVE::Tools::file_set_contents($filename, $raw);
} else {
- unlink $hostfw_conf_filename;
+ unlink $filename;
}
}
return if !$vmfw_conf;
# When the 'ipfilter' option is enabled every device for which there
- # is no 'ipfilter-netX' ipset defiend gets an implicit empty default
+ # is no 'ipfilter-netX' ipset defined gets an implicit empty default
# ipset.
# The reason is that ipfilter ipsets are always filled with standard
# IPv6 link-local filters.
return if !$vmfw_conf;
# When the 'ipfilter' option is enabled every device for which there
- # is no 'ipfilter-netX' ipset defiend gets an implicit empty default
+ # is no 'ipfilter-netX' ipset defined gets an implicit empty default
# ipset.
# The reason is that ipfilter ipsets are always filled with standard
# IPv6 link-local filters, as well as the IP addresses configured
eval {
my $conf = $vmdata->{qemu}->{$vmid};
my $vmfw_conf = $vmfw_configs->{$vmid};
- return if !$vmfw_conf;
+ return if !$vmfw_conf || !$vmfw_conf->{options}->{enable};
my $ipsets = $vmfw_conf->{ipset};
foreach my $netid (sort keys %$conf) {
ruleset_create_chain($ruleset, $tapchain);
if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
- ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP');
+ ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP');
}
if (@$arpfilter){
if (defined($change_only_regex)) {
$action = 'ignore' if ($chain !~ m/$change_only_regex/);
$statushash->{$chain}->{rules} = $active_chains->{$chain}->{rules};
+ $statushash->{$chain}->{policy} = $active_chains->{$chain}->{policy};
$sig = $sig->{sig};
}
$statushash->{$chain}->{action} = $action;
my $pve_include = 0;
foreach my $chain (sort keys %$statushash) {
next if ($statushash->{$chain}->{action} eq 'delete');
- $cmdlist .= ":$chain ACCEPT\n";
+ my $policy = $statushash->{$chain}->{policy} // 'ACCEPT';
+ $cmdlist .= ":$chain $policy\n";
$pve_include = 1 if ($chain eq 'PVEFW-FORWARD');
}