fw4: add support for configurable includes
authorJo-Philipp Wich <jo@mein.io>
Mon, 13 Jun 2022 13:49:14 +0000 (15:49 +0200)
committerJo-Philipp Wich <jo@mein.io>
Wed, 15 Jun 2022 11:32:17 +0000 (13:32 +0200)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
root/sbin/fw4
root/usr/share/firewall4/main.uc
root/usr/share/firewall4/templates/ruleset.uc
root/usr/share/ucode/fw4.uc
tests/lib/mocklib/fs.uc

index b089ac3b3544fa2e48dc4856bdf7a19cd888cc2b..cf23e581d6a91d6fd0298655fb080cc755f6bcfc 100755 (executable)
@@ -34,6 +34,9 @@ start() {
 
                ACTION=start \
                        utpl -S $MAIN | nft $VERBOSE -f $STDIN
+
+               ACTION=includes \
+                       utpl -S $MAIN
        } 1000>$LOCK
 }
 
index 9b28ea6f1391d74709ad0393f8d3efd455b02965..077191df3abc803f5ab3943eadf216ae6261cf7c 100644 (file)
@@ -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();
 }
index 712697f2e0029be6c2766141f6045f01b9bc198d..a09cb1f3238a04efb05ecd30abd8803a085ba8ae 100644 (file)
@@ -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') %}
index 95e2540a74da869784f562dcada87c811eba51c2..85456c9fde0005403433dedfa6aeb7f1242ba790 100644 (file)
@@ -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" ],
index 10f30746819219d7cf31b8b70d81174a209abf30..61ad0b9dee88c5fc516dd95017cf85c489a7c80f 100644 (file)
@@ -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),