my $pvefw_conf_dir = "/etc/pve/firewall";
my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
-# dynamically include PVE::QemuServer and PVE::OpenVZ
+# dynamically include PVE::QemuServer and PVE::LXC
# to avoid dependency problems
my $have_qemu_server;
eval {
require PVE::QemuServer;
+ require PVE::QemuConfig;
$have_qemu_server = 1;
};
-my $have_pve_manager;
-eval {
- require PVE::OpenVZ;
- $have_pve_manager = 1;
-};
-
my $have_lxc;
eval {
require PVE::LXC;
],
'NeighborDiscovery' => [
"IPv6 neighbor solicitation, neighbor and router advertisement",
+ { action => 'PARAM', proto => 'icmpv6', dport => 'router-solicitation' },
{ action => 'PARAM', proto => 'icmpv6', dport => 'router-advertisement' },
{ action => 'PARAM', proto => 'icmpv6', dport => 'neighbor-solicitation' },
{ action => 'PARAM', proto => 'icmpv6', dport => 'neighbor-advertisement' },
],
+ 'DHCPv6' => [
+ { action => 'PARAM', proto => 'udp', dport => '546:547', sport => '546:547' },
+ ],
'Trcrt' => [
{ action => 'PARAM', proto => 'udp', dport => '33434:33524' },
{ action => 'PARAM', proto => 'icmpv6', dport => 'echo-request' },
'PVEFW-smurfs' => [
# same as shorewall smurfs action
# Filter packets for smurfs (packets with a broadcast address as the source).
- "-s 0.0.0.0/32 -j RETURN",
+ "-s 0.0.0.0/32 -j RETURN", # allow DHCP
"-m addrtype --src-type BROADCAST -g PVEFW-smurflog",
"-s 224.0.0.0/4 -g PVEFW-smurflog",
],
}
sub parse_port_name_number_or_range {
- my ($str) = @_;
+ my ($str, $dport) = @_;
my $services = PVE::Firewall::get_etc_services();
my $count = 0;
my $port = $1;
die "invalid port '$port'\n" if $port > 65535;
} else {
- if ($icmp_type_names->{$item}) {
+ if ($dport && $icmp_type_names->{$item}) {
$icmp_port = 1;
- } elsif ($icmpv6_type_names->{$item}) {
+ } elsif ($dport && $icmpv6_type_names->{$item}) {
$icmp_port = 1;
} else {
die "invalid port '$item'\n" if !$services->{byname}->{$item};
return $count;
}
-PVE::JSONSchema::register_format('pve-fw-port-spec', \&pve_fw_verify_port_spec);
-sub pve_fw_verify_port_spec {
+PVE::JSONSchema::register_format('pve-fw-sport-spec', \&pve_fw_verify_sport_spec);
+sub pve_fw_verify_sport_spec {
+ my ($portstr) = @_;
+
+ parse_port_name_number_or_range($portstr, 0);
+
+ return $portstr;
+}
+
+PVE::JSONSchema::register_format('pve-fw-dport-spec', \&pve_fw_verify_dport_spec);
+sub pve_fw_verify_dport_spec {
my ($portstr) = @_;
- parse_port_name_number_or_range($portstr);
+ parse_port_name_number_or_range($portstr, 1);
return $portstr;
}
next if !defined($v);
$data->{$k} = $v;
# Note: digest ignores refs ($rule->{errors})
- $sha->add($k, ':', $v, "\n") if !ref($v); ;
+ # since Digest::SHA expects a series of bytes,
+ # we have to encode the value here to prevent errors when
+ # using utf8 characters (eg. in comments)
+ $sha->add($k, ':', encode_utf8($v), "\n") if !ref($v); ;
}
push @$res, $data;
}
return wantarray ? ($res, $digest) : $res;
}
+
+my $addr_list_descr = "This can refer to a single IP address, an IP set ('+ipsetname') or an IP alias definition. You can also specify an address range like '20.34.101.207-201.3.9.99', or a list of IP addresses and networks (entries are separated by comma). Please do not mix IPv4 and IPv6 addresses inside such lists.";
+
+my $port_descr = "You can use service names or simple numbers (0-65535), as defined in '/etc/services'. Port ranges can be specified with '\\d+:\\d+', for example '80:85', and you can use comma separated list to match several ports or ranges.";
+
my $rule_properties = {
pos => {
description => "Update rule at position <pos>.",
optional => 1,
maxLength => 128,
},
- iface => get_standard_option('pve-iface', { optional => 1 }),
+ iface => get_standard_option('pve-iface', {
+ description => "Network interface name. You have to use network configuration key names for VMs and containers ('net\\d+'). Host related rules can use arbitrary strings.",
+ optional => 1
+ }),
source => {
+ description => "Restrict packet source address. $addr_list_descr",
type => 'string', format => 'pve-fw-addr-spec',
optional => 1,
},
dest => {
+ description => "Restrict packet destination address. $addr_list_descr",
type => 'string', format => 'pve-fw-addr-spec',
optional => 1,
},
proto => {
+ description => "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.",
type => 'string', format => 'pve-fw-protocol-spec',
optional => 1,
},
optional => 1,
},
sport => {
- type => 'string', format => 'pve-fw-port-spec',
+ description => "Restrict TCP/UDP source port. $port_descr",
+ type => 'string', format => 'pve-fw-sport-spec',
optional => 1,
},
dport => {
- type => 'string', format => 'pve-fw-port-spec',
+ description => "Restrict TCP/UDP destination port. $port_descr",
+ type => 'string', format => 'pve-fw-dport-spec',
optional => 1,
},
comment => {
if !$allow_iface;
eval { PVE::JSONSchema::pve_verify_iface($rule->{iface}); };
&$add_error('iface', $@) if $@;
- if ($rule_env eq 'vm') {
+ if ($rule_env eq 'vm' || $rule_env eq 'ct') {
&$add_error('iface', "value does not match the regex pattern 'net\\d+'")
if $rule->{iface} !~ m/^net(\d+)$/;
- } elsif ($rule_env eq 'ct') {
- &$add_error('iface', "value does not match the regex pattern '(venet|eth\\d+)'")
- if $rule->{iface} !~ m/^(venet|eth(\d+))$/;
}
}
}
if ($rule->{dport}) {
- eval { parse_port_name_number_or_range($rule->{dport}); };
+ eval { parse_port_name_number_or_range($rule->{dport}, 1); };
&$add_error('dport', $@) if $@;
&$add_error('proto', "missing property - 'dport' requires this property")
if !$rule->{proto};
}
if ($rule->{sport}) {
- eval { parse_port_name_number_or_range($rule->{sport}); };
+ eval { parse_port_name_number_or_range($rule->{sport}, 0); };
&$add_error('sport', $@) if $@;
&$add_error('proto', "missing property - 'sport' requires this property")
if !$rule->{proto};
return 1 if $name =~ m/^PVEFW-\S+$/;
- return 1 if $name =~ m/^tap\d+i\d+-(:?IN|OUT)$/;
+ return 1 if $name =~ m/^tap\d+i\d+-(?:IN|OUT)$/;
- return 1 if $name =~ m/^veth\d+.\d+-(:?IN|OUT)$/; # fixme: dev name is configurable
+ return 1 if $name =~ m/^veth\d+i\d+-(?:IN|OUT)$/;
- return 1 if $name =~ m/^venet0-\d+-(:?IN|OUT)$/;
-
- return 1 if $name =~ m/^fwbr\d+(v\d+)?-(:?FW|IN|OUT|IPS)$/;
- return 1 if $name =~ m/^GROUP-(:?[^\s\-]+)-(:?IN|OUT)$/;
+ return 1 if $name =~ m/^fwbr\d+(v\d+)?-(?:FW|IN|OUT|IPS)$/;
+ return 1 if $name =~ m/^GROUP-(?:$security_group_name_pattern)-(?:IN|OUT)$/;
return undef;
};
die "unable to emit macro - internal error" if $rule->{macro}; # should not happen
- my $nbdport = defined($rule->{dport}) ? parse_port_name_number_or_range($rule->{dport}) : 0;
- my $nbsport = defined($rule->{sport}) ? parse_port_name_number_or_range($rule->{sport}) : 0;
+ my $nbdport = defined($rule->{dport}) ? parse_port_name_number_or_range($rule->{dport}, 1) : 0;
+ my $nbsport = defined($rule->{sport}) ? parse_port_name_number_or_range($rule->{sport}, 0) : 0;
my @cmd = ();
if ($rule->{dport}) {
if ($rule->{proto} && $rule->{proto} eq 'icmp') {
# Note: we use dport to store --icmp-type
- die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
+ die "unknown icmp-type '$rule->{dport}'\n"
+ if $rule->{dport} !~ /^\d+$/ && !defined($icmp_type_names->{$rule->{dport}});
push @cmd, "-m icmp --icmp-type $rule->{dport}";
} elsif ($rule->{proto} && $rule->{proto} eq 'icmpv6') {
# Note: we use dport to store --icmpv6-type
- die "unknown icmpv6-type '$rule->{dport}'\n" if !defined($icmpv6_type_names->{$rule->{dport}});
+ die "unknown icmpv6-type '$rule->{dport}'\n"
+ if $rule->{dport} !~ /^\d+$/ && !defined($icmpv6_type_names->{$rule->{dport}});
push @cmd, "-m icmpv6 --icmpv6-type $rule->{dport}";
} else {
if ($nbdport > 1) {
}
}
+sub ruleset_chain_add_ndp {
+ my ($ruleset, $chain, $ipversion, $options, $direction, $accept) = @_;
+ return if $ipversion != 6 || (defined($options->{ndp}) && !$options->{ndp});
+
+ ruleset_addrule($ruleset, $chain, "-p icmpv6 --icmpv6-type router-solicitation $accept");
+ if ($direction ne 'OUT' || $options->{radv}) {
+ ruleset_addrule($ruleset, $chain, "-p icmpv6 --icmpv6-type router-advertisement $accept");
+ }
+ ruleset_addrule($ruleset, $chain, "-p icmpv6 --icmpv6-type neighbor-solicitation $accept");
+ ruleset_addrule($ruleset, $chain, "-p icmpv6 --icmpv6-type neighbor-advertisement $accept");
+}
+
sub ruleset_chain_add_conn_filters {
my ($ruleset, $chain, $accept) = @_;
my $accept = generate_nfqueue($options);
if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
- if ($direction eq 'OUT') {
- ruleset_generate_rule($ruleset, $chain, $ipversion,
- { action => 'PVEFW-SET-ACCEPT-MARK',
- proto => 'udp', sport => 68, dport => 67 });
- } else {
- ruleset_generate_rule($ruleset, $chain, $ipversion,
- { action => 'ACCEPT',
- proto => 'udp', sport => 67, dport => 68 });
+ if ($ipversion == 4) {
+ if ($direction eq 'OUT') {
+ ruleset_generate_rule($ruleset, $chain, $ipversion,
+ { action => 'PVEFW-SET-ACCEPT-MARK',
+ proto => 'udp', sport => 68, dport => 67 });
+ } else {
+ ruleset_generate_rule($ruleset, $chain, $ipversion,
+ { action => 'ACCEPT',
+ proto => 'udp', sport => 67, dport => 68 });
+ }
+ } elsif ($ipversion == 6) {
+ if ($direction eq 'OUT') {
+ ruleset_generate_rule($ruleset, $chain, $ipversion,
+ { action => 'PVEFW-SET-ACCEPT-MARK',
+ proto => 'udp', sport => 546, dport => 547 });
+ } else {
+ ruleset_generate_rule($ruleset, $chain, $ipversion,
+ { action => 'ACCEPT',
+ proto => 'udp', sport => 547, dport => 546 });
+ }
}
+
}
if ($direction eq 'OUT') {
if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
ruleset_addrule($ruleset, $chain, "-m mac ! --mac-source $macaddr -j DROP");
}
+ if ($ipversion == 6 && !$options->{radv}) {
+ ruleset_addrule($ruleset, $chain, '-p icmpv6 --icmpv6-type router-advertisement -j DROP');
+ }
if ($ipfilter_ipset) {
ruleset_addrule($ruleset, $chain, "-m set ! --match-set $ipfilter_ipset src -j DROP");
}
ruleset_addrule($ruleset, $chain, "-j MARK --set-mark 0"); # clear mark
}
+
+ my $accept_action = $direction eq 'OUT' ? '-g PVEFW-SET-ACCEPT-MARK' : "-j $accept";
+ ruleset_chain_add_ndp($ruleset, $chain, $ipversion, $options, $direction, $accept_action);
}
sub ruleset_add_group_rule {
}
}
-sub generate_venet_rules_direction {
- my ($ruleset, $cluster_conf, $vmfw_conf, $vmid, $ip, $direction, $ipversion) = @_;
-
- my $lc_direction = lc($direction);
-
- my $rules = $vmfw_conf->{rules};
-
- my $options = $vmfw_conf->{options};
- my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
-
- my $chain = "venet0-$vmid-$direction";
-
- ruleset_create_vm_chain($ruleset, $chain, $ipversion, $options, undef, undef, $direction);
-
- ruleset_generate_vm_rules($ruleset, $rules, $cluster_conf, $vmfw_conf, $chain, 'venet', $direction, undef, $ipversion);
-
- # implement policy
- my $policy;
-
- if ($direction eq 'OUT') {
- $policy = $options->{policy_out} || 'ACCEPT'; # allow everything by default
- } else {
- $policy = $options->{policy_in} || 'DROP'; # allow nothing by default
- }
-
- my $accept = generate_nfqueue($options);
- my $accept_action = $direction eq 'OUT' ? "PVEFW-SET-ACCEPT-MARK" : $accept;
- ruleset_add_chain_policy($ruleset, $chain, $ipversion, $vmid, $policy, $loglevel, $accept_action);
-
- if ($direction eq 'OUT') {
- ruleset_generate_rule_insert($ruleset, "PVEFW-VENET-OUT", $ipversion, {
- action => $chain,
- source => $ip,
- iface_in => 'venet0'});
- } else {
- ruleset_generate_rule($ruleset, "PVEFW-VENET-IN", $ipversion, {
- action => $chain,
- dest => $ip,
- iface_out => 'venet0'});
- }
-}
-
sub generate_tap_rules_direction {
my ($ruleset, $cluster_conf, $iface, $netid, $macaddr, $vmfw_conf, $vmid, $direction, $ipversion) = @_;
my $ipfilter_name = compute_ipfilter_ipset_name($netid);
my $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion)
- if $vmfw_conf->{ipset}->{$ipfilter_name};
+ if $options->{ipfilter} || $vmfw_conf->{ipset}->{$ipfilter_name};
# create chain with mac and ip filter
ruleset_create_vm_chain($ruleset, $tapchain, $ipversion, $options, $macaddr, $ipfilter_ipset, $direction);
ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
ruleset_chain_add_conn_filters($ruleset, $chain, 'ACCEPT');
+ ruleset_chain_add_ndp($ruleset, $chain, $ipversion, $options, 'IN', '-j RETURN');
ruleset_chain_add_input_filters($ruleset, $chain, $ipversion, $options, $cluster_conf, $loglevel);
# we use RETURN because we need to check also tap rules
# we use RETURN because we may want to check other thigs later
$accept_action = 'RETURN';
+ ruleset_chain_add_ndp($ruleset, $chain, $ipversion, $options, 'OUT', "-j $accept_action");
ruleset_addrule($ruleset, $chain, "-p igmp -j $accept_action"); # important for multicast
my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
- if ($line =~ m/^(enable|dhcp|macfilter|ips):\s*(0|1)\s*$/i) {
+ if ($line =~ m/^(enable|dhcp|ndp|radv|macfilter|ipfilter|ips):\s*(0|1)\s*$/i) {
$opt = lc($1);
$value = int($2);
} elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
- if ($line =~ m/^(enable|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
+ if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp):\s*(0|1)\s*$/i) {
$opt = lc($1);
$value = int($2);
} elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
sub read_local_vm_config {
- my $openvz = {};
my $qemu = {};
my $lxc = {};
- my $vmdata = { openvz => $openvz, qemu => $qemu, lxc => $lxc };
+ my $vmdata = { qemu => $qemu, lxc => $lxc };
my $vmlist = PVE::Cluster::get_vmlist();
return $vmdata if !$vmlist || !$vmlist->{ids};
my $d = $ids->{$vmid};
next if !$d->{node} || $d->{node} ne $nodename;
next if !$d->{type};
- if ($d->{type} eq 'openvz') {
- if ($have_pve_manager) {
- my $cfspath = PVE::OpenVZ::cfs_config_path($vmid);
- if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
- $openvz->{$vmid} = $conf;
- }
- }
- } elsif ($d->{type} eq 'qemu') {
+ if ($d->{type} eq 'qemu') {
if ($have_qemu_server) {
- my $cfspath = PVE::QemuServer::cfs_config_path($vmid);
+ my $cfspath = PVE::QemuConfig->cfs_config_path($vmid);
if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
$qemu->{$vmid} = $conf;
}
}
} elsif ($d->{type} eq 'lxc') {
if ($have_lxc) {
- my $cfspath = PVE::LXC::cfs_config_path($vmid);
+ my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
$lxc->{$vmid} = $conf;
}
$raw .= "\n";
}
- mkdir $pvefw_conf_dir;
-
my $filename = "$pvefw_conf_dir/$vmid.fw";
- PVE::Tools::file_set_contents($filename, $raw);
+ if ($raw) {
+ mkdir $pvefw_conf_dir;
+ PVE::Tools::file_set_contents($filename, $raw);
+ } else {
+ unlink $filename;
+ }
+}
+
+sub remove_vmfw_conf {
+ my ($vmid) = @_;
+
+ my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw";
+
+ unlink $vmfw_conffile;
+}
+
+sub clone_vmfw_conf {
+ my ($vmid, $newid) = @_;
+
+ my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw";
+ my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw";
+
+ 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);
+ }
}
sub read_vm_firewall_configs {
next if !$vmfw_conf->{options}; # skip if file does not exists
$vmfw_configs->{$vmid} = $vmfw_conf;
}
- foreach my $vmid (keys %{$vmdata->{openvz}}) {
- my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir, $verbose);
- next if !$vmfw_conf->{options}; # skip if file does not exists
- $vmfw_configs->{$vmid} = $vmfw_conf;
- }
foreach my $vmid (keys %{$vmdata->{lxc}}) {
my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir, $verbose);
next if !$vmfw_conf->{options}; # skip if file does not exists
}
sub generate_ipset_chains {
- my ($ipset_ruleset, $clusterfw_conf, $fw_conf) = @_; #fixme
+ my ($ipset_ruleset, $clusterfw_conf, $fw_conf, $device_ips, $ipsets) = @_;
- foreach my $ipset (keys %{$fw_conf->{ipset}}) {
+ foreach my $ipset (keys %{$ipsets}) {
- my $options = $fw_conf->{ipset}->{$ipset};
+ my $options = $ipsets->{$ipset};
+
+ if ($device_ips && $ipset =~ /^ipfilter-(net\d+)$/) {
+ if (my $ips = $device_ips->{$1}) {
+ $options = [@$options, @$ips];
+ }
+ }
# remove duplicates
my $nethash = {};
}
#http://backreference.org/2013/03/01/ipv6-address-normalization/
if ($ver == 6) {
- $cidr = lc(Net::IP::ip_compress_address($cidr, 6));
+ # ip_compress_address takes an address only, no CIDR
+ my ($addr, $prefix_len) = ($cidr =~ m@^([^/]*)(/.*)?$@);
+ $cidr = lc(Net::IP::ip_compress_address($addr, 6));
+ $cidr .= $prefix_len if defined($prefix_len);
$cidr =~ s|/128$||;
} else {
$cidr =~ s|/32$||;
}
}
- mkdir $pvefw_conf_dir;
- PVE::Tools::file_set_contents($clusterfw_conf_filename, $raw);
+ if ($raw) {
+ mkdir $pvefw_conf_dir;
+ PVE::Tools::file_set_contents($clusterfw_conf_filename, $raw);
+ } else {
+ unlink $clusterfw_conf_filename;
+ }
}
sub load_hostfw_conf {
$raw .= "\n";
}
- PVE::Tools::file_set_contents($hostfw_conf_filename, $raw);
+ if ($raw) {
+ PVE::Tools::file_set_contents($hostfw_conf_filename, $raw);
+ } else {
+ unlink $hostfw_conf_filename;
+ }
}
sub compile {
$vmfw_configs = read_vm_firewall_configs($cluster_conf, $vmdata, undef, $verbose);
}
- my ($ruleset, $ipset_ruleset) = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4, $verbose);
- my ($rulesetv6) = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6, $verbose);
-
- return ($ruleset, $ipset_ruleset, $rulesetv6);
-}
-
-sub compile_iptables_filter {
- my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion, $verbose) = @_;
-
- $cluster_conf->{ipset}->{venet0} = [];
- my $venet0_ipset_chain = compute_ipset_chain_name(0, 'venet0', $ipversion);
+ return ({},{},{}) if !$cluster_conf->{options}->{enable};
my $localnet;
if ($cluster_conf->{aliases}->{local_network}) {
push @{$cluster_conf->{ipset}->{management}}, { cidr => $localnet };
- return ({}, {}) if !$cluster_conf->{options}->{enable};
+ my $ruleset = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 4, $verbose);
+ my $rulesetv6 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, 6, $verbose);
+ my $ipset_ruleset = compile_ipsets($cluster_conf, $vmfw_configs, $vmdata);
+
+ return ($ruleset, $ipset_ruleset, $rulesetv6);
+}
+
+sub compile_iptables_filter {
+ my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $ipversion, $verbose) = @_;
my $ruleset = {};
ruleset_chain_add_conn_filters($ruleset, "PVEFW-FORWARD", "ACCEPT");
-
- ruleset_create_chain($ruleset, "PVEFW-VENET-OUT");
- ruleset_addrule($ruleset, "PVEFW-FORWARD", "-i venet0 -m set --match-set ${venet0_ipset_chain} src -j PVEFW-VENET-OUT");
- ruleset_addrule($ruleset, "PVEFW-INPUT", "-i venet0 -m set --match-set ${venet0_ipset_chain} src -j PVEFW-VENET-OUT");
-
ruleset_create_chain($ruleset, "PVEFW-FWBR-IN");
ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $ipversion, $hostfw_options, $cluster_conf, $loglevel);
ruleset_create_chain($ruleset, "PVEFW-FWBR-OUT");
ruleset_addrule($ruleset, "PVEFW-FORWARD", "-m physdev --physdev-is-bridged --physdev-out fwln+ -j PVEFW-FWBR-OUT");
- ruleset_create_chain($ruleset, "PVEFW-VENET-IN");
- ruleset_chain_add_input_filters($ruleset, "PVEFW-VENET-IN", $ipversion, $hostfw_options, $cluster_conf, $loglevel);
-
- ruleset_addrule($ruleset, "PVEFW-FORWARD", "-o venet0 -m set --match-set ${venet0_ipset_chain} dst -j PVEFW-VENET-IN");
-
generate_std_chains($ruleset, $hostfw_options, $ipversion);
my $hostfw_enable = !(defined($hostfw_options->{enable}) && ($hostfw_options->{enable} == 0));
- my $ipset_ruleset = {};
-
if ($hostfw_enable) {
eval { enable_host_firewall($ruleset, $hostfw_conf, $cluster_conf, $ipversion); };
warn $@ if $@; # just to be sure - should not happen
}
- ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-o venet0 -m set --match-set ${venet0_ipset_chain} dst -j PVEFW-VENET-IN");
-
# generate firewall rules for QEMU VMs
foreach my $vmid (keys %{$vmdata->{qemu}}) {
eval {
my $vmfw_conf = $vmfw_configs->{$vmid};
return if !$vmfw_conf;
- generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf);
-
foreach my $netid (keys %$conf) {
next if $netid !~ m/^net(\d+)$/;
my $net = PVE::QemuServer::parse_net($conf->{$netid});
my $vmfw_conf = $vmfw_configs->{$vmid};
return if !$vmfw_conf;
- generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf);
-
if ($vmfw_conf->{options}->{enable}) {
foreach my $netid (keys %$conf) {
next if $netid !~ m/^net(\d+)$/;
- my $net = PVE::LXC::parse_lxc_network($conf->{$netid});
+ my $net = PVE::LXC::Config->parse_lxc_network($conf->{$netid});
next if !$net->{firewall};
my $iface = "veth${vmid}i$1";
my $macaddr = $net->{hwaddr};
warn $@ if $@; # just to be sure - should not happen
}
- # generate firewall rules for OpenVZ containers
- foreach my $vmid (keys %{$vmdata->{openvz}}) {
- eval {
- my $conf = $vmdata->{openvz}->{$vmid};
+ if(ruleset_chain_exist($ruleset, "PVEFW-IPS")){
+ ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j PVEFW-IPS");
+ }
+
+ return $ruleset;
+}
+
+sub mac_to_linklocal {
+ my ($macaddr) = @_;
+ my @parts = split(/:/, $macaddr);
+ # The standard link local address uses the fe80::/64 prefix with the
+ # modified EUI-64 identifier derived from the MAC address by flipping the
+ # universal/local bit and inserting FF:FE in the middle.
+ # See RFC 4291.
+ $parts[0] = sprintf("%02x", hex($parts[0]) ^ 0x02);
+ my @meui64 = (@parts[0,1,2], 'ff', 'fe', @parts[3,4,5]);
+ return "fe80::$parts[0]$parts[1]:$parts[2]FF:FE$parts[3]:$parts[4]$parts[5]";
+}
+
+sub compile_ipsets {
+ my ($cluster_conf, $vmfw_configs, $vmdata) = @_;
+ my $localnet;
+ if ($cluster_conf->{aliases}->{local_network}) {
+ $localnet = $cluster_conf->{aliases}->{local_network}->{cidr};
+ } else {
+ my $localnet_ver;
+ ($localnet, $localnet_ver) = parse_ip_or_cidr(local_network() || '127.0.0.0/8');
+
+ $cluster_conf->{aliases}->{local_network} = {
+ name => 'local_network', cidr => $localnet, ipversion => $localnet_ver };
+ }
+
+ push @{$cluster_conf->{ipset}->{management}}, { cidr => $localnet };
+
+
+ my $ipset_ruleset = {};
+
+ # generate ipsets for QEMU VMs
+ foreach my $vmid (keys %{$vmdata->{qemu}}) {
+ eval {
+ my $conf = $vmdata->{qemu}->{$vmid};
my $vmfw_conf = $vmfw_configs->{$vmid};
return if !$vmfw_conf;
- generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf);
+ # When the 'ipfilter' option is enabled every device for which there
+ # is no 'ipfilter-netX' ipset defiend gets an implicit empty default
+ # ipset.
+ # The reason is that ipfilter ipsets are always filled with standard
+ # IPv6 link-local filters.
+ my $ipsets = $vmfw_conf->{ipset};
+ my $implicit_sets = {};
- if ($vmfw_conf->{options}->{enable}) {
- if ($conf->{ip_address} && $conf->{ip_address}->{value}) {
- my $ip = $conf->{ip_address}->{value};
- $ip =~ s/\s+/,/g;
+ my $device_ips = {};
+ foreach my $netid (keys %$conf) {
+ next if $netid !~ m/^net(\d+)$/;
+ my $net = PVE::QemuServer::parse_net($conf->{$netid});
+ next if !$net->{firewall};
- my @ips = ();
+ if ($vmfw_conf->{options}->{ipfilter} && !$ipsets->{"ipfilter-$netid"}) {
+ $implicit_sets->{"ipfilter-$netid"} = [];
+ }
- 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;
- }
+ my $macaddr = $net->{macaddr};
+ my $linklocal = mac_to_linklocal($macaddr);
+ $device_ips->{$netid} = [
+ { cidr => $linklocal },
+ { cidr => 'fe80::/10', nomatch => 1 }
+ ];
+ }
- 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);
- }
+ generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf, $device_ips, $ipsets);
+ generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf, $device_ips, $implicit_sets);
+ };
+ warn $@ if $@; # just to be sure - should not happen
+ }
+
+ # generate firewall rules for LXC containers
+ foreach my $vmid (keys %{$vmdata->{lxc}}) {
+ eval {
+ my $conf = $vmdata->{lxc}->{$vmid};
+ my $vmfw_conf = $vmfw_configs->{$vmid};
+ 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
+ # ipset.
+ # The reason is that ipfilter ipsets are always filled with standard
+ # IPv6 link-local filters, as well as the IP addresses configured
+ # for the container.
+ my $ipsets = $vmfw_conf->{ipset};
+ my $implicit_sets = {};
+
+ my $device_ips = {};
+ foreach my $netid (keys %$conf) {
+ next if $netid !~ m/^net(\d+)$/;
+ my $net = PVE::LXC::Config->parse_lxc_network($conf->{$netid});
+ next if !$net->{firewall};
+
+ if ($vmfw_conf->{options}->{ipfilter} && !$ipsets->{"ipfilter-$netid"}) {
+ $implicit_sets->{"ipfilter-$netid"} = [];
}
- }
- if ($conf->{netif} && $conf->{netif}->{value}) {
- my $netif = PVE::OpenVZ::parse_netif($conf->{netif}->{value});
- foreach my $netid (keys %$netif) {
- my $d = $netif->{$netid};
- my $bridge = $d->{bridge};
- next if !$bridge || $bridge !~ m/^vmbr\d+(v(\d+))?f$/; # firewall enabled ?
- my $macaddr = $d->{mac};
- my $iface = $d->{host_ifname};
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
- $vmfw_conf, $vmid, 'IN', $ipversion);
- generate_tap_rules_direction($ruleset, $cluster_conf, $iface, $netid, $macaddr,
- $vmfw_conf, $vmid, 'OUT', $ipversion);
+ my $macaddr = $net->{hwaddr};
+ my $linklocal = mac_to_linklocal($macaddr);
+ my $set = $device_ips->{$netid} = [
+ { cidr => $linklocal },
+ { cidr => 'fe80::/10', nomatch => 1 }
+ ];
+ if ($net->{ip} =~ m!^($IPV4RE)(?:/\d+)?$!) {
+ push @$set, { cidr => $1 };
+ }
+ if ($net->{ip6} =~ m!^($IPV6RE)(?:/\d+)?$!) {
+ push @$set, { cidr => $1 };
}
}
+
+ generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf, $device_ips, $ipsets);
+ generate_ipset_chains($ipset_ruleset, $cluster_conf, $vmfw_conf, $device_ips, $implicit_sets);
};
warn $@ if $@; # just to be sure - should not happen
}
- if(ruleset_chain_exist($ruleset, "PVEFW-IPS")){
- ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j PVEFW-IPS");
- }
-
- generate_ipset_chains($ipset_ruleset, undef, $cluster_conf);
+ generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{ipset});
- return ($ruleset, $ipset_ruleset);
+ return $ipset_ruleset;
}
sub get_ruleset_status {
enable_bridge_firewall();
my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
- get_ipset_cmdlist($ipset_ruleset, undef, $verbose);
+ get_ipset_cmdlist($ipset_ruleset, $verbose);
my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset, $verbose);
my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables");