From 4d5af8b0196912ab21f1b288f73cafa2eb6a1345 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Fri, 4 Feb 2022 23:44:25 +0100 Subject: [PATCH] fw4: consolidate helper code - Move various local helper functions out of main.uc into the fw4 class - Rework settype reading to use nft JSON output as terse mode now works - Simplify testing flowtable enable conditions - Adjust testcases to changed flowtable logic Signed-off-by: Jo-Philipp Wich --- root/usr/share/firewall4/main.uc | 119 +------ root/usr/share/firewall4/templates/ruleset.uc | 10 +- root/usr/share/ucode/fw4.uc | 86 +++++ tests/01_configuration/01_ruleset | 8 +- .../fs/opendir~_sys_class_net_br-lan.json | 3 + .../mocks/fs/opendir~_sys_class_net_eth0.json | 1 + .../mocks/fs/opendir~_sys_class_net_eth1.json | 1 + ...ft_--terse_--json_list_flowtables_inet.txt | 1 + tests/mocks/ubus/network.device~status.json | 302 ++++++++++++++++++ 9 files changed, 409 insertions(+), 122 deletions(-) create mode 100644 tests/mocks/fs/opendir~_sys_class_net_br-lan.json create mode 100644 tests/mocks/fs/opendir~_sys_class_net_eth0.json create mode 100644 tests/mocks/fs/opendir~_sys_class_net_eth1.json create mode 100644 tests/mocks/fs/popen~_usr_sbin_nft_--terse_--json_list_flowtables_inet.txt create mode 100644 tests/mocks/ubus/network.device~status.json diff --git a/root/usr/share/firewall4/main.uc b/root/usr/share/firewall4/main.uc index 839346b..692d9aa 100644 --- a/root/usr/share/firewall4/main.uc +++ b/root/usr/share/firewall4/main.uc @@ -1,50 +1,6 @@ {% let fw4 = require("fw4"); -let ubus = require("ubus"); -let fs = require("fs"); - -/* Find existing sets. - * - * Unfortunately, terse mode (-t) is incompatible with JSON output so - * we either need to parse a potentially huge JSON just to get the set - * header data or scrape the ordinary nft output to obtain the same - * information. Opt for the latter to avoid parsing potentially huge - * JSON documents. - */ -function find_existing_sets() { - let fd = fs.popen("nft -t list sets inet", "r"); - - if (!fd) { - warn(sprintf("Unable to execute 'nft' for listing existing sets: %s\n", - fs.error())); - return {}; - } - - let line, table, set; - let sets = {}; - - while ((line = fd.read("line")) !== "") { - let m; - - if ((m = match(line, /^table inet (.+) \{\n$/)) != null) { - table = m[1]; - } - else if ((m = match(line, /^\tset (.+) \{\n$/)) != null) { - set = m[1]; - } - else if ((m = match(line, /^\t\ttype (.+)\n$/)) != null) { - if (table == "fw4" && set) - sets[set] = split(m[1], " . "); - - set = null; - } - } - - fd.close(); - - return sets; -} function read_state() { let state = fw4.read_state(); @@ -59,7 +15,7 @@ function read_state() { function reload_sets() { let state = read_state(), - sets = find_existing_sets(); + sets = fw4.check_set_types(); for (let set in state.ipsets) { if (!set.loadfile || !length(set.entries)) @@ -96,81 +52,10 @@ function reload_sets() { } } -function resolve_lower_devices(devstatus, devname) { - let dir = fs.opendir("/sys/class/net/" + devname); - let devs = []; - - if (dir) { - if (!devstatus || devstatus[devname]?.["hw-tc-offload"]) { - push(devs, devname); - } - else { - let e; - - while ((e = dir.read()) != null) - if (index(e, "lower_") === 0) - push(devs, ...resolve_lower_devices(devstatus, substr(e, 6))); - } - - dir.close(); - } - - return devs; -} - -function resolve_offload_devices() { - if (!fw4.default_option("flow_offloading")) - return []; - - let devstatus = null; - let devices = []; - - if (fw4.default_option("flow_offloading_hw")) { - let bus = require("ubus").connect(); - - if (bus) { - devstatus = bus.call("network.device", "status") || {}; - bus.disconnect(); - } - } - - for (let zone in fw4.zones()) - for (let device in zone.match_devices) - push(devices, ...resolve_lower_devices(devstatus, device)); - - return uniq(devices); -} - -function check_flowtable() { - let nft = fs.popen("nft --terse --json list flowtables inet"); - let info; - - if (nft) { - try { - info = json(nft.read("all")); - } - catch (e) { - info = {}; - } - - nft.close(); - } - - for (let item in info?.nftables) - if (item?.flowtable?.table == "fw4" && item?.flowtable?.name == "ft") - return true; - - return false; -} - function render_ruleset(use_statefile) { fw4.load(use_statefile); - include("templates/ruleset.uc", { - fw4, type, exists, length, include, - devices: resolve_offload_devices(), - flowtable: check_flowtable() - }); + include("templates/ruleset.uc", { fw4, type, exists, length, include }); } function lookup_network(net) { diff --git a/root/usr/share/firewall4/templates/ruleset.uc b/root/usr/share/firewall4/templates/ruleset.uc index 8020bed..b402315 100644 --- a/root/usr/share/firewall4/templates/ruleset.uc +++ b/root/usr/share/firewall4/templates/ruleset.uc @@ -1,18 +1,20 @@ +{% let flowtable_devices = fw4.resolve_offload_devices(); -%} + table inet fw4 flush table inet fw4 -{% if (flowtable): %} +{% if (fw4.check_flowtable()): %} delete flowtable inet fw4 ft {% endif %} table inet fw4 { -{% if (fw4.default_option("flow_offloading") && length(devices) > 0): %} +{% if (length(flowtable_devices) > 0): %} # # Flowtable # flowtable ft { hook ingress priority 0; - devices = {{ fw4.set(devices, true) }}; + devices = {{ fw4.set(flowtable_devices, true) }}; {% if (fw4.default_option("flow_offloading_hw")): %} flags offload; {% endif %} @@ -90,7 +92,7 @@ table inet fw4 { chain forward { type filter hook forward priority filter; policy {{ fw4.forward_policy(true) }}; -{% if (fw4.default_option("flow_offloading") && length(devices) > 0): %} +{% if (length(flowtable_devices) > 0): %} meta l4proto { tcp, udp } flow offload @ft; {% endif %} ct state established,related accept comment "!fw4: Allow forwarded established and related flows" diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc index fb87208..932637e 100644 --- a/root/usr/share/ucode/fw4.uc +++ b/root/usr/share/ucode/fw4.uc @@ -359,6 +359,51 @@ function map_setmatch(set, match, proto) { return fields; } +function resolve_lower_devices(devstatus, devname) { + let dir = fs.opendir("/sys/class/net/" + devname); + let devs = []; + + if (dir) { + if (!devstatus || devstatus[devname]?.["hw-tc-offload"]) { + push(devs, devname); + } + else { + let e; + + while ((e = dir.read()) != null) + if (index(e, "lower_") === 0) + push(devs, ...resolve_lower_devices(devstatus, substr(e, 6))); + } + + dir.close(); + } + + return devs; +} + +function nft_json_command(...args) { + let cmd = [ "/usr/sbin/nft", "--terse", "--json", ...args ]; + let nft = fs.popen(join(" ", cmd), "r"); + let info; + + if (nft) { + try { + info = filter(json(nft.read("all"))?.nftables, + item => (type(item) == "object" && !item.metainfo)); + } + catch (e) { + warn(sprintf("Unable to parse nftables JSON output: %s\n", e)); + } + + nft.close(); + } + else { + warn(sprintf("Unable to popen() %s: %s\n", cmd, fs.error())); + } + + return info || []; +} + return { read_kernel_version: function() { @@ -375,6 +420,47 @@ return { return v; }, + resolve_offload_devices: function() { + if (!this.default_option("flow_offloading")) + return []; + + let devstatus = null; + let devices = []; + + if (this.default_option("flow_offloading_hw")) { + let bus = ubus.connect(); + + if (bus) { + devstatus = bus.call("network.device", "status") || {}; + bus.disconnect(); + } + } + + for (let zone in fw4.zones()) + for (let device in zone.match_devices) + push(devices, ...resolve_lower_devices(devstatus, device)); + + return uniq(devices); + }, + + check_set_types: function() { + let sets = {}; + + for (let item in nft_json_command("list", "sets", "inet")) + if (item.set?.table == "fw4") + sets[item.set.name] = (type(item.set.type) == "array") ? item.set.type : [ item.set.type ]; + + return sets; + }, + + check_flowtable: function() { + for (let item in nft_json_command("list", "flowtables", "inet")) + if (item.flowtable?.table == "fw4" && item.flowtable?.name == "ft") + return true; + + return false; + }, + read_state: function() { let fd = fs.open(STATEFILE, "r"); let state = null; diff --git a/tests/01_configuration/01_ruleset b/tests/01_configuration/01_ruleset index 7f4fcfd..879bdbf 100644 --- a/tests/01_configuration/01_ruleset +++ b/tests/01_configuration/01_ruleset @@ -26,7 +26,7 @@ table inet fw4 { flowtable ft { hook ingress priority 0; - devices = { "br-lan", "eth1" }; + devices = { "eth0", "eth1" }; flags offload; } @@ -296,6 +296,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] ctx.call object method args +[call] fs.opendir path +[call] fs.opendir path +[call] fs.opendir path +[call] fs.opendir path +[call] fs.popen cmdline mode [call] fs.open path mode [call] fs.open path mode -- End -- diff --git a/tests/mocks/fs/opendir~_sys_class_net_br-lan.json b/tests/mocks/fs/opendir~_sys_class_net_br-lan.json new file mode 100644 index 0000000..5b163ad --- /dev/null +++ b/tests/mocks/fs/opendir~_sys_class_net_br-lan.json @@ -0,0 +1,3 @@ +[ + "lower_eth0" +] diff --git a/tests/mocks/fs/opendir~_sys_class_net_eth0.json b/tests/mocks/fs/opendir~_sys_class_net_eth0.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/opendir~_sys_class_net_eth0.json @@ -0,0 +1 @@ +[] diff --git a/tests/mocks/fs/opendir~_sys_class_net_eth1.json b/tests/mocks/fs/opendir~_sys_class_net_eth1.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/tests/mocks/fs/opendir~_sys_class_net_eth1.json @@ -0,0 +1 @@ +[] diff --git a/tests/mocks/fs/popen~_usr_sbin_nft_--terse_--json_list_flowtables_inet.txt b/tests/mocks/fs/popen~_usr_sbin_nft_--terse_--json_list_flowtables_inet.txt new file mode 100644 index 0000000..72d2dfe --- /dev/null +++ b/tests/mocks/fs/popen~_usr_sbin_nft_--terse_--json_list_flowtables_inet.txt @@ -0,0 +1 @@ +{"nftables": [{"metainfo": {"version": "1.0.1", "release_name": "Fearless Fosdick #3", "json_schema_version": 1}}]} diff --git a/tests/mocks/ubus/network.device~status.json b/tests/mocks/ubus/network.device~status.json new file mode 100644 index 0000000..5d402f0 --- /dev/null +++ b/tests/mocks/ubus/network.device~status.json @@ -0,0 +1,302 @@ +{ + "eth0": { + "external": false, + "present": true, + "type": "Network device", + "up": true, + "carrier": true, + "auth_status": false, + "link-advertising": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-F" + ], + "link-partner-advertising": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-H", + "1000baseT-F" + ], + "link-supported": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-F" + ], + "speed": "1000F", + "autoneg": true, + "hw-tc-offload": true, + "devtype": "dsa", + "mtu": 1500, + "mtu6": 1500, + "macaddr": "74:ac:b9:a1:84:46", + "txqueuelen": 1000, + "ipv6": false, + "ip6segmentrouting": false, + "promisc": false, + "rpfilter": 0, + "acceptlocal": false, + "igmpversion": 0, + "mldversion": 0, + "neigh4reachabletime": 30000, + "neigh6reachabletime": 30000, + "neigh4gcstaletime": 60, + "neigh6gcstaletime": 60, + "neigh4locktime": 100, + "dadtransmits": 1, + "multicast": true, + "sendredirects": true, + "drop_v4_unicast_in_l2_multicast": false, + "drop_v6_unicast_in_l2_multicast": false, + "drop_gratuitous_arp": false, + "drop_unsolicited_na": false, + "arp_accept": false, + "statistics": { + "collisions": 0, + "rx_frame_errors": 0, + "tx_compressed": 0, + "multicast": 0, + "rx_length_errors": 0, + "tx_dropped": 0, + "rx_bytes": 53206036390, + "rx_missed_errors": 0, + "tx_errors": 0, + "rx_compressed": 0, + "rx_over_errors": 0, + "tx_fifo_errors": 0, + "rx_crc_errors": 0, + "rx_packets": 115554141, + "tx_heartbeat_errors": 0, + "rx_dropped": 0, + "tx_aborted_errors": 0, + "tx_packets": 44534107, + "rx_errors": 0, + "tx_bytes": 41926154781, + "tx_window_errors": 0, + "rx_fifo_errors": 0, + "tx_carrier_errors": 0 + } + }, + "eth1": { + "external": false, + "present": true, + "type": "Network device", + "up": true, + "carrier": true, + "auth_status": false, + "link-advertising": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-F" + ], + "link-partner-advertising": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-F" + ], + "link-supported": [ + "10baseT-H", + "10baseT-F", + "100baseT-H", + "100baseT-F", + "1000baseT-F" + ], + "speed": "1000F", + "autoneg": true, + "hw-tc-offload": true, + "devtype": "dsa", + "mtu": 1500, + "mtu6": 1500, + "macaddr": "74:ac:b9:a1:84:47", + "txqueuelen": 1000, + "ipv6": false, + "ip6segmentrouting": false, + "promisc": false, + "rpfilter": 0, + "acceptlocal": false, + "igmpversion": 0, + "mldversion": 0, + "neigh4reachabletime": 30000, + "neigh6reachabletime": 30000, + "neigh4gcstaletime": 60, + "neigh6gcstaletime": 60, + "neigh4locktime": 100, + "dadtransmits": 1, + "multicast": true, + "sendredirects": true, + "drop_v4_unicast_in_l2_multicast": false, + "drop_v6_unicast_in_l2_multicast": false, + "drop_gratuitous_arp": false, + "drop_unsolicited_na": false, + "arp_accept": false, + "statistics": { + "collisions": 0, + "rx_frame_errors": 0, + "tx_compressed": 0, + "multicast": 0, + "rx_length_errors": 0, + "tx_dropped": 0, + "rx_bytes": 470925492, + "rx_missed_errors": 0, + "tx_errors": 0, + "rx_compressed": 0, + "rx_over_errors": 0, + "tx_fifo_errors": 0, + "rx_crc_errors": 0, + "rx_packets": 4591620, + "tx_heartbeat_errors": 0, + "rx_dropped": 0, + "tx_aborted_errors": 0, + "tx_packets": 4210181, + "rx_errors": 0, + "tx_bytes": 7724980929, + "tx_window_errors": 0, + "rx_fifo_errors": 0, + "tx_carrier_errors": 0 + } + }, + "lo": { + "external": false, + "present": true, + "type": "Network device", + "up": true, + "carrier": true, + "auth_status": false, + "hw-tc-offload": false, + "devtype": "loopback", + "mtu": 65536, + "mtu6": 65536, + "macaddr": "00:00:00:00:00:00", + "txqueuelen": 1000, + "ipv6": true, + "ip6segmentrouting": false, + "promisc": false, + "rpfilter": 0, + "acceptlocal": false, + "igmpversion": 0, + "mldversion": 0, + "neigh4reachabletime": 30000, + "neigh6reachabletime": 30000, + "neigh4gcstaletime": 60, + "neigh6gcstaletime": 60, + "neigh4locktime": 100, + "dadtransmits": 1, + "multicast": false, + "sendredirects": true, + "drop_v4_unicast_in_l2_multicast": false, + "drop_v6_unicast_in_l2_multicast": false, + "drop_gratuitous_arp": false, + "drop_unsolicited_na": false, + "arp_accept": false, + "statistics": { + "collisions": 0, + "rx_frame_errors": 0, + "tx_compressed": 0, + "multicast": 0, + "rx_length_errors": 0, + "tx_dropped": 0, + "rx_bytes": 2993476, + "rx_missed_errors": 0, + "tx_errors": 0, + "rx_compressed": 0, + "rx_over_errors": 0, + "tx_fifo_errors": 0, + "rx_crc_errors": 0, + "rx_packets": 34056, + "tx_heartbeat_errors": 0, + "rx_dropped": 0, + "tx_aborted_errors": 0, + "tx_packets": 34056, + "rx_errors": 0, + "tx_bytes": 2993476, + "tx_window_errors": 0, + "rx_fifo_errors": 0, + "tx_carrier_errors": 0 + } + }, + "br-lan": { + "external": false, + "present": true, + "type": "bridge", + "up": true, + "carrier": true, + "auth_status": false, + "link-advertising": [ + + ], + "link-partner-advertising": [ + + ], + "link-supported": [ + + ], + "speed": "1000F", + "autoneg": false, + "hw-tc-offload": false, + "devtype": "bridge", + "bridge-members": [ + "eth0" + ], + "bridge-vlans": [ + + ], + "mtu": 1500, + "mtu6": 1500, + "macaddr": "74:ac:b9:a1:84:46", + "txqueuelen": 1000, + "ipv6": true, + "ip6segmentrouting": false, + "promisc": false, + "rpfilter": 0, + "acceptlocal": false, + "igmpversion": 0, + "mldversion": 0, + "neigh4reachabletime": 30000, + "neigh6reachabletime": 30000, + "neigh4gcstaletime": 60, + "neigh6gcstaletime": 60, + "neigh4locktime": 100, + "dadtransmits": 1, + "multicast": true, + "sendredirects": true, + "drop_v4_unicast_in_l2_multicast": false, + "drop_v6_unicast_in_l2_multicast": false, + "drop_gratuitous_arp": false, + "drop_unsolicited_na": false, + "arp_accept": false, + "statistics": { + "collisions": 0, + "rx_frame_errors": 0, + "tx_compressed": 0, + "multicast": 27625, + "rx_length_errors": 0, + "tx_dropped": 0, + "rx_bytes": 9505679939, + "rx_missed_errors": 0, + "tx_errors": 0, + "rx_compressed": 0, + "rx_over_errors": 0, + "tx_fifo_errors": 0, + "rx_crc_errors": 0, + "rx_packets": 13282955, + "tx_heartbeat_errors": 0, + "rx_dropped": 0, + "tx_aborted_errors": 0, + "tx_packets": 12647462, + "rx_errors": 0, + "tx_bytes": 12983448170, + "tx_window_errors": 0, + "rx_fifo_errors": 0, + "tx_carrier_errors": 0 + } + } +} -- 2.30.2