commit cd2a3cbf5e6d9c17b8e9a92097d0e33cf769791f
Author: Nacho Barrientos <nacho.barrientos@cern.ch>
Date:   Tue Mar 23 10:33:44 2021 +0100

    Add rules for QEMU/libvirt guests

diff --git a/REFERENCE.md b/REFERENCE.md
index 1fdbab0..6d7552a 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -59,6 +59,7 @@ and Manager Daemons (MGR).
 * [`nftables::rules::out::tor`](#nftablesrulesouttor): manage out tor
 * [`nftables::rules::out::wireguard`](#nftablesrulesoutwireguard): manage out wireguard
 * [`nftables::rules::puppet`](#nftablesrulespuppet): manage in puppet
+* [`nftables::rules::qemu`](#nftablesrulesqemu): Bridged network configuration for qemu/libvirt
 * [`nftables::rules::samba`](#nftablesrulessamba): manage Samba, the suite to allow Windows file sharing on Linux resources.
 * [`nftables::rules::smtp`](#nftablesrulessmtp): manage in smtp
 * [`nftables::rules::smtp_submission`](#nftablesrulessmtp_submission): manage in smtp submission
@@ -791,6 +792,94 @@ puppet server ports
 
 Default value: `[8140]`
 
+### <a name="nftablesrulesqemu"></a>`nftables::rules::qemu`
+
+This class configures the typical firewall setup that libvirt
+creates. Depending on your requirements you can switch on and off
+several aspects, for instance if you don't do DHCP to your guests
+you can disable the rules that accept DHCP traffic on the host or if
+you don't want your guests to talk to hosts outside you can disable
+forwarding and/or masquerading for IPv4 traffic.
+
+#### Parameters
+
+The following parameters are available in the `nftables::rules::qemu` class:
+
+* [`interface`](#interface)
+* [`network_v4`](#network_v4)
+* [`network_v6`](#network_v6)
+* [`dns`](#dns)
+* [`dhcpv4`](#dhcpv4)
+* [`forward_traffic`](#forward_traffic)
+* [`internal_traffic`](#internal_traffic)
+* [`masquerade`](#masquerade)
+
+##### <a name="interface"></a>`interface`
+
+Data type: `String[1]`
+
+Interface name used by the bridge.
+
+Default value: `'virbr0'`
+
+##### <a name="network_v4"></a>`network_v4`
+
+Data type: `Stdlib::IP::Address::V4::CIDR`
+
+The IPv4 network prefix used in the virtual network.
+
+Default value: `'192.168.122.0/24'`
+
+##### <a name="network_v6"></a>`network_v6`
+
+Data type: `Optional[Stdlib::IP::Address::V6::CIDR]`
+
+The IPv6 network prefix used in the virtual network.
+
+Default value: ``undef``
+
+##### <a name="dns"></a>`dns`
+
+Data type: `Boolean`
+
+Allow DNS traffic from the guests to the host.
+
+Default value: ``true``
+
+##### <a name="dhcpv4"></a>`dhcpv4`
+
+Data type: `Boolean`
+
+Allow DHCPv4 traffic from the guests to the host.
+
+Default value: ``true``
+
+##### <a name="forward_traffic"></a>`forward_traffic`
+
+Data type: `Boolean`
+
+Allow forwarded traffic (out all, in related/established)
+generated by the virtual network.
+
+Default value: ``true``
+
+##### <a name="internal_traffic"></a>`internal_traffic`
+
+Data type: `Boolean`
+
+Allow guests in the virtual network to talk to each other.
+
+Default value: ``true``
+
+##### <a name="masquerade"></a>`masquerade`
+
+Data type: `Boolean`
+
+Do NAT masquerade on all IPv4 traffic generated by guests
+to external networks.
+
+Default value: ``true``
+
 ### <a name="nftablesrulessamba"></a>`nftables::rules::samba`
 
 manage Samba, the suite to allow Windows file sharing on Linux resources.
diff --git a/manifests/rules/qemu.pp b/manifests/rules/qemu.pp
new file mode 100644
index 0000000..5686a35
--- /dev/null
+++ b/manifests/rules/qemu.pp
@@ -0,0 +1,112 @@
+# @summary Bridged network configuration for qemu/libvirt
+#
+# This class configures the typical firewall setup that libvirt
+# creates. Depending on your requirements you can switch on and off
+# several aspects, for instance if you don't do DHCP to your guests
+# you can disable the rules that accept DHCP traffic on the host or if
+# you don't want your guests to talk to hosts outside you can disable
+# forwarding and/or masquerading for IPv4 traffic.
+#
+# @param interface
+#   Interface name used by the bridge.
+#
+# @param network_v4
+#   The IPv4 network prefix used in the virtual network.
+#
+# @param network_v6
+#   The IPv6 network prefix used in the virtual network.
+#
+# @param dns
+#   Allow DNS traffic from the guests to the host.
+#
+# @param dhcpv4
+#   Allow DHCPv4 traffic from the guests to the host.
+#
+# @param forward_traffic
+#   Allow forwarded traffic (out all, in related/established)
+#   generated by the virtual network.
+#
+# @param internal_traffic
+#   Allow guests in the virtual network to talk to each other.
+#
+# @param masquerade
+#   Do NAT masquerade on all IPv4 traffic generated by guests
+#   to external networks.
+class nftables::rules::qemu (
+  String[1]                               $interface         = 'virbr0',
+  Stdlib::IP::Address::V4::CIDR           $network_v4        = '192.168.122.0/24',
+  Optional[Stdlib::IP::Address::V6::CIDR] $network_v6        = undef,
+  Boolean                                 $dns               = true,
+  Boolean                                 $dhcpv4            = true,
+  Boolean                                 $forward_traffic   = true,
+  Boolean                                 $internal_traffic  = true,
+  Boolean                                 $masquerade        = true,
+) {
+  if $dns {
+    nftables::rule {
+      'default_in-qemu_udp_dns':
+        content => "iifname \"${interface}\" udp dport 53 accept";
+      'default_in-qemu_tcp_dns':
+        content => "iifname \"${interface}\" tcp dport 53 accept";
+    }
+  }
+
+  if $dhcpv4 {
+    nftables::rule {
+      'default_in-qemu_dhcpv4':
+        content => "iifname \"${interface}\" meta l4proto udp udp dport 67 accept";
+      # The rule below is created by libvirt. It should not be necessary here
+      # as it should be accepted by the conntrack rules in OUTPUT.
+      #'default_out-qemu_dhcpv4':
+      #  content => "oifname \"${interface}\" meta l4proto udp udp dport 68 accept";
+    }
+  }
+
+  if $forward_traffic {
+    nftables::rule {
+      'default_fwd-qemu_oip_v4':
+        content => "oifname \"${interface}\" ip daddr ${network_v4} ct state related,established accept";
+      'default_fwd-qemu_iip_v4':
+        content => "iifname \"${interface}\" ip saddr ${network_v4} accept";
+    }
+    if $network_v6 {
+      nftables::rule {
+        'default_fwd-qemu_oip_v6':
+          content => "oifname \"${interface}\" ip6 daddr ${network_v6} ct state related,established accept";
+        'default_fwd-qemu_iip_v6':
+          content => "iifname \"${interface}\" ip6 saddr ${network_v6} accept";
+      }
+    }
+  }
+
+  if $internal_traffic {
+    nftables::rule {
+      'default_fwd-qemu_io_internal':
+        content => "iifname \"${interface}\" oifname \"${interface}\" accept",
+    }
+  }
+
+  # Libvirt rejects all the remaining forwarded traffic passing
+  # through the virtual interface. This is not necessary here because
+  # of the default policy in default_fwd.
+
+  if $masquerade {
+    nftables::rule {
+      'POSTROUTING-qemu_ignore_multicast':
+        table   => 'ip-nat',
+        content => "ip saddr ${network_v4} ip daddr 224.0.0.0/24 return";
+      'POSTROUTING-qemu_ignore_broadcast':
+        table   => 'ip-nat',
+        content => "ip saddr ${network_v4} ip daddr 255.255.255.255 return";
+      'POSTROUTING-qemu_masq_tcp':
+        table   => 'ip-nat',
+        content => "meta l4proto tcp ip saddr ${network_v4} ip daddr != ${network_v4} masquerade to :1024-65535";
+      'POSTROUTING-qemu_masq_udp':
+        table   => 'ip-nat',
+        content => "meta l4proto udp ip saddr ${network_v4} ip daddr != ${network_v4} masquerade to :1024-65535";
+      'POSTROUTING-qemu_masq_ip':
+        table   => 'ip-nat',
+        content => "ip saddr ${network_v4} ip daddr != ${network_v4} masquerade";
+    }
+  }
+}
diff --git a/spec/acceptance/all_rules_spec.rb b/spec/acceptance/all_rules_spec.rb
index 504732e..341b570 100644
--- a/spec/acceptance/all_rules_spec.rb
+++ b/spec/acceptance/all_rules_spec.rb
@@ -39,6 +39,7 @@ describe 'nftables class' do
       include nftables::rules::samba
       include nftables::rules::activemq
       include nftables::rules::docker_ce
+      include nftables::rules::qemu
       include nftables::rules::out::postgres
       include nftables::rules::out::icmp
       include nftables::rules::out::dns
diff --git a/spec/classes/rules/qemu_spec.rb b/spec/classes/rules/qemu_spec.rb
new file mode 100644
index 0000000..fc656cc
--- /dev/null
+++ b/spec/classes/rules/qemu_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe 'nftables::rules::qemu' do
+  on_supported_os.each do |os, os_facts|
+    context "on #{os}" do
+      let(:facts) { os_facts }
+
+      context 'default options' do
+        it { is_expected.to compile }
+        it {
+          is_expected.to contain_nftables__rule('default_in-qemu_udp_dns').
+            with_content('iifname "virbr0" udp dport 53 accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_in-qemu_tcp_dns').
+            with_content('iifname "virbr0" tcp dport 53 accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_in-qemu_dhcpv4').
+            with_content('iifname "virbr0" meta l4proto udp udp dport 67 accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_oip_v4').
+            with_content('oifname "virbr0" ip daddr 192.168.122.0/24 ct state related,established accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_iip_v4').
+            with_content('iifname "virbr0" ip saddr 192.168.122.0/24 accept')
+        }
+        it { is_expected.not_to contain_nftables__rule('default_fwd-qemu_oip_v6') }
+        it { is_expected.not_to contain_nftables__rule('default_fwd-qemu_iip_v6') }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_io_internal').
+            with_content('iifname "virbr0" oifname "virbr0" accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('POSTROUTING-qemu_ignore_multicast').with(
+            content: 'ip saddr 192.168.122.0/24 ip daddr 224.0.0.0/24 return',
+            table: 'ip-nat'
+          )
+        }
+        it {
+          is_expected.to contain_nftables__rule('POSTROUTING-qemu_ignore_broadcast').with(
+            content: 'ip saddr 192.168.122.0/24 ip daddr 255.255.255.255 return',
+            table: 'ip-nat'
+          )
+        }
+        it {
+          is_expected.to contain_nftables__rule('POSTROUTING-qemu_masq_tcp').with(
+            content: 'meta l4proto tcp ip saddr 192.168.122.0/24 ip daddr != 192.168.122.0/24 masquerade to :1024-65535',
+            table: 'ip-nat'
+          )
+        }
+        it {
+          is_expected.to contain_nftables__rule('POSTROUTING-qemu_masq_udp').with(
+            content: 'meta l4proto udp ip saddr 192.168.122.0/24 ip daddr != 192.168.122.0/24 masquerade to :1024-65535',
+            table: 'ip-nat'
+          )
+        }
+        it {
+          is_expected.to contain_nftables__rule('POSTROUTING-qemu_masq_ip').with(
+            content: 'ip saddr 192.168.122.0/24 ip daddr != 192.168.122.0/24 masquerade',
+            table: 'ip-nat'
+          )
+        }
+      end
+
+      context 'with all off' do
+        let(:params) do
+          {
+            dns: false,
+            dhcpv4: false,
+            forward_traffic: false,
+            internal_traffic: false,
+            masquerade: false,
+          }
+        end
+
+        it { is_expected.to compile }
+        it { is_expected.to have_resource_count(0) }
+      end
+
+      context 'ipv6 prefix' do
+        let(:params) do
+          {
+            network_v6: '20ac:cafe:1:1::/64',
+          }
+        end
+
+        it { is_expected.to compile }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_oip_v4').
+            with_content('oifname "virbr0" ip daddr 192.168.122.0/24 ct state related,established accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_iip_v4').
+            with_content('iifname "virbr0" ip saddr 192.168.122.0/24 accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_oip_v6').
+            with_content('oifname "virbr0" ip6 daddr 20ac:cafe:1:1::/64 ct state related,established accept')
+        }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_iip_v6').
+            with_content('iifname "virbr0" ip6 saddr 20ac:cafe:1:1::/64 accept')
+        }
+      end
+
+      context 'change interface' do
+        let(:params) do
+          {
+            interface: 'vfoo0'
+          }
+        end
+
+        it { is_expected.to compile }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_iip_v4').
+            with_content('iifname "vfoo0" ip saddr 192.168.122.0/24 accept')
+        }
+      end
+
+      context 'change ipv4 prefix' do
+        let(:params) do
+          {
+            network_v4: '172.16.0.0/12'
+          }
+        end
+
+        it { is_expected.to compile }
+        it {
+          is_expected.to contain_nftables__rule('default_fwd-qemu_iip_v4').
+            with_content('iifname "virbr0" ip saddr 172.16.0.0/12 accept')
+        }
+      end
+    end
+  end
+end
