summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJo-Philipp Wich2022-06-13 13:49:14 +0000
committerJo-Philipp Wich2022-06-15 11:32:17 +0000
commit11256ff0374fb594e31b0a4e3857f3810ba2933d (patch)
tree7fd17b46852511190f1147474db1fae7cbf5b705
parent53886e559b9765c61db646fffb2953ab12aac1e7 (diff)
downloadfirewall4-11256ff0374fb594e31b0a4e3857f3810ba2933d.tar.gz
fw4: add support for configurable includes
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
-rwxr-xr-xroot/sbin/fw43
-rw-r--r--root/usr/share/firewall4/main.uc22
-rw-r--r--root/usr/share/firewall4/templates/ruleset.uc37
-rw-r--r--root/usr/share/ucode/fw4.uc123
-rw-r--r--tests/lib/mocklib/fs.uc16
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),