]> git.proxmox.com Git - pve-firewall.git/blobdiff - src/PVE/Firewall.pm
ebtables: test layer2_protocols in an external chain
[pve-firewall.git] / src / PVE / Firewall.pm
index ef00d0ce6d31ee82ddad66a2c822c0c094d2753c..6ac303831a62f630d437ef0e0508decd2b72f5ac 100644 (file)
@@ -1049,12 +1049,13 @@ sub parse_port_name_number_or_range {
     my @elements = split(/,/, $str);
     die "extraneous commas in list\n" if $str ne join(',', @elements);
     foreach my $item (@elements) {
-       if ($item =~ m/^(\d+):(\d+)$/) {
+       if ($item =~ m/^([0-9]+):([0-9]+)$/) {
            $count += 2;
            my ($port1, $port2) = ($1, $2);
            die "invalid port '$port1'\n" if $port1 > 65535;
            die "invalid port '$port2'\n" if $port2 > 65535;
-       } elsif ($item =~ m/^(\d+)$/) {
+           die "backwards range '$port1:$port2' not allowed, did you mean '$port2:$port1'?\n" if $port1 > $port2;
+       } elsif ($item =~ m/^([0-9]+)$/) {
            $count += 1;
            my $port = $1;
            die "invalid port '$port'\n" if $port > 65535;
@@ -1241,6 +1242,12 @@ our $host_option_properties = {
        type => 'boolean',
        optional => 1,
     },
+    nf_conntrack_allow_invalid => {
+       description => "Allow invalid packets on connection tracking.",
+       type => 'boolean',
+       default => 0,
+       optional => 1,
+    },
 };
 
 our $vm_option_properties = {
@@ -2107,7 +2114,7 @@ sub ruleset_add_chain_policy {
     } elsif ($policy eq 'REJECT') {
        ruleset_addrule($ruleset, $chain, "", "-j PVEFW-Reject");
 
-       ruleset_addrule($ruleset, $chain, "", "-g PVEFW-reject", $loglevel, "policy $policy:", $vmid);
+       ruleset_addrule($ruleset, $chain, "", "-g PVEFW-reject", $loglevel, "policy $policy: ", $vmid);
     } else {
        # should not happen
        die "internal error: unknown policy '$policy'";
@@ -2127,9 +2134,11 @@ sub ruleset_chain_add_ndp {
 }
 
 sub ruleset_chain_add_conn_filters {
-    my ($ruleset, $chain, $accept) = @_;
+    my ($ruleset, $chain, $allow_invalid, $accept) = @_;
 
-    ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID", "-j DROP");
+    if (!$allow_invalid) {
+       ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID", "-j DROP");
+    }
     ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED", "-j $accept");
 }
 
@@ -2355,7 +2364,7 @@ sub enable_host_firewall {
 
     ruleset_addrule($ruleset, $chain, "-i lo", "-j ACCEPT");
 
-    ruleset_chain_add_conn_filters($ruleset, $chain, 'ACCEPT');
+    ruleset_chain_add_conn_filters($ruleset, $chain, 0, 'ACCEPT');
     ruleset_chain_add_ndp($ruleset, $chain, $ipversion, $options, 'IN', '-j RETURN');
     ruleset_chain_add_input_filters($ruleset, $chain, $ipversion, $options, $cluster_conf, $loglevel);
 
@@ -2413,7 +2422,7 @@ sub enable_host_firewall {
 
     ruleset_addrule($ruleset, $chain, "-o lo", "-j ACCEPT");
 
-    ruleset_chain_add_conn_filters($ruleset, $chain, 'ACCEPT');
+    ruleset_chain_add_conn_filters($ruleset, $chain, 0, 'ACCEPT');
 
     # we use RETURN because we may want to check other thigs later
     $accept_action = 'RETURN';
@@ -2637,7 +2646,7 @@ sub parse_hostfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp):\s*(0|1)\s*$/i) {
+    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid):\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) {
@@ -3460,7 +3469,8 @@ sub compile_iptables_filter {
     # fixme: what log level should we use here?
     my $loglevel = get_option_log_level($hostfw_options, "log_level_out");
 
-    ruleset_chain_add_conn_filters($ruleset, "PVEFW-FORWARD", "ACCEPT");
+    my $conn_allow_invalid = $hostfw_options->{nf_conntrack_allow_invalid} // 0;
+    ruleset_chain_add_conn_filters($ruleset, "PVEFW-FORWARD", $conn_allow_invalid, "ACCEPT");
 
     ruleset_create_chain($ruleset, "PVEFW-FWBR-IN");
     ruleset_chain_add_input_filters($ruleset, "PVEFW-FWBR-IN", $ipversion, $hostfw_options, $cluster_conf, $loglevel);
@@ -3677,6 +3687,7 @@ sub compile_ebtables_filter {
            my $conf = $vmdata->{qemu}->{$vmid};
            my $vmfw_conf = $vmfw_configs->{$vmid};
            return if !$vmfw_conf;
+           my $ipsets = $vmfw_conf->{ipset};
 
            foreach my $netid (sort keys %$conf) {
                next if $netid !~ m/^net(\d+)$/;
@@ -3684,9 +3695,15 @@ sub compile_ebtables_filter {
                next if !$net->{firewall};
                my $iface = "tap${vmid}i$1";
                my $macaddr = $net->{macaddr};
-
-               generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid);
-
+               my $arpfilter = [];
+               if (defined(my $ipset = $ipsets->{"ipfilter-$netid"})) {
+                   foreach my $ipaddr (@$ipset) {
+                       my($ip, $version) = parse_ip_or_cidr($ipaddr->{cidr});
+                       next if !$ip || ($version && $version != 4);
+                       push(@$arpfilter, $ip);
+                   }
+               }
+               generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter);
            }
        };
        warn $@ if $@; # just to be sure - should not happen
@@ -3699,6 +3716,7 @@ sub compile_ebtables_filter {
 
            my $vmfw_conf = $vmfw_configs->{$vmid};
            return if !$vmfw_conf || !$vmfw_conf->{options}->{enable};
+           my $ipsets = $vmfw_conf->{ipset};
 
            foreach my $netid (sort keys %$conf) {
                next if $netid !~ m/^net(\d+)$/;
@@ -3706,7 +3724,16 @@ sub compile_ebtables_filter {
                next if !$net->{firewall};
                my $iface = "veth${vmid}i$1";
                my $macaddr = $net->{hwaddr};
-               generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid);
+               my $arpfilter = [];
+               if (defined(my $ipset = $ipsets->{"ipfilter-$netid"})) {
+                   foreach my $ipaddr (@$ipset) {
+                       my($ip, $version) = parse_ip_or_cidr($ipaddr->{cidr});
+                       next if !$ip || ($version && $version != 4);
+                       push(@$arpfilter, $ip);
+                   }
+               }
+               push(@$arpfilter, $net->{ip}) if $net->{ip} && $vmfw_conf->{options}->{ipfilter};
+               generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter);
            }
        };
        warn $@ if $@; # just to be sure - should not happen
@@ -3716,7 +3743,7 @@ sub compile_ebtables_filter {
 }
 
 sub generate_tap_layer2filter {
-    my ($ruleset, $iface, $macaddr, $vmfw_conf, $vmid) = @_;
+    my ($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter) = @_;
     my $options = $vmfw_conf->{options};
 
     my $tapchain = $iface."-OUT";
@@ -3731,15 +3758,30 @@ sub generate_tap_layer2filter {
            ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP');
     }
 
+    if (@$arpfilter){
+       my $arpchain = $tapchain."-ARP";
+       ruleset_addrule($ruleset, $tapchain, "-p ARP", "-j $arpchain");
+       ruleset_create_chain($ruleset, $arpchain);
+
+       foreach my $ip (@{$arpfilter}) {
+           ruleset_addrule($ruleset, $arpchain, "-p ARP --arp-ip-src $ip", '-j RETURN');
+       }
+       ruleset_addrule($ruleset, $arpchain, '', '-j DROP');
+    }
+
     if (defined($options->{layer2_protocols})){
+       my $protochain = $tapchain."-PROTO";
+       ruleset_addrule($ruleset, $tapchain, '', "-j $protochain");
+       ruleset_create_chain($ruleset, $protochain);
+
        foreach my $proto (split(/,/, $options->{layer2_protocols})) {
-           ruleset_addrule($ruleset, $tapchain, "-p $proto", '-j ACCEPT');
+           ruleset_addrule($ruleset, $protochain, "-p $proto", '-j RETURN');
        }
-       ruleset_addrule($ruleset, $tapchain, '', "-j DROP");
-    } else {
-       ruleset_addrule($ruleset, $tapchain, '', '-j ACCEPT');
+       ruleset_addrule($ruleset, $protochain, '', '-j DROP');
     }
 
+    ruleset_addrule($ruleset, $tapchain, '', '-j ACCEPT');
+
     ruleset_addrule($ruleset, 'PVEFW-FWBR-OUT', "-i $iface", "-j $tapchain");
 }
 
@@ -4068,6 +4110,7 @@ sub apply_ruleset {
 
     update_nf_conntrack_tcp_timeout_established($hostfw_conf);
 
+    update_nf_conntrack_logging($hostfw_conf);
 }
 
 sub update_nf_conntrack_max {
@@ -4104,6 +4147,23 @@ sub update_nf_conntrack_tcp_timeout_established {
     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established", $value);
 }
 
+my $log_nf_conntrack_enabled = undef;
+sub update_nf_conntrack_logging {
+    my ($hostfw_conf) = @_;
+
+    my $options = $hostfw_conf->{options} || {};
+    my $value = $options->{log_nf_conntrack} || 0;
+    if (!defined($log_nf_conntrack_enabled)
+       || $value != $log_nf_conntrack_enabled)
+    {
+       my $tmpfile = "$pve_fw_status_dir/log_nf_conntrack";
+       PVE::Tools::file_set_contents($tmpfile, $value);
+
+       PVE::Tools::run_command([qw(systemctl try-reload-or-restart pvefw-logger.service)]);
+       $log_nf_conntrack_enabled = $value;
+    }
+}
+
 sub remove_pvefw_chains {
 
     PVE::Firewall::remove_pvefw_chains_iptables("iptables");