+sub compile_ebtables_filter {
+ my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata) = @_;
+
+ if (!($cluster_conf->{options}->{ebtables} // 1)) {
+ return {};
+ }
+
+ 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 (sort keys %{$vmdata->{qemu}}) {
+ eval {
+ 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+)$/;
+ my $net = PVE::QemuServer::parse_net($conf->{$netid});
+ next if !$net->{firewall};
+ my $iface = "tap${vmid}i$1";
+ my $macaddr = $net->{macaddr};
+ 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
+ }
+
+ # generate firewall rules for LXC containers
+ foreach my $vmid (sort keys %{$vmdata->{lxc}}) {
+ eval {
+ my $conf = $vmdata->{lxc}->{$vmid};
+
+ 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+)$/;
+ my $net = PVE::LXC::Config->parse_lxc_network($conf->{$netid});
+ next if !$net->{firewall};
+ my $iface = "veth${vmid}i$1";
+ my $macaddr = $net->{hwaddr};
+ 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);
+ }
+ }
+ if (defined(my $ip = $net->{ip}) && $vmfw_conf->{options}->{ipfilter}) {
+ # ebtables changes this to a .0/MASK network but we just
+ # want the address here, no network - see #2193
+ $ip =~ s|/(\d+)$||;
+ if ($ip ne 'dhcp') {
+ push @$arpfilter, $ip;
+ }
+ }
+ generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter);
+ }
+ };
+ warn $@ if $@; # just to be sure - should not happen
+ }
+
+ return $ruleset;
+}
+
+sub generate_tap_layer2filter {
+ my ($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter) = @_;
+ 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 (@$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, $protochain, "-p $proto", '-j RETURN');
+ }
+ ruleset_addrule($ruleset, $protochain, '', '-j DROP');
+ }
+
+ ruleset_addrule($ruleset, $tapchain, '', '-j ACCEPT');
+
+ ruleset_addrule($ruleset, 'PVEFW-FWBR-OUT', "-i $iface", "-j $tapchain");
+}
+
+# the parameter $change_only_regex changes two things if defined:
+# * all chains not matching it will be left intact
+# * both the $active_chains hash and the returned status_hash have different
+# structure (they contain a key named 'rules').