diff --git a/README.md b/README.md
index a77aa8a7..8c7ae1f1 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,14 @@ Initially puppet deploys all configuration to
If and only if successful the configuration will be copied to
the real locations before the service is reloaded.
+## Un-managed rules
+
+By default, rules added manually by the administrator to the in-memory
+ruleset will be left untouched. However,
+`nftables::purge_unmanaged_rules` can be set to `true` to revert this
+behaviour and force a reload of the ruleset during the Puppet run if
+non-managed changes are detected.
+
## Basic types
### nftables::config
diff --git a/REFERENCE.md b/REFERENCE.md
index 1497e7ea..750dd976 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -162,6 +162,8 @@ The following parameters are available in the `nftables` class:
* [`inet_filter`](#-nftables--inet_filter)
* [`nat`](#-nftables--nat)
* [`nat_table_name`](#-nftables--nat_table_name)
+* [`purge_unmanaged_rules`](#-nftables--purge_unmanaged_rules)
+* [`inmem_rules_hash_file`](#-nftables--inmem_rules_hash_file)
* [`sets`](#-nftables--sets)
* [`log_prefix`](#-nftables--log_prefix)
* [`log_discarded`](#-nftables--log_discarded)
@@ -270,6 +272,25 @@ The name of the 'nat' table.
Default value: `'nat'`
+##### `purge_unmanaged_rules`
+
+Data type: `Boolean`
+
+Prohibits in-memory rules that are not declared in Puppet
+code. Setting this to true activates a check that reloads nftables
+if the rules in memory have been modified without Puppet.
+
+Default value: `false`
+
+##### `inmem_rules_hash_file`
+
+Data type: `Stdlib::Unixpath`
+
+The name of the file where the hash of the in-memory rules
+will be stored.
+
+Default value: `'/var/tmp/puppet-nft-memhash'`
+
##### `sets`
Data type: `Hash`
diff --git a/manifests/init.pp b/manifests/init.pp
index 1b9ea057..f1255127 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -46,6 +46,15 @@
# @param nat_table_name
# The name of the 'nat' table.
#
+# @param purge_unmanaged_rules
+# Prohibits in-memory rules that are not declared in Puppet
+# code. Setting this to true activates a check that reloads nftables
+# if the rules in memory have been modified without Puppet.
+#
+# @param inmem_rules_hash_file
+# The name of the file where the hash of the in-memory rules
+# will be stored.
+#
# @param sets
# Allows sourcing set definitions directly from Hiera.
#
@@ -134,10 +143,12 @@
Boolean $fwd_drop_invalid = $fwd_conntrack,
Boolean $inet_filter = true,
Boolean $nat = true,
+ Boolean $purge_unmanaged_rules = false,
Hash $rules = {},
Hash $sets = {},
String $log_prefix = '[nftables] %s %s',
String[1] $nat_table_name = 'nat',
+ Stdlib::Unixpath $inmem_rules_hash_file = '/var/tmp/puppet-nft-memhash',
Boolean $log_discarded = true,
Variant[Boolean[false], String] $log_limit = '3/minute burst 5 packets',
Variant[Boolean[false], Pattern[/icmp(v6|x)? type .+|tcp reset/]] $reject_with = 'icmpx type port-unreachable',
@@ -221,6 +232,26 @@
restart => 'PATH=/usr/bin:/bin systemctl reload nftables',
}
+ if $purge_unmanaged_rules {
+ # Reload nftables ruleset from disk if running state not match last service change hash, or is absent (-s required to ignore counters)
+ exec { 'nftables_memory_state_check':
+ command => ['echo', 'reloading_nftables'],
+ path => $facts['path'],
+ provider => shell,
+ unless => ["test -s ${inmem_rules_hash_file} -a \"$(nft -s list ruleset | sha1sum)\" = \"$(cat ${inmem_rules_hash_file})\""],
+ notify => Service['nftables'],
+ }
+
+ # Generate nftables hash upon changes to the nftables service
+ exec { 'nftables_generate_hash':
+ command => ["nft -s list ruleset | sha1sum > ${inmem_rules_hash_file}"],
+ path => $facts['path'],
+ provider => shell,
+ subscribe => Service['nftables'],
+ refreshonly => true,
+ }
+ }
+
systemd::dropin_file { 'puppet_nft.conf':
ensure => present,
unit => 'nftables.service',
diff --git a/spec/classes/nftables_spec.rb b/spec/classes/nftables_spec.rb
index 36a68438..b4830e91 100644
--- a/spec/classes/nftables_spec.rb
+++ b/spec/classes/nftables_spec.rb
@@ -130,6 +130,18 @@
)
}
+ it {
+ expect(subject).not_to contain_exec('nftables_memory_state_check')
+ }
+
+ it {
+ expect(subject).not_to contain_exec('nftables_generate_hash')
+ }
+
+ it {
+ expect(subject).not_to contain_file('/var/tmp/puppet-nft-memhash')
+ }
+
it {
expect(subject).to contain_exec('nft validate').with(
refreshonly: true,
@@ -298,6 +310,31 @@
it { is_expected.to have_nftables__set_resource_count(0) }
end
+ context 'when purging unmanaged rules' do
+ let(:params) do
+ {
+ 'purge_unmanaged_rules' => true,
+ 'inmem_rules_hash_file' => '/foo/bar',
+ }
+ end
+
+ it {
+ is_expected.to contain_exec('nftables_memory_state_check').with(
+ command: %w[echo reloading_nftables],
+ notify: 'Service[nftables]',
+ unless: ['test -s /foo/bar -a "$(nft -s list ruleset | sha1sum)" = "$(cat /foo/bar)"']
+ )
+ }
+
+ it {
+ is_expected.to contain_exec('nftables_generate_hash').with(
+ command: ['nft -s list ruleset | sha1sum > /foo/bar'],
+ subscribe: 'Service[nftables]',
+ refreshonly: true
+ )
+ }
+ end
+
%w[ip ip6 inet arp bridge netdev].each do |family|
context "with noflush_tables parameter set to valid family #{family}" do
let(:params) do