commit e53053ce3df771f5fcc24f87dd86f5c49db91b46
Author: Steve Traylen <steve.traylen@cern.ch>
Date:   Wed Nov 18 17:01:11 2020 +0100

    Add comments for all the nftable::rules entries
    
    For each nftable::rule this adds an extra concat fragment to
    add a comment containing the name and order number for the rule.
    
    The motivation here is to make the mapping from resulting rules back
    to puppet code more obvious. When adding a new rule it should be more
    obvious to understand what order to choose.
    
    An example resulting table ends up reading as:
    
    ```
    HASH Start of fragment order:00 default_in header
    chain default_in {
    HASH   Start of fragment order:50 rulename:default_in-dhcpv6_client
      ip6 saddr fe80::/10 ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept
    HASH   Start of fragment order:50 rulename:default_in-ssh
      tcp dport {22} accept
    HASH   Start of fragment order:90 rulename:default_in-drop_broadcasts
      meta pkttype broadcast counter drop
    HASH Start of fragment order:99 default_in footer
    }
    
    ```
    
    In addition there is a new test `nftables::rule`. This includes
    a pending test since I would assume setting source and content
    on a rule should be an error however this currently not the case.

diff --git a/manifests/chain.pp b/manifests/chain.pp
index d53b3cf..26dbe51 100644
--- a/manifests/chain.pp
+++ b/manifests/chain.pp
@@ -29,10 +29,10 @@ define nftables::chain(
       target => $concat_name;
     "${concat_name}-header":
       order   => '00',
-      content => "chain ${chain} {";
+      content => "# Start of fragment order:00 ${chain} header\nchain ${chain} {";
     "${concat_name}-footer":
       order   => '99',
-      content => '}';
+      content => "# Start of fragment order:99 ${chain} footer\n}";
   }
 
   if $inject {
diff --git a/manifests/rule.pp b/manifests/rule.pp
index d9aeadc..0d9919d 100644
--- a/manifests/rule.pp
+++ b/manifests/rule.pp
@@ -25,9 +25,15 @@ define nftables::rule(
       $fragment = "nftables-${table}-chain-${data[0]}-rule-${data[1]}"
     }
 
+    concat::fragment{"${fragment}_header":
+      content => "#   Start of fragment order:${order} rulename:${rulename}",
+      order   => "${order}${fragment}a",
+      target  => "nftables-${table}-chain-${data[0]}",
+    }
+
     concat::fragment{
       $fragment:
-        order  => $order,
+        order  => "${order}${fragment}b",
         target => "nftables-${table}-chain-${data[0]}",
     }
 
diff --git a/spec/classes/bridges_spec.rb b/spec/classes/bridges_spec.rb
index 0187299..f4605bb 100644
--- a/spec/classes/bridges_spec.rb
+++ b/spec/classes/bridges_spec.rb
@@ -29,14 +29,14 @@ describe 'nftables' do
         is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-rule-bridge_br0_br0').with(
           target:  'nftables-inet-filter-chain-default_fwd',
           content: %r{^  iifname br0 oifname br0 accept$},
-          order:   '08',
+          order:   '08nftables-inet-filter-chain-default_fwd-rule-bridge_br0_br0b',
         )
       }
       it {
         is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-rule-bridge_br1_br1').with(
           target:  'nftables-inet-filter-chain-default_fwd',
           content: %r{^  iifname br1 oifname br1 accept$},
-          order:   '08',
+          order:   '08nftables-inet-filter-chain-default_fwd-rule-bridge_br1_br1b',
         )
       }
       it { is_expected.not_to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-bridge_br0_br1') }
diff --git a/spec/classes/dnat4_spec.rb b/spec/classes/dnat4_spec.rb
index 3b385c9..d55173d 100644
--- a/spec/classes/dnat4_spec.rb
+++ b/spec/classes/dnat4_spec.rb
@@ -70,7 +70,7 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-rule-jump_ingoing').with(
             target:  'nftables-inet-filter-chain-default_fwd',
             content: %r{^  iifname eth0 oifname eth1 jump ingoing$},
-            order:   '20',
+            order:   '20nftables-inet-filter-chain-default_fwd-rule-jump_ingoingb',
           )
         }
         it {
@@ -92,28 +92,28 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-ingoing-rule-http').with(
             target:  'nftables-inet-filter-chain-ingoing',
             content: %r{^  ip daddr 192.0.2.2 tcp dport http accept$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-ingoing-rule-httpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-ingoing-rule-https').with(
             target:  'nftables-inet-filter-chain-ingoing',
             content: %r{^  ip daddr 192.0.2.2 tcp dport https accept$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-ingoing-rule-httpsb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-ingoing-rule-http_alt').with(
             target:  'nftables-inet-filter-chain-ingoing',
             content: %r{^  iifname eth0 ip daddr 192.0.2.2 tcp dport 8000 accept$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-ingoing-rule-http_altb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-ingoing-rule-wireguard').with(
             target:  'nftables-inet-filter-chain-ingoing',
             content: %r{^  iifname eth0 ip daddr 192.0.2.3 udp dport 51820 accept$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-ingoing-rule-wireguardb',
           )
         }
         it {
@@ -144,42 +144,42 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  type nat hook prerouting priority -100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-PREROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-PREROUTING-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-http').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  tcp dport http dnat to 192.0.2.2$},
-            order:   '10',
+            order:   '10nftables-ip-nat-chain-PREROUTING-rule-httpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-https').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  tcp dport https dnat to 192.0.2.2$},
-            order:   '10',
+            order:   '10nftables-ip-nat-chain-PREROUTING-rule-httpsb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-http_alt').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  iifname eth0 tcp dport 8080 dnat to 192.0.2.2:8000$},
-            order:   '10',
+            order:   '10nftables-ip-nat-chain-PREROUTING-rule-http_altb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-wireguard').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  iifname eth0 udp dport 51820 dnat to 192.0.2.3$},
-            order:   '10',
+            order:   '10nftables-ip-nat-chain-PREROUTING-rule-wireguardb',
           )
         }
         it {
diff --git a/spec/classes/inet_filter_spec.rb b/spec/classes/inet_filter_spec.rb
index a00f60e..3045c8f 100644
--- a/spec/classes/inet_filter_spec.rb
+++ b/spec/classes/inet_filter_spec.rb
@@ -63,49 +63,49 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-type').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  type filter hook input priority 0$},
-            order:   '01',
+            order:   '01nftables-inet-filter-chain-INPUT-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-policy').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  policy drop$},
-            order:   '02',
+            order:   '02nftables-inet-filter-chain-INPUT-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-lo').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  iifname lo accept$},
-            order:   '03',
+            order:   '03nftables-inet-filter-chain-INPUT-rule-lob',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-jump_global').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  jump global$},
-            order:   '04',
+            order:   '04nftables-inet-filter-chain-INPUT-rule-jump_globalb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-jump_default_in').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  jump default_in$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-INPUT-rule-jump_default_inb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  log prefix \"\[nftables\] INPUT Rejected: \" flags all counter$},
-            order:   '97',
+            order:   '97nftables-inet-filter-chain-INPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-reject').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  reject with icmpx type port-unreachable$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-INPUT-rule-rejectb',
           )
         }
         it {
@@ -143,7 +143,7 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_in-rule-ssh').with(
             target:  'nftables-inet-filter-chain-default_in',
             content: %r{^  tcp dport \{22\} accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_in-rule-sshb',
           )
         }
       end
@@ -169,49 +169,49 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-type').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  type filter hook output priority 0$},
-            order:   '01',
+            order:   '01nftables-inet-filter-chain-OUTPUT-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-policy').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  policy drop$},
-            order:   '02',
+            order:   '02nftables-inet-filter-chain-OUTPUT-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-lo').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  oifname lo accept$},
-            order:   '03',
+            order:   '03nftables-inet-filter-chain-OUTPUT-rule-lob',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-jump_global').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  jump global$},
-            order:   '04',
+            order:   '04nftables-inet-filter-chain-OUTPUT-rule-jump_globalb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-jump_default_out').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  jump default_out$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-OUTPUT-rule-jump_default_outb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  log prefix \"\[nftables\] OUTPUT Rejected: \" flags all counter$},
-            order:   '97',
+            order:   '97nftables-inet-filter-chain-OUTPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-reject').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  reject with icmpx type port-unreachable$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-OUTPUT-rule-rejectb',
           )
         }
         it {
@@ -249,35 +249,35 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnsudp').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  udp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnsudpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnstcp').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  tcp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnstcpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-chrony').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  udp dport 123 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-chronyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-http').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  tcp dport 80 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-httpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-https').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  tcp dport 443 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-httpsb',
           )
         }
       end
@@ -303,42 +303,42 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-type').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  type filter hook forward priority 0$},
-            order:   '01',
+            order:   '01nftables-inet-filter-chain-FORWARD-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-policy').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  policy drop$},
-            order:   '02',
+            order:   '02nftables-inet-filter-chain-FORWARD-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-jump_global').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  jump global$},
-            order:   '03',
+            order:   '03nftables-inet-filter-chain-FORWARD-rule-jump_globalb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-jump_default_fwd').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  jump default_fwd$},
-            order:   '10',
+            order:   '10nftables-inet-filter-chain-FORWARD-rule-jump_default_fwdb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  log prefix \"\[nftables\] FORWARD Rejected: \" flags all counter$},
-            order:   '97',
+            order:   '97nftables-inet-filter-chain-FORWARD-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-reject').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  reject with icmpx type port-unreachable$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-FORWARD-rule-rejectb',
           )
         }
         it {
@@ -375,61 +375,53 @@ describe 'nftables' do
       end
 
       context 'custom log prefix without variable substitution' do
-        let(:params) do
-          {
-            'log_prefix' => 'test',
-          }
-        end
+        let(:pre_condition) { 'class{\'nftables\': log_prefix => "test "}' }
 
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-INPUT',
-            content: %r{^  log prefix "test" flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \"test " flags all counter$},
+            order:   '97nftables-inet-filter-chain-INPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
-            content: %r{^  log prefix "test" flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \"test " flags all counter$},
+            order:   '97nftables-inet-filter-chain-OUTPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-FORWARD',
-            content: %r{^  log prefix "test" flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \"test " flags all counter$},
+            order:   '97nftables-inet-filter-chain-FORWARD-rule-log_discardedb',
           )
         }
       end
 
       context 'custom log prefix with variable substitution' do
-        let(:params) do
-          {
-            'log_prefix' => ' bar [%<chain>s] ', # rubocop:disable Style/FormatStringToken
-          }
-        end
+        let(:pre_condition) { 'class{\'nftables\': log_prefix => " bar [%<chain>s] "}' } # rubocop:disable Style/FormatStringToken
 
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-INPUT',
-            content: %r{^  log prefix " bar \[INPUT\] " flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \" bar \[INPUT\] " flags all counter$},
+            order:   '97nftables-inet-filter-chain-INPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
-            content: %r{^  log prefix " bar \[OUTPUT\] " flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \" bar \[OUTPUT\] " flags all counter$},
+            order:   '97nftables-inet-filter-chain-OUTPUT-rule-log_discardedb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-log_discarded').with(
             target:  'nftables-inet-filter-chain-FORWARD',
-            content: %r{^  log prefix " bar \[FORWARD\] " flags all counter$},
-            order:   '97',
+            content: %r{^  log prefix \" bar \[FORWARD\] " flags all counter$},
+            order:   '97nftables-inet-filter-chain-FORWARD-rule-log_discardedb',
           )
         }
       end
@@ -472,21 +464,21 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-reject').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  reject with tcp reset$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-INPUT-rule-rejectb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-OUTPUT-rule-reject').with(
             target:  'nftables-inet-filter-chain-OUTPUT',
             content: %r{^  reject with tcp reset$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-OUTPUT-rule-rejectb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-FORWARD-rule-reject').with(
             target:  'nftables-inet-filter-chain-FORWARD',
             content: %r{^  reject with tcp reset$},
-            order:   '98',
+            order:   '98nftables-inet-filter-chain-FORWARD-rule-rejectb',
           )
         }
       end
diff --git a/spec/classes/ip_nat_spec.rb b/spec/classes/ip_nat_spec.rb
index 23e9eed..9a54558 100644
--- a/spec/classes/ip_nat_spec.rb
+++ b/spec/classes/ip_nat_spec.rb
@@ -96,14 +96,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  type nat hook prerouting priority -100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-PREROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-PREROUTING-rule-policyb',
           )
         }
         it {
@@ -136,14 +136,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  type nat hook postrouting priority 100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-POSTROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-POSTROUTING-rule-policyb',
           )
         }
         it {
@@ -176,14 +176,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip6-nat-chain-PREROUTING6-rule-type').with(
             target:  'nftables-ip6-nat-chain-PREROUTING6',
             content: %r{^  type nat hook prerouting priority -100$},
-            order:   '01',
+            order:   '01nftables-ip6-nat-chain-PREROUTING6-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip6-nat-chain-PREROUTING6-rule-policy').with(
             target:  'nftables-ip6-nat-chain-PREROUTING6',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip6-nat-chain-PREROUTING6-rule-policyb',
           )
         }
         it {
@@ -216,14 +216,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip6-nat-chain-POSTROUTING6-rule-type').with(
             target:  'nftables-ip6-nat-chain-POSTROUTING6',
             content: %r{^  type nat hook postrouting priority 100$},
-            order:   '01',
+            order:   '01nftables-ip6-nat-chain-POSTROUTING6-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip6-nat-chain-POSTROUTING6-rule-policy').with(
             target:  'nftables-ip6-nat-chain-POSTROUTING6',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip6-nat-chain-POSTROUTING6-rule-policyb',
           )
         }
         it {
diff --git a/spec/classes/masquerade_spec.rb b/spec/classes/masquerade_spec.rb
index 501c82a..c095cd1 100644
--- a/spec/classes/masquerade_spec.rb
+++ b/spec/classes/masquerade_spec.rb
@@ -54,49 +54,49 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  type nat hook postrouting priority 100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-POSTROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-POSTROUTING-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade_eth0').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  oifname eth0 masquerade$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-masquerade_eth0b',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade_eth1_vpn').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  oifname eth1 ip saddr 192\.0\.2\.0\/24 masquerade$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-masquerade_eth1_vpnb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade_ssh').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  ip saddr 192\.0\.2\.0\/24 ip daddr 198.51.100.2 tcp dport 22 masquerade$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-masquerade_sshb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade_ssh_gitlab').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  ip saddr 192\.0\.2\.0\/24 ip daddr 198.51.100.2 tcp dport 22 masquerade$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-masquerade_ssh_gitlabb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade_wireguard').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  udp dport 51820 masquerade$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-masquerade_wireguardb',
           )
         }
         it {
diff --git a/spec/classes/nftables_spec.rb b/spec/classes/nftables_spec.rb
index d67f9ef..bf4c002 100644
--- a/spec/classes/nftables_spec.rb
+++ b/spec/classes/nftables_spec.rb
@@ -85,7 +85,7 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-INPUT-rule-web_accept').with(
             target:  'nftables-inet-filter-chain-INPUT',
             content: %r{^  iifname eth0 tcp dport \{ 80, 443 \} accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-INPUT-rule-web_acceptb',
           )
         }
       end
diff --git a/spec/classes/router_spec.rb b/spec/classes/router_spec.rb
index d272b2c..3e6fe7d 100644
--- a/spec/classes/router_spec.rb
+++ b/spec/classes/router_spec.rb
@@ -50,14 +50,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-rule-out').with(
             target:  'nftables-inet-filter-chain-default_fwd',
             content: %r{^  iifname eth1 oifname eth0 accept$},
-            order:   '20',
+            order:   '20nftables-inet-filter-chain-default_fwd-rule-outb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_fwd-rule-drop').with(
             target:  'nftables-inet-filter-chain-default_fwd',
             content: %r{^  iifname eth0 drop$},
-            order:   '90',
+            order:   '90nftables-inet-filter-chain-default_fwd-rule-dropb',
           )
         }
         it {
@@ -88,14 +88,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  type nat hook prerouting priority -100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-PREROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-PREROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-PREROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-PREROUTING-rule-policyb',
           )
         }
         it {
@@ -126,21 +126,21 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  type nat hook postrouting priority 100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-POSTROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-POSTROUTING-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-masquerade').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  oifname eth0 masquerade$},
-            order:   '20',
+            order:   '20nftables-ip-nat-chain-POSTROUTING-rule-masqueradeb',
           )
         }
         it {
diff --git a/spec/classes/rules_out_dns_spec.rb b/spec/classes/rules_out_dns_spec.rb
index 9e60148..5f0df42 100644
--- a/spec/classes/rules_out_dns_spec.rb
+++ b/spec/classes/rules_out_dns_spec.rb
@@ -18,14 +18,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnsudp').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  udp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnsudpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnstcp').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  tcp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnstcpb',
           )
         }
       end
@@ -45,14 +45,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnsudp-0').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  ip daddr 192.0.2.1 udp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnsudp-0b',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnstcp-0').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  ip daddr 192.0.2.1 tcp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnstcp-0b',
           )
         }
 
@@ -60,14 +60,14 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnsudp-1').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  ip6 daddr 2001:db8::1 udp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnsudp-1b',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-inet-filter-chain-default_out-rule-dnstcp-1').with(
             target:  'nftables-inet-filter-chain-default_out',
             content: %r{^  ip6 daddr 2001:db8::1 tcp dport 53 accept$},
-            order:   '50',
+            order:   '50nftables-inet-filter-chain-default_out-rule-dnstcp-1b',
           )
         }
       end
diff --git a/spec/classes/snat4_spec.rb b/spec/classes/snat4_spec.rb
index c78db62..ba16ea5 100644
--- a/spec/classes/snat4_spec.rb
+++ b/spec/classes/snat4_spec.rb
@@ -55,42 +55,42 @@ describe 'nftables' do
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-type').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  type nat hook postrouting priority 100$},
-            order:   '01',
+            order:   '01nftables-ip-nat-chain-POSTROUTING-rule-typeb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-policy').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  policy accept$},
-            order:   '02',
+            order:   '02nftables-ip-nat-chain-POSTROUTING-rule-policyb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-static').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  oifname eth0 snat 198\.51\.100\.1$},
-            order:   '60',
+            order:   '60nftables-ip-nat-chain-POSTROUTING-rule-staticb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-1_1').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  oifname eth0 ip saddr 192\.0\.2\.2 snat 198\.51\.100\.3$},
-            order:   '61',
+            order:   '61nftables-ip-nat-chain-POSTROUTING-rule-1_1b',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-1_1_smtp').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  ip saddr 192\.0\.2\.2 tcp dport 25 snat 198\.51\.100\.2$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-1_1_smtpb',
           )
         }
         it {
           is_expected.to contain_concat__fragment('nftables-ip-nat-chain-POSTROUTING-rule-1_1_wireguard').with(
             target:  'nftables-ip-nat-chain-POSTROUTING',
             content: %r{^  ip saddr 192\.0\.2\.2 udp dport 51820 snat 198\.51\.100\.2$},
-            order:   '70',
+            order:   '70nftables-ip-nat-chain-POSTROUTING-rule-1_1_wireguardb',
           )
         }
         it {
diff --git a/spec/defines/rule_spec.rb b/spec/defines/rule_spec.rb
new file mode 100644
index 0000000..1cf01e6
--- /dev/null
+++ b/spec/defines/rule_spec.rb
@@ -0,0 +1,167 @@
+require 'spec_helper'
+
+describe 'nftables::rule' do
+  let(:title) { 'out-foo' }
+
+  on_supported_os.each do |os, facts|
+    context "on #{os}" do
+      let(:facts) do
+        facts
+      end
+
+      context 'with title set to <CHAIN_NAME>-<RULE>' do
+        let(:title) { 'CHAIN_NAME-RULE' }
+
+        context 'with source and content both unset' do
+          it { is_expected.not_to compile }
+        end
+        context 'with source and content both set' do
+          let(:params) do
+            {
+              source: 'foo',
+              content: 'puppet:///modules/foo/bar',
+            }
+          end
+
+          it {
+            pending('Setting source and content should be made to fail')
+            is_expected.not_to compile
+          }
+        end
+
+        context 'with content parameter set' do
+          let(:params) do
+            {
+              content: 'port 22 allow',
+            }
+          end
+
+          it { is_expected.to compile.with_all_deps }
+          it { is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE') }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE_header').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULEa',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              content: %r{^#.*$},
+            }
+          }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULEb',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              content: '  port 22 allow',
+            }
+          }
+          context 'with optional parameters set' do
+            let(:params) do
+              super().merge(order: '85',
+                            table: 'TABLE')
+            end
+
+            it {
+              is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE_header').with
+              {
+                order: '85nftables-TABLE-chain-CHAIN_NAME-rule-RULEa',
+                target: 'nftables-TABLE-chain-CHAIN_NAME',
+                content: %r{^#.*$},
+              }
+            }
+            it { is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE') }
+            it {
+              is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE').with
+              {
+                order: '85nftables-TABLE-chain-CHAIN_NAME-rule-RULEb',
+                target: 'nftables-TABLE-chain-CHAIN_NAME',
+                content: '  port 22 allow',
+              }
+            }
+          end
+        end
+
+        context 'with source parameter set' do
+          let(:params) do
+            {
+              source: 'puppet:///modules/foo/bar',
+            }
+          end
+
+          it { is_expected.to compile.with_all_deps }
+          it { is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE') }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE_header').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULEa',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              content: %r{^#.*$},
+            }
+          }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULEb',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              source: 'puppet:///modules/foo/bar',
+            }
+          }
+          context 'with optional parameters set' do
+            let(:params) do
+              super().merge(order: '85',
+                            table: 'TABLE')
+            end
+
+            it {
+              is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE_header').with
+              {
+                order: '85nftables-TABLE-chain-CHAIN_NAME-rule-RULEa',
+                target: 'nftables-TABLE-chain-CHAIN_NAME',
+                content: %r{^#.*$},
+              }
+            }
+            it { is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE') }
+            it {
+              is_expected.to contain_concat__fragment('nftables-TABLE-chain-CHAIN_NAME-rule-RULE').with
+              {
+                order: '85nftables-TABLE-chain-CHAIN_NAME-rule-RULEb',
+                target: 'nftables-TABLE-chain-CHAIN_NAME',
+                source: 'puppet:///modules/foo/bar',
+              }
+            }
+          end
+        end
+      end
+
+      context 'with title set to <CHAIN_NAME>-<RULE>-22' do
+        let(:title) { 'CHAIN_NAME-RULE-22' }
+
+        context 'with content parameter set' do
+          let(:params) do
+            {
+              content: 'port 22 allow',
+            }
+          end
+
+          it { is_expected.to compile.with_all_deps }
+          it { is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE-22') }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE-22_header').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULE-22a',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              content: %r{^#.*$},
+            }
+          }
+          it {
+            is_expected.to contain_concat__fragment('nftables-inet-filter-chain-CHAIN_NAME-rule-RULE-22').with
+            {
+              order: '50nftables-inet-filter-chain-CHAIN_NAME-rule-RULE-22b',
+              target: 'nftables-inet-filter-chain-CHAIN_NAME',
+              content: '  port 22 allow',
+            }
+          }
+        end
+      end
+    end
+  end
+end
