]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
LXC refactoring
[pve-firewall.git] / src / PVE / Firewall.pm
index db8884f4468d0d586cc73965e6efa6fb47290509..a7faeaf40499cbebf840ca7d321acc9e870b80ca 100644 (file)
@@ -2073,7 +2073,7 @@ sub generate_tap_rules_direction {
 
     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);
@@ -2360,7 +2360,7 @@ sub parse_vmfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|dhcp|ndp|radv|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) {
@@ -2714,7 +2714,7 @@ sub read_local_vm_config {
            }
         } 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;
                 }
@@ -2972,11 +2972,17 @@ sub generate_std_chains {
 }
 
 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 = {};
@@ -2991,7 +2997,10 @@ sub generate_ipset_chains {
                }
                #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$||;
@@ -3157,14 +3166,7 @@ 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) = @_;
+    return ({},{},{}) if !$cluster_conf->{options}->{enable};
 
     my $localnet;
     if ($cluster_conf->{aliases}->{local_network}) {
@@ -3179,7 +3181,15 @@ sub compile_iptables_filter {
 
     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 = {};
 
@@ -3207,8 +3217,6 @@ sub compile_iptables_filter {
 
     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
@@ -3221,8 +3229,6 @@ sub compile_iptables_filter {
            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});
@@ -3246,12 +3252,10 @@ sub compile_iptables_filter {
             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};
@@ -3269,9 +3273,128 @@ sub compile_iptables_filter {
        ruleset_insertrule($ruleset, "PVEFW-FORWARD", "-m conntrack --ctstate RELATED,ESTABLISHED -j PVEFW-IPS");
     }
 
-    generate_ipset_chains($ipset_ruleset, undef, $cluster_conf);
+    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;
+
+           # 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 = {};
+
+           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};
+
+               if ($vmfw_conf->{options}->{ipfilter} && !$ipsets->{"ipfilter-$netid"}) {
+                   $implicit_sets->{"ipfilter-$netid"} = [];
+               }
+
+               my $macaddr = $net->{macaddr};
+               my $linklocal = mac_to_linklocal($macaddr);
+               $device_ips->{$netid} = [
+                   { cidr => $linklocal },
+                   { cidr => 'fe80::/10', nomatch => 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
+    }
+
+    # 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"} = [];
+               }
+
+               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
+    }
+
+    generate_ipset_chains($ipset_ruleset, undef, $cluster_conf, undef, $cluster_conf->{ipset});
 
-    return ($ruleset, $ipset_ruleset);
+    return $ipset_ruleset;
 }
 
 sub get_ruleset_status {
@@ -3445,7 +3568,7 @@ sub apply_ruleset {
     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");