From 11256ff0374fb594e31b0a4e3857f3810ba2933d Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 13 Jun 2022 15:49:14 +0200 Subject: [PATCH] fw4: add support for configurable includes Signed-off-by: Jo-Philipp Wich --- root/sbin/fw4 | 3 + root/usr/share/firewall4/main.uc | 22 ++++ root/usr/share/firewall4/templates/ruleset.uc | 37 ++++++ root/usr/share/ucode/fw4.uc | 123 +++++++++++++++++- tests/lib/mocklib/fs.uc | 16 ++- 5 files changed, 199 insertions(+), 2 deletions(-) diff --git a/root/sbin/fw4 b/root/sbin/fw4 index b089ac3..cf23e58 100755 --- a/root/sbin/fw4 +++ b/root/sbin/fw4 @@ -34,6 +34,9 @@ start() { ACTION=start \ utpl -S $MAIN | nft $VERBOSE -f $STDIN + + ACTION=includes \ + utpl -S $MAIN } 1000>$LOCK } diff --git a/root/usr/share/firewall4/main.uc b/root/usr/share/firewall4/main.uc index 9b28ea6..077191d 100644 --- a/root/usr/share/firewall4/main.uc +++ b/root/usr/share/firewall4/main.uc @@ -113,6 +113,25 @@ function lookup_zone(name, dev) { exit(1); } +function run_includes() { + let state = read_state(), + paths = []; + + for (let inc in state.includes) { + if (inc.type != 'script') + continue; + + let path = replace(inc.path, "'", "'\\''"); + let rc = system([ + 'sh', '-c', + `exec 1000>&-; config() { echo "You cannot use UCI in firewall includes!" >&2; exit 1; }; . '${path}'` + ], 30000); + + if (rc != 0) + warn(`Include '${inc.path}' failed with exit code ${rc}\n`); + } +} + switch (getenv("ACTION")) { case "start": @@ -132,4 +151,7 @@ case "device": case "zone": return lookup_zone(getenv("OBJECT"), getenv("DEVICE")); + +case "includes": + return run_includes(); } diff --git a/root/usr/share/firewall4/templates/ruleset.uc b/root/usr/share/firewall4/templates/ruleset.uc index 712697f..a09cb1f 100644 --- a/root/usr/share/firewall4/templates/ruleset.uc +++ b/root/usr/share/firewall4/templates/ruleset.uc @@ -9,6 +9,7 @@ flush table inet fw4 {% if (fw4.check_flowtable()): %} delete flowtable inet fw4 ft {% endif %} +{% fw4.includes('ruleset-prepend') %} table inet fw4 { {% if (length(flowtable_devices) > 0): %} @@ -80,6 +81,7 @@ table inet fw4 { # include "/etc/nftables.d/*.nft" +{% fw4.includes('table-prepend') %} # @@ -91,6 +93,7 @@ table inet fw4 { iifname "lo" accept comment "!fw4: Accept traffic from loopback" +{% fw4.includes('chain-prepend', 'input') %} ct state established,related accept comment "!fw4: Allow inbound established and related flows" {% if (fw4.default_option("drop_invalid")): %} ct state invalid drop comment "!fw4: Drop flows with invalid conntrack state" @@ -107,6 +110,7 @@ table inet fw4 { {% if (fw4.input_policy() == "reject"): %} jump handle_reject {% endif %} +{% fw4.includes('chain-append', 'input') %} } chain forward { @@ -115,6 +119,7 @@ table inet fw4 { {% if (length(flowtable_devices) > 0): %} meta l4proto { tcp, udp } flow offload @ft; {% endif %} +{% fw4.includes('chain-prepend', 'forward') %} ct state established,related accept comment "!fw4: Allow forwarded established and related flows" {% if (fw4.default_option("drop_invalid")): %} ct state invalid drop comment "!fw4: Drop flows with invalid conntrack state" @@ -125,6 +130,7 @@ table inet fw4 { {% for (let zone in fw4.zones()): for (let rule in zone.match_rules): %} {%+ include("zone-jump.uc", { fw4, zone, rule, direction: "forward" }) %} {% endfor; endfor %} +{% fw4.includes('chain-append', 'forward') %} {% if (fw4.forward_policy() == "reject"): %} jump handle_reject {% endif %} @@ -135,6 +141,7 @@ table inet fw4 { oifname "lo" accept comment "!fw4: Accept traffic towards loopback" +{% fw4.includes('chain-prepend', 'output') %} ct state established,related accept comment "!fw4: Allow outbound established and related flows" {% if (fw4.default_option("drop_invalid")): %} ct state invalid drop comment "!fw4: Drop flows with invalid conntrack state" @@ -154,6 +161,7 @@ table inet fw4 { {%+ include("zone-jump.uc", { fw4, zone, rule, direction: "output" }) %} {% endfor %} {% endfor %} +{% fw4.includes('chain-append', 'output') %} {% if (fw4.output_policy() == "reject"): %} jump handle_reject {% endif %} @@ -200,29 +208,35 @@ table inet fw4 { {% endif %} {% for (let zone in fw4.zones()): %} chain input_{{ zone.name }} { +{% fw4.includes('chain-prepend', `input_${zone.name}`) %} {% for (let rule in fw4.rules(`input_${zone.name}`)): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} {% if (zone.dflags.dnat): %} ct status dnat accept comment "!fw4: Accept port redirections" {% endif %} +{% fw4.includes('chain-append', `input_${zone.name}`) %} jump {{ zone.input }}_from_{{ zone.name }} } chain output_{{ zone.name }} { +{% fw4.includes('chain-prepend', `output_${zone.name}`) %} {% for (let rule in fw4.rules(`output_${zone.name}`)): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} +{% fw4.includes('chain-append', `output_${zone.name}`) %} jump {{ zone.output }}_to_{{ zone.name }} } chain forward_{{ zone.name }} { +{% fw4.includes('chain-prepend', `forward_${zone.name}`) %} {% for (let rule in fw4.rules(`forward_${zone.name}`)): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} {% if (zone.dflags.dnat): %} ct status dnat accept comment "!fw4: Accept port forwards" {% endif %} +{% fw4.includes('chain-append', `forward_${zone.name}`) %} jump {{ zone.forward }}_to_{{ zone.name }} } @@ -260,6 +274,7 @@ table inet fw4 { chain dstnat { type nat hook prerouting priority dstnat; policy accept; +{% fw4.includes('chain-prepend', 'dstnat') %} {% for (let zone in fw4.zones()): %} {% if (zone.dflags.dnat): %} {% for (let rule in zone.match_rules): %} @@ -267,10 +282,12 @@ table inet fw4 { {% endfor %} {% endif %} {% endfor %} +{% fw4.includes('chain-append', 'dstnat') %} } chain srcnat { type nat hook postrouting priority srcnat; policy accept; +{% fw4.includes('chain-prepend', 'srcnat') %} {% for (let redirect in fw4.redirects("srcnat")): %} {%+ include("redirect.uc", { fw4, redirect }) %} {% endfor %} @@ -281,19 +298,23 @@ table inet fw4 { {% endfor %} {% endif %} {% endfor %} +{% fw4.includes('chain-append', 'srcnat') %} } {% for (let zone in fw4.zones()): %} {% if (zone.dflags.dnat): %} chain dstnat_{{ zone.name }} { +{% fw4.includes('chain-prepend', `dstnat_${zone.name}`) %} {% for (let redirect in fw4.redirects(`dstnat_${zone.name}`)): %} {%+ include("redirect.uc", { fw4, redirect }) %} {% endfor %} +{% fw4.includes('chain-append', `dstnat_${zone.name}`) %} } {% endif %} {% if (zone.dflags.snat): %} chain srcnat_{{ zone.name }} { +{% fw4.includes('chain-prepend', `srcnat_${zone.name}`) %} {% for (let redirect in fw4.redirects(`srcnat_${zone.name}`)): %} {%+ include("redirect.uc", { fw4, redirect }) %} {% endfor %} @@ -311,6 +332,7 @@ table inet fw4 { {% endfor %} {% endfor %} {% endif %} +{% fw4.includes('chain-append', `srcnat_${zone.name}`) %} } {% endif %} @@ -333,10 +355,12 @@ table inet fw4 { {% endfor %} {% endif %} {% endfor %} +{% fw4.includes('chain-append', 'raw_prerouting') %} } chain raw_output { type filter hook output priority raw; policy accept; +{% fw4.includes('chain-prepend', 'raw_output') %} {% for (let zone in fw4.zones()): %} {% if (zone.dflags["notrack"]): %} {% for (let rule in zone.match_rules): %} @@ -348,6 +372,7 @@ table inet fw4 { {% endfor %} {% endif %} {% endfor %} +{% fw4.includes('chain-append', 'raw_output') %} } {% for (let zone in fw4.zones()): %} @@ -367,34 +392,43 @@ table inet fw4 { chain mangle_prerouting { type filter hook prerouting priority mangle; policy accept; +{% fw4.includes('chain-prepend', 'mangle_prerouting') %} {% for (let rule in fw4.rules("mangle_prerouting")): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} +{% fw4.includes('chain-append', 'mangle_prerouting') %} } chain mangle_postrouting { type filter hook postrouting priority mangle; policy accept; +{% fw4.includes('chain-prepend', 'mangle_postrouting') %} {% for (let rule in fw4.rules("mangle_postrouting")): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} +{% fw4.includes('chain-append', 'mangle_postrouting') %} } chain mangle_input { type filter hook input priority mangle; policy accept; +{% fw4.includes('chain-prepend', 'mangle_input') %} {% for (let rule in fw4.rules("mangle_input")): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} +{% fw4.includes('chain-append', 'mangle_input') %} } chain mangle_output { type route hook output priority mangle; policy accept; +{% fw4.includes('chain-prepend', 'mangle_output') %} {% for (let rule in fw4.rules("mangle_output")): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} +{% fw4.includes('chain-append', 'mangle_output') %} } chain mangle_forward { type filter hook forward priority mangle; policy accept; +{% fw4.includes('chain-prepend', 'mangle_forward') %} {% for (let rule in fw4.rules("mangle_forward")): %} {%+ include("rule.uc", { fw4, rule }) %} {% endfor %} @@ -406,5 +440,8 @@ table inet fw4 { {% endfor %} {% endif %} {% endfor %} +{% fw4.includes('chain-append', 'mangle_forward') %} } +{% fw4.includes('table-append') %} } +{% fw4.includes('ruleset-append') %} diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc index 95e2540..85456c9 100644 --- a/root/usr/share/ucode/fw4.uc +++ b/root/usr/share/ucode/fw4.uc @@ -726,6 +726,13 @@ return { this.cursor.foreach("firewall", "nat", n => self.parse_nat(n)); + // + // Build list of includes + // + + this.cursor.foreach("firewall", "include", i => self.parse_include(i)); + + if (use_statefile) { let fd = fs.open(STATEFILE, "w"); @@ -734,7 +741,8 @@ return { zones: this.state.zones, ipsets: this.state.ipsets, networks: this.state.networks, - ubus_rules: this.state.ubus_rules + ubus_rules: this.state.ubus_rules, + includes: this.state.includes }); fd.close(); @@ -1475,6 +1483,29 @@ return { return length(rv) ? rv : null; }, + parse_includetype: function(val) { + return this.parse_enum(val, [ + "script", + "nftables" + ]); + }, + + parse_includeposition: function(val) { + return replace(this.parse_enum(val, [ + "ruleset-prepend", + "ruleset-postpend", + "ruleset-append", + + "table-prepend", + "table-postpend", + "table-append", + + "chain-prepend", + "chain-postpend", + "chain-append" + ]), "postpend", "append"); + }, + parse_string: function(val) { return "" + val; }, @@ -1703,6 +1734,36 @@ return { return this.state.ipsets; }, + includes: function(position, chain) { + let stmts = []; + let pad = ''; + let pre = ''; + + switch (position) { + case 'table-prepend': + case 'table-append': + pad = '\t'; + pre = '\n'; + break; + + case 'chain-prepend': + case 'chain-append': + pad = '\t\t'; + break; + + default: + pre = '\n'; + } + + push(stmts, pre); + + for (let inc in this.state.includes) + if (inc.type == 'nftables' && inc.position == position && (!chain || inc.chain == chain)) + push(stmts, `${pad}include "${inc.path}"\n`); + + print(length(stmts) > 1 ? join('', stmts) : ''); + }, + parse_setfile: function(set, cb) { let fd = fs.open(set.loadfile, "r"); @@ -3012,6 +3073,66 @@ return { } }, + parse_include: function(data) { + let inc = this.parse_options(data, { + enabled: [ "bool", "1" ], + + path: [ "string", null, REQUIRED ], + type: [ "includetype", "script" ], + + fw4_compatible: [ "bool", data.path != "/etc/firewall.user" ], + + family: [ "family", null, UNSUPPORTED ], + reload: [ "bool", null, UNSUPPORTED ], + + position: [ "includeposition" ], + chain: [ "string" ] + }); + + if (inc.type == "script" && !inc.fw4_compatible) { + this.warn_section(data, "is not marked as compatible with fw4, ignoring section"); + this.warn_section(data, "requires 'option fw4_compatible 1' to be considered compatible"); + return; + } + + for (let opt in [ "table", "chain", "position" ]) { + if (inc.type != "nftables" && inc[opt]) { + this.warn_section(data, `must not specify '${opt}' for non-nftables includes, ignoring section`); + return; + } + } + + switch (inc.position ??= 'table-append') { + case 'ruleset-prepend': + case 'ruleset-append': + case 'table-prepend': + case 'table-append': + if (inc.chain) + this.warn_section(data, `specifies 'chain' which has no effect for position ${inc.position}`); + + delete inc.chain; + break; + + case 'chain-prepend': + case 'chain-append': + if (!inc.chain) { + this.warn_section(data, `must specify 'chain' for position ${inc.position}, ignoring section`); + return; + } + + break; + } + + let path = fs.readlink(inc.path) ?? inc.path; + + if (!fs.access(path)) { + this.warn_section(data, `specifies unreachable path '${path}', ignoring section`); + return; + } + + push(this.state.includes ||= [], { ...inc, path }); + }, + parse_ipset: function(data) { let ipset = this.parse_options(data, { enabled: [ "bool", "1" ], diff --git a/tests/lib/mocklib/fs.uc b/tests/lib/mocklib/fs.uc index 10f3074..61ad0b9 100644 --- a/tests/lib/mocklib/fs.uc +++ b/tests/lib/mocklib/fs.uc @@ -5,7 +5,7 @@ return { readlink: function(path) { mocklib.trace_call("fs", "readlink", { path }); - return path + "-link"; + return path; }, stat: function(path) { @@ -151,6 +151,20 @@ return { return limit ? substr(mock, 0, limit) : mock; }, + access: (fpath) => { + let path = sprintf("fs/open~%s.txt", replace(fpath, /[^A-Za-z0-9_-]+/g, '_')), + mock = mocklib.read_data_file(path); + + if (!mock) { + mocklib.I("No stdout fixture defined for fs.access() path %s.", fpath); + mocklib.I("Provide a mock output through the following text file:\n%s\n", path); + + return false; + } + + return true; + }, + opendir: (path) => { let file = sprintf("fs/opendir~%s.json", replace(path, /[^A-Za-z0-9_-]+/g, '_')), mock = mocklib.read_json_file(file), -- 2.30.2