From a4484d4612931800583a7219271b63224491244c Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Thu, 11 Aug 2022 13:48:14 +0200 Subject: [PATCH] fw4: support automatic includes 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 --- root/usr/share/nftables.d/README | 22 ++ root/usr/share/ucode/fw4.uc | 20 +- tests/01_configuration/01_ruleset | 6 + tests/01_configuration/02_rule_order | 6 + tests/05_includes/04_disabled_include | 205 ++++++++++++++++++ tests/lib/mocklib/fs.uc | 34 +++ ...usr_share_nftables_d_ruleset-post_nft.json | 1 + ..._usr_share_nftables_d_ruleset-pre_nft.json | 1 + ...~_usr_share_nftables_d_table-post_nft.json | 1 + ...b~_usr_share_nftables_d_table-pre_nft.json | 1 + ...ndir~_usr_share_nftables_d_chain-post.json | 1 + ...endir~_usr_share_nftables_d_chain-pre.json | 1 + .../fs/open~_sys_class_net_br-lan_uevent.txt | 3 + .../fs/open~_sys_class_net_eth0_uevent.txt | 6 + .../fs/open~_sys_class_net_eth1_uevent.txt | 6 + 15 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 root/usr/share/nftables.d/README create mode 100644 tests/05_includes/04_disabled_include create mode 100644 tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-post_nft.json create mode 100644 tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-pre_nft.json create mode 100644 tests/mocks/fs/glob~_usr_share_nftables_d_table-post_nft.json create mode 100644 tests/mocks/fs/glob~_usr_share_nftables_d_table-pre_nft.json create mode 100644 tests/mocks/fs/opendir~_usr_share_nftables_d_chain-post.json create mode 100644 tests/mocks/fs/opendir~_usr_share_nftables_d_chain-pre.json create mode 100644 tests/mocks/fs/open~_sys_class_net_br-lan_uevent.txt create mode 100644 tests/mocks/fs/open~_sys_class_net_eth0_uevent.txt create mode 100644 tests/mocks/fs/open~_sys_class_net_eth1_uevent.txt diff --git a/root/usr/share/nftables.d/README b/root/usr/share/nftables.d/README new file mode 100644 index 0000000..e4aa9f8 --- /dev/null +++ b/root/usr/share/nftables.d/README @@ -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. diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc index 2dc44ac..dcb13ad 100644 --- a/root/usr/share/ucode/fw4.uc +++ b/root/usr/share/ucode/fw4.uc @@ -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 }); }, diff --git a/tests/01_configuration/01_ruleset b/tests/01_configuration/01_ruleset index 1bf8f72..06249f2 100644 --- a/tests/01_configuration/01_ruleset +++ b/tests/01_configuration/01_ruleset @@ -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 +[call] fs.glob pattern +[call] fs.glob pattern +[call] fs.glob pattern +[call] fs.lsdir path +[call] fs.lsdir path [call] ctx.call object method args [call] fs.opendir path [call] fs.opendir path diff --git a/tests/01_configuration/02_rule_order b/tests/01_configuration/02_rule_order index 3c1546e..245bb74 100644 --- a/tests/01_configuration/02_rule_order +++ b/tests/01_configuration/02_rule_order @@ -229,5 +229,11 @@ table inet fw4 { [call] ctx.call object method args [call] ctx.call object method args <{ "type": "firewall" }> [call] fs.open path mode +[call] fs.glob pattern +[call] fs.glob pattern +[call] fs.glob pattern +[call] fs.glob pattern +[call] fs.lsdir path +[call] fs.lsdir path [call] fs.popen cmdline mode -- End -- diff --git a/tests/05_includes/04_disabled_include b/tests/05_includes/04_disabled_include new file mode 100644 index 0000000..ac0a6c8 --- /dev/null +++ b/tests/05_includes/04_disabled_include @@ -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 -- diff --git a/tests/lib/mocklib/fs.uc b/tests/lib/mocklib/fs.uc index 61ad0b9..6482e6a 100644 --- a/tests/lib/mocklib/fs.uc +++ b/tests/lib/mocklib/fs.uc @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-post_nft.json @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/glob~_usr_share_nftables_d_ruleset-pre_nft.json @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/glob~_usr_share_nftables_d_table-post_nft.json @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/glob~_usr_share_nftables_d_table-pre_nft.json @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-post.json @@ -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 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/opendir~_usr_share_nftables_d_chain-pre.json @@ -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 index 0000000..d019219 --- /dev/null +++ b/tests/mocks/fs/open~_sys_class_net_br-lan_uevent.txt @@ -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 index 0000000..f6ada71 --- /dev/null +++ b/tests/mocks/fs/open~_sys_class_net_eth0_uevent.txt @@ -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 index 0000000..6db7cfd --- /dev/null +++ b/tests/mocks/fs/open~_sys_class_net_eth1_uevent.txt @@ -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 -- 2.30.2