fw4: support automatic includes
authorJo-Philipp Wich <jo@mein.io>
Thu, 11 Aug 2022 11:48:14 +0000 (13:48 +0200)
committerJo-Philipp Wich <jo@mein.io>
Fri, 12 Aug 2022 12:35:58 +0000 (14:35 +0200)
Introduce a new directory tree /usr/share/nftables.d/ which may contain
partial nftables files being included into the rendered ruleset.

The include position is derived from the file path;

 - Files in .../nftables.d/table-pre/ and .../nftables.d/table-post/ are
   included before and after the `table inet fw4 { ... }` declaration
   respectively

 - Files in .../nftables.d/ruleset-pre/ and .../nftables.d/ruleset-post/
   are included before the first chain and after the last chain
   declaration within the fw4 table respectively

 - Files in .../nftables.d/chain-pre/${chain}/ and .../chain-post/${chain}/
   are included before the first and after the last rule within the mentioned
   chain of the fw4 table respectively

Automatic includes can be disabled by setting the `auto_includes` option to
`0` in the global defaults section.

Also adjust testcases accordingly.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
15 files changed:
root/usr/share/nftables.d/README [new file with mode: 0644]
root/usr/share/ucode/fw4.uc
tests/01_configuration/01_ruleset
tests/01_configuration/02_rule_order
tests/05_includes/04_disabled_include [new file with mode: 0644]
tests/lib/mocklib/fs.uc
tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-post_nft.json [new file with mode: 0644]
tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-pre_nft.json [new file with mode: 0644]
tests/mocks/fs/glob~_usr_share_nftables_d_table-post_nft.json [new file with mode: 0644]
tests/mocks/fs/glob~_usr_share_nftables_d_table-pre_nft.json [new file with mode: 0644]
tests/mocks/fs/opendir~_usr_share_nftables_d_chain-post.json [new file with mode: 0644]
tests/mocks/fs/opendir~_usr_share_nftables_d_chain-pre.json [new file with mode: 0644]
tests/mocks/fs/open~_sys_class_net_br-lan_uevent.txt [new file with mode: 0644]
tests/mocks/fs/open~_sys_class_net_eth0_uevent.txt [new file with mode: 0644]
tests/mocks/fs/open~_sys_class_net_eth1_uevent.txt [new file with mode: 0644]

diff --git a/root/usr/share/nftables.d/README b/root/usr/share/nftables.d/README
new file mode 100644 (file)
index 0000000..e4aa9f8
--- /dev/null
@@ -0,0 +1,22 @@
+This directory may contain partial nftables files which are automatically
+included into the nftables ruleset generated by the fw4 program.
+
+Only accessible files (no broken symlinks, no files with insufficient
+permissions) with an `*.nft` file extension are considered.
+
+The include position of each file within the overall ruleset is derived
+from the file path:
+
+ - Files in ./table-pre/ and ./table-post/ are included before and after
+   the `table inet fw4 { ... }` declaration respectively
+
+ - Files in ./ruleset-pre/ and ./ruleset-post/ are included before the
+   first chain and after the last chain declaration within the fw4 table
+   respectively
+
+ - Files in ./chain-pre/${chain}/ and ./chain-post/${chain}/ are included
+   before the first and after the last rule within the mentioned chain of
+   the fw4 table respectively
+
+Automatic inclusion of these files can be disabled by setting the global
+`auto_includes` option within the defaults section of /etc/config/firewall.
index 2dc44ac9684b44e90754a0fda1a66f90c5899079..dcb13ad6cb00135529605e79388e1bf9fc48ee1c 100644 (file)
@@ -733,6 +733,19 @@ return {
                this.cursor.foreach("firewall", "include", i => self.parse_include(i));
 
 
+               //
+               // Discover automatic includes
+               //
+
+               if (this.default_option("auto_includes")) {
+                       for (let position in [ 'ruleset-pre', 'ruleset-post', 'table-pre', 'table-post', 'chain-pre', 'chain-post' ])
+                               for (let chain in (position in [ 'chain-pre', 'chain-post' ]) ? fs.lsdir(`/usr/share/nftables.d/${position}`) : [ null ])
+                                       for (let path in fs.glob(`/usr/share/nftables.d/${position}/${chain ?? ''}/*.nft`))
+                                               if (fs.access(path))
+                                                       this.parse_include({ type: 'nftables', position, chain, path });
+               }
+
+
                if (use_statefile) {
                        let fd = fs.open(STATEFILE, "w");
 
@@ -1876,7 +1889,9 @@ return {
                        custom_chains: [ "bool", null, UNSUPPORTED ],
                        disable_ipv6: [ "bool", null, UNSUPPORTED ],
                        flow_offloading: [ "bool", "0" ],
-                       flow_offloading_hw: [ "bool", "0" ]
+                       flow_offloading_hw: [ "bool", "0" ],
+
+                       auto_includes: [ "bool", "1" ]
                });
 
                if (defs.synflood_protect === null)
@@ -3153,6 +3168,9 @@ return {
                        return;
                }
 
+               if (!data['.name'])
+                       this.warn(`Automatically including '${path}'`);
+
                push(this.state.includes ||= [], { ...inc, path });
        },
 
index 1bf8f721de92f972a878452a2134d88bdc922010..06249f257910040eb704e7d611bad69dc55d7131 100644 (file)
@@ -303,6 +303,12 @@ table inet fw4 {
 [!] Section @defaults[0] specifies unknown option 'unknown_defaults_option'
 [!] Section @rule[9] (Test-Deprecated-Rule-Option) option '_name' is deprecated by fw4
 [!] Section @rule[9] (Test-Deprecated-Rule-Option) specifies unknown option 'unknown_rule_option'
+[call] fs.glob pattern </usr/share/nftables.d/ruleset-pre//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/ruleset-post//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/table-pre//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/table-post//*.nft>
+[call] fs.lsdir path </usr/share/nftables.d/chain-pre>
+[call] fs.lsdir path </usr/share/nftables.d/chain-post>
 [call] ctx.call object <network.device> method <status> args <null>
 [call] fs.opendir path </sys/class/net/br-lan>
 [call] fs.opendir path </sys/class/net/eth0>
index 3c1546e025e93bfca687128440b430ca74bbffad..245bb74dedabccd18159c9bb411052051a0d5ff2 100644 (file)
@@ -229,5 +229,11 @@ table inet fw4 {
 [call] ctx.call object <network.interface> method <dump> args <null>
 [call] ctx.call object <service> method <get_data> args <{ "type": "firewall" }>
 [call] fs.open path </proc/version> mode <r>
+[call] fs.glob pattern </usr/share/nftables.d/ruleset-pre//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/ruleset-post//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/table-pre//*.nft>
+[call] fs.glob pattern </usr/share/nftables.d/table-post//*.nft>
+[call] fs.lsdir path </usr/share/nftables.d/chain-pre>
+[call] fs.lsdir path </usr/share/nftables.d/chain-post>
 [call] fs.popen cmdline </usr/sbin/nft --terse --json list flowtables inet> mode <r>
 -- End --
diff --git a/tests/05_includes/04_disabled_include b/tests/05_includes/04_disabled_include
new file mode 100644 (file)
index 0000000..ac0a6c8
--- /dev/null
@@ -0,0 +1,205 @@
+Testing that include sections with `option enabled 0` are skipped.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File fs/open~_sys_class_net_eth0_flags.txt --
+0x1103
+-- End --
+
+-- File fs/open~_etc_testinclude1_nft.txt --
+# dummy
+-- End --
+
+-- File fs/open~_etc_testinclude2_nft.txt --
+# dummy
+-- End --
+
+-- File fs/open~_etc_testinclude3_nft.txt --
+# dummy
+-- End --
+
+-- File uci/firewall.json --
+{
+       "zone": [
+               {
+                       "name": "test",
+                       "device": [ "eth0" ],
+                       "auto_helper": 0
+               }
+       ],
+       "include": [
+               {
+                       ".description": "By default, this include should be processed due to implicit enabled 1",
+                       "path": "/etc/testinclude1.nft",
+                       "type": "nftables"
+               },
+
+               {
+                       ".description": "This include should be processed due to explicit enabled 1",
+                       "path": "/etc/testinclude2.nft",
+                       "type": "nftables",
+                       "enabled": "1"
+               },
+
+               {
+                       ".description": "This include should be skipped due to explicit enabled 0",
+                       "path": "/etc/testinclude3.nft",
+                       "type": "nftables",
+                       "enabled": "0"
+               }
+       ]
+}
+-- End --
+
+-- Expect stderr --
+[!] Section @include[2] is disabled, ignoring section
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Defines
+       #
+
+       define test_devices = { "eth0" }
+       define test_subnets = {  }
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+               iifname "eth0" jump input_test comment "!fw4: Handle test IPv4/IPv6 input traffic"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+               iifname "eth0" jump forward_test comment "!fw4: Handle test IPv4/IPv6 forward traffic"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               oifname "eth0" jump output_test comment "!fw4: Handle test IPv4/IPv6 output traffic"
+       }
+
+       chain prerouting {
+               type filter hook prerouting priority filter; policy accept;
+       }
+
+       chain handle_reject {
+               meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
+               reject with icmpx type port-unreachable comment "!fw4: Reject any other traffic"
+       }
+
+       chain input_test {
+               jump drop_from_test
+       }
+
+       chain output_test {
+               jump drop_to_test
+       }
+
+       chain forward_test {
+               jump drop_to_test
+       }
+
+       chain drop_from_test {
+               iifname "eth0" counter drop comment "!fw4: drop test IPv4/IPv6 traffic"
+       }
+
+       chain drop_to_test {
+               oifname "eth0" counter drop comment "!fw4: drop test IPv4/IPv6 traffic"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+
+       #
+       # Mangle rules
+       #
+
+       chain mangle_prerouting {
+               type filter hook prerouting priority mangle; policy accept;
+       }
+
+       chain mangle_postrouting {
+               type filter hook postrouting priority mangle; policy accept;
+       }
+
+       chain mangle_input {
+               type filter hook input priority mangle; policy accept;
+       }
+
+       chain mangle_output {
+               type route hook output priority mangle; policy accept;
+       }
+
+       chain mangle_forward {
+               type filter hook forward priority mangle; policy accept;
+       }
+
+       include "/etc/testinclude1.nft"
+       include "/etc/testinclude2.nft"
+}
+-- End --
index 61ad0b9dee88c5fc516dd95017cf85c489a7c80f..6482e6ab95d8f25c4671708fee7b155ff0e66b12 100644 (file)
@@ -200,5 +200,39 @@ return {
                };
        },
 
+       glob: (pattern) => {
+               let file = sprintf("fs/glob~%s.json", replace(pattern, /[^A-Za-z0-9_-]+/g, '_')),
+                   mock = mocklib.read_json_file(file),
+                   index = 0;
+
+               if (!mock || mock != mock) {
+                       mocklib.I("No stat result fixture defined for fs.glob() call on %s.", pattern);
+                       mocklib.I("Provide a mock result through the following JSON file:\n%s\n", file);
+
+                       mock = [];
+               }
+
+               mocklib.trace_call("fs", "glob", { pattern });
+
+               return mock;
+       },
+
+       lsdir: (path) => {
+               let file = sprintf("fs/opendir~%s.json", replace(path, /[^A-Za-z0-9_-]+/g, '_')),
+                   mock = mocklib.read_json_file(file),
+                   index = 0;
+
+               if (!mock || mock != mock) {
+                       mocklib.I("No stat result fixture defined for fs.lsdir() call on %s.", path);
+                       mocklib.I("Provide a mock result through the following JSON file:\n%s\n", file);
+
+                       mock = [];
+               }
+
+               mocklib.trace_call("fs", "lsdir", { path });
+
+               return mock;
+       },
+
        error: () => "Unspecified error"
 };
diff --git a/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-post_nft.json b/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-post_nft.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-pre_nft.json b/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-pre_nft.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/glob~_usr_share_nftables_d_table-post_nft.json b/tests/mocks/fs/glob~_usr_share_nftables_d_table-post_nft.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/glob~_usr_share_nftables_d_table-pre_nft.json b/tests/mocks/fs/glob~_usr_share_nftables_d_table-pre_nft.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-post.json b/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-post.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-pre.json b/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-pre.json
new file mode 100644 (file)
index 0000000..fe51488
--- /dev/null
@@ -0,0 +1 @@
+[]
diff --git a/tests/mocks/fs/open~_sys_class_net_br-lan_uevent.txt b/tests/mocks/fs/open~_sys_class_net_br-lan_uevent.txt
new file mode 100644 (file)
index 0000000..d019219
--- /dev/null
@@ -0,0 +1,3 @@
+DEVTYPE=bridge
+INTERFACE=switch0
+IFINDEX=12
diff --git a/tests/mocks/fs/open~_sys_class_net_eth0_uevent.txt b/tests/mocks/fs/open~_sys_class_net_eth0_uevent.txt
new file mode 100644 (file)
index 0000000..f6ada71
--- /dev/null
@@ -0,0 +1,6 @@
+DEVTYPE=dsa
+OF_NAME=port
+OF_FULLNAME=/ethernet@1e100000/mdio-bus/switch@1f/ports/port@0
+OF_COMPATIBLE_N=0
+INTERFACE=eth0
+IFINDEX=3
diff --git a/tests/mocks/fs/open~_sys_class_net_eth1_uevent.txt b/tests/mocks/fs/open~_sys_class_net_eth1_uevent.txt
new file mode 100644 (file)
index 0000000..6db7cfd
--- /dev/null
@@ -0,0 +1,6 @@
+DEVTYPE=dsa
+OF_NAME=port
+OF_FULLNAME=/ethernet@1e100000/mdio-bus/switch@1f/ports/port@1
+OF_COMPATIBLE_N=0
+INTERFACE=eth1
+IFINDEX=4