X-Git-Url: https://git.proxmox.com/?p=pve-firewall.git;a=blobdiff_plain;f=src%2FPVE%2FFirewall.pm;h=96cf9bd915b900ad73ecca18ebad22da62bcaebb;hp=c8a430c71ba0cc2e47f8cb70cfcd1bdbfb145079;hb=518f67dcda991c52fe6c033f913e9199eb40cd80;hpb=d50f24ea6dc1109d1f8c095f2d048cdba41f9696 diff --git a/src/PVE/Firewall.pm b/src/PVE/Firewall.pm index c8a430c..96cf9bd 100644 --- a/src/PVE/Firewall.pm +++ b/src/PVE/Firewall.pm @@ -1711,6 +1711,12 @@ sub ipset_restore_cmdlist { run_command("/sbin/ipset restore", input => $cmdlist, errmsg => "ipset_restore_cmdlist"); } +sub ebtables_restore_cmdlist { + my ($cmdlist) = @_; + + run_command("/sbin/ebtables-restore", input => $cmdlist, errmsg => "ebtables_restore_cmdlist"); +} + sub iptables_get_chains { my ($iptablescmd) = @_; @@ -1821,6 +1827,45 @@ sub ipset_get_chains { return $res; } +sub ebtables_get_chains { + + my $res = {}; + my $chains = {}; + + my $parser = sub { + my $line = shift; + return if $line =~ m/^#/; + return if $line =~ m/^\s*$/; + if ($line =~ m/^:(\S+)\s\S+$/) { + # Make sure we know chains exist even if they're empty. + $chains->{$1} //= []; + } elsif ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) { + my $chain = $1; + $line =~ s/\s+$//; + push @{$chains->{$chain}}, $line; + } elsif ($line =~ m/^(?:\S+)\s(tap\d+i\d+-(:?IN|OUT))\s(?:\S+).*/) { + my $chain = $1; + $line =~ s/\s+$//; + push @{$chains->{$chain}}, $line; + } elsif ($line =~ m/^(?:\S+)\s(veth\d+i\d+-(:?IN|OUT))\s(?:\S+).*/) { + my $chain = $1; + $line =~ s/\s+$//; + push @{$chains->{$chain}}, $line; + } else { + # simply ignore the rest + return; + } + }; + + run_command("/sbin/ebtables-save", outfunc => $parser); + + # compute digest for each chain + foreach my $chain (keys %$chains) { + $res->{$chain} = iptables_chain_digest($chains->{$chain}); + } + return $res; +} + # substitude action of rule according to action hash sub rule_substitude_action { my ($rule, $actions) = @_; @@ -2023,7 +2068,9 @@ sub ruleset_addrule { my $logaction = get_log_rule_base($chain, $vmid, $logmsg, $log); push @{$ruleset->{$chain}}, "-A $chain $match $logaction"; } - push @{$ruleset->{$chain}}, "-A $chain $match $action"; + # for stable ebtables digests avoid double-spaces to match ebtables-save output + $match .= ' ' if length($match); + push @{$ruleset->{$chain}}, "-A $chain ${match}$action"; } sub ruleset_insertrule { @@ -2548,6 +2595,14 @@ sub parse_fw_rule { return $rule; } +sub verify_ethertype { + my ($value) = @_; + my $types = get_etc_ethertypes(); + die "unknown ethernet protocol type: $value\n" + if !defined($types->{byname}->{$value}) && + !defined($types->{byid}->{$value}); +} + sub parse_vmfw_option { my ($line) = @_; @@ -2567,6 +2622,10 @@ sub parse_vmfw_option { } elsif ($line =~ m/^(ips_queues):\s*((\d+)(:(\d+))?)\s*$/i) { $opt = lc($1); $value = $2; + } elsif ($line =~ m/^(layer2_protocols):\s*(((\S+)[,]?)+)\s*$/i) { + $opt = lc($1); + $value = $2; + verify_ethertype($_) foreach split(/\s*,\s*/, $value); } else { die "can't parse option '$line'\n" } @@ -3380,9 +3439,10 @@ sub compile { 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 $ebtables_ruleset = compile_ebtables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $verbose); my $ipset_ruleset = compile_ipsets($cluster_conf, $vmfw_configs, $vmdata); - return ($ruleset, $ipset_ruleset, $rulesetv6); + return ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset); } sub compile_iptables_filter { @@ -3594,6 +3654,94 @@ sub compile_ipsets { return $ipset_ruleset; } +sub compile_ebtables_filter { + my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $verbose) = @_; + + return ({}, {}) if !$cluster_conf->{options}->{enable}; + + my $ruleset = {}; + + ruleset_create_chain($ruleset, "PVEFW-FORWARD"); + + + ruleset_create_chain($ruleset, "PVEFW-FWBR-OUT"); + #for ipv4 and ipv6, check macaddress in iptables, so we use conntrack 'ESTABLISHED', to speedup rules + ruleset_addrule($ruleset, 'PVEFW-FORWARD', '-p IPv4', '-j ACCEPT'); + ruleset_addrule($ruleset, 'PVEFW-FORWARD', '-p IPv6', '-j ACCEPT'); + ruleset_addrule($ruleset, 'PVEFW-FORWARD', '-o fwln+', '-j PVEFW-FWBR-OUT'); + + # generate firewall rules 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; + + foreach my $netid (keys %$conf) { + next if $netid !~ m/^net(\d+)$/; + my $net = PVE::QemuServer::parse_net($conf->{$netid}); + next if !$net->{firewall}; + my $iface = "tap${vmid}i$1"; + my $macaddr = $net->{macaddr}; + + generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid); + + } + }; + 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 || !$vmfw_conf->{options}->{enable}; + + 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}; + my $iface = "veth${vmid}i$1"; + my $macaddr = $net->{hwaddr}; + generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid); + } + }; + warn $@ if $@; # just to be sure - should not happen + } + + return $ruleset; +} + +sub generate_tap_layer2filter { + my ($ruleset, $iface, $macaddr, $vmfw_conf, $vmid) = @_; + my $options = $vmfw_conf->{options}; + + my $tapchain = $iface."-OUT"; + + # ebtables remove zeros from mac pairs + $macaddr =~ s/0([0-9a-f])/$1/ig; + $macaddr = lc($macaddr); + + ruleset_create_chain($ruleset, $tapchain); + + if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) { + ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP'); + } + + if (defined($options->{layer2_protocols})){ + foreach my $proto (split(/,/, $options->{layer2_protocols})) { + ruleset_addrule($ruleset, $tapchain, "-p $proto", '-j ACCEPT'); + } + ruleset_addrule($ruleset, $tapchain, '', "-j DROP"); + } else { + ruleset_addrule($ruleset, $tapchain, '', '-j ACCEPT'); + } + + ruleset_addrule($ruleset, 'PVEFW-FWBR-OUT', "-i $iface", "-j $tapchain"); +} + sub get_ruleset_status { my ($ruleset, $active_chains, $digest_fn, $verbose) = @_; @@ -3701,6 +3849,39 @@ sub get_ruleset_cmdlist { return wantarray ? ($cmdlist, $changes) : $cmdlist; } +sub get_ebtables_cmdlist { + my ($ruleset, $verbose) = @_; + + my $changes = 0; + my $cmdlist = "*filter\n"; + + my ($active_chains, $hooks) = ebtables_get_chains(); + my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, $verbose); + + # create chains first + foreach my $chain (sort keys %$ruleset) { + my $stat = $statushash->{$chain}; + die "internal error" if !$stat; + $cmdlist .= ":$chain ACCEPT\n"; + } + + if ($ruleset->{'PVEFW-FORWARD'}) { + $cmdlist .= "-A FORWARD -j PVEFW-FORWARD\n"; + } + + foreach my $chain (sort keys %$ruleset) { + my $stat = $statushash->{$chain}; + die "internal error" if !$stat; + $changes = 1 if ($stat->{action} ne 'exists'); + + foreach my $cmd (@{$ruleset->{$chain}}) { + $cmdlist .= "$cmd\n"; + } + } + + return wantarray ? ($cmdlist, $changes) : $cmdlist; +} + sub get_ipset_cmdlist { my ($ruleset, $verbose) = @_; @@ -3760,7 +3941,7 @@ sub get_ipset_cmdlist { } sub apply_ruleset { - my ($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $verbose) = @_; + my ($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $ebtables_ruleset, $verbose) = @_; enable_bridge_firewall(); @@ -3769,6 +3950,7 @@ sub apply_ruleset { my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset, $verbose); my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, $verbose, "ip6tables"); + my ($ebtables_cmdlist, $ebtables_changes) = get_ebtables_cmdlist($ebtables_ruleset, $verbose); if ($verbose) { if ($ipset_changes) { @@ -3786,6 +3968,11 @@ sub apply_ruleset { print "ip6tables changes:\n"; print $cmdlistv6; } + + if ($ebtables_changes) { + print "ebtables changes:\n"; + print $ebtables_cmdlist; + } } my $tmpfile = "$pve_fw_status_dir/ipsetcmdlist1"; @@ -3808,6 +3995,11 @@ sub apply_ruleset { ipset_restore_cmdlist($ipset_delete_cmdlist) if $ipset_delete_cmdlist; + ebtables_restore_cmdlist($ebtables_cmdlist); + + $tmpfile = "$pve_fw_status_dir/ebtablescmdlist"; + PVE::Tools::file_set_contents($tmpfile, $ebtables_cmdlist || ''); + # test: re-read status and check if everything is up to date my $active_chains = iptables_get_chains(); my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest, 0); @@ -3832,6 +4024,17 @@ sub apply_ruleset { } } + my $active_ebtables_chains = ebtables_get_chains(); + my $ebtables_statushash = get_ruleset_status($ebtables_ruleset, $active_ebtables_chains, \&iptables_chain_digest, 0); + + foreach my $chain (sort keys %$ebtables_ruleset) { + my $stat = $ebtables_statushash->{$chain}; + if ($stat->{action} ne 'exists') { + warn "ebtables : unable to update chain '$chain'\n"; + $errors = 1; + } + } + die "unable to apply firewall changes\n" if $errors; update_nf_conntrack_max($hostfw_conf); @@ -3947,9 +4150,9 @@ sub update { my $hostfw_conf = load_hostfw_conf($cluster_conf); - my ($ruleset, $ipset_ruleset, $rulesetv6) = compile($cluster_conf, $hostfw_conf); + my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = compile($cluster_conf, $hostfw_conf); - apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6); + apply_ruleset($ruleset, $hostfw_conf, $ipset_ruleset, $rulesetv6, $ebtables_ruleset); }; run_locked($code);