fw4: honour enabled option of include sections
[project/firewall4.git] / root / usr / share / ucode / fw4.uc
index 4ca1d79fa00a955e849e3261e409969ba65c47ce..2dc44ac9684b44e90754a0fda1a66f90c5899079 100644 (file)
@@ -131,14 +131,19 @@ const dscp_classes = {
 };
 
 function to_mask(bits, v6) {
-       let m = [];
+       let m = [], n = false;
 
-       if (bits < 0 || bits > (v6 ? 128 : 32))
+       if (bits < 0) {
+               n = true;
+               bits = -bits;
+       }
+
+       if (bits > (v6 ? 128 : 32))
                return null;
 
        for (let i = 0; i < (v6 ? 16 : 4); i++) {
                let b = (bits < 8) ? bits : 8;
-               m[i] = (0xff << (8 - b)) & 0xff;
+               m[i] = (n ? ~(0xff << (8 - b)) : (0xff << (8 - b))) & 0xff;
                bits -= b;
        }
 
@@ -333,11 +338,11 @@ function map_setmatch(set, match, proto) {
 
                switch (t) {
                case 'ipv4_addr':
-                       fields[i] = `ip ${dir}saddr`;
+                       fields[i] = `ip ${dir}addr`;
                        break;
 
                case 'ipv6_addr':
-                       fields[i] = `ip6 ${dir}saddr`;
+                       fields[i] = `ip6 ${dir}addr`;
                        break;
 
                case 'ether_addr':
@@ -356,20 +361,27 @@ function map_setmatch(set, match, proto) {
        return fields;
 }
 
-function resolve_lower_devices(devstatus, devname) {
+function resolve_lower_devices(devstatus, devname, require_hwoffload) {
        let dir = fs.opendir(`/sys/class/net/${devname}`);
        let devs = [];
 
        if (dir) {
-               if (!devstatus || devstatus[devname]?.["hw-tc-offload"]) {
-                       push(devs, devname);
-               }
-               else {
+               switch (devstatus[devname]?.devtype) {
+               case 'vlan':
+               case 'bridge':
                        let e;
 
                        while ((e = dir.read()) != null)
                                if (index(e, "lower_") === 0)
-                                       push(devs, ...resolve_lower_devices(devstatus, substr(e, 6)));
+                                       push(devs, ...resolve_lower_devices(devstatus, substr(e, 6), require_hwoffload));
+
+                       break;
+
+               default:
+                       if (!require_hwoffload || devstatus[devname]?.["hw-tc-offload"])
+                               push(devs, devname);
+
+                       break;
                }
 
                dir.close();
@@ -438,22 +450,21 @@ return {
 
                let devstatus = null;
                let devices = [];
+               let bus = ubus.connect();
 
-               if (this.default_option("flow_offloading_hw")) {
-                       let bus = ubus.connect();
-
-                       if (bus) {
-                               devstatus = bus.call("network.device", "status") || {};
-                               bus.disconnect();
-                       }
+               if (bus) {
+                       devstatus = bus.call("network.device", "status") || {};
+                       bus.disconnect();
+               }
 
+               if (this.default_option("flow_offloading_hw")) {
                        for (let zone in this.zones())
                                for (let device in zone.related_physdevs)
-                                       push(devices, ...resolve_lower_devices(devstatus, device));
+                                       push(devices, ...resolve_lower_devices(devstatus, device, true));
 
-                       devices = uniq(devices);
+                       devices = sort(uniq(devices));
 
-                       if (nft_try_hw_offload(devices))
+                       if (length(devices) && nft_try_hw_offload(devices))
                                return devices;
 
                        this.warn('Hardware flow offloading unavailable, falling back to software offloading');
@@ -463,10 +474,10 @@ return {
                }
 
                for (let zone in this.zones())
-                       for (let device in zone.match_devices)
-                               push(devices, ...resolve_lower_devices(null, device));
+                       for (let device in zone.related_physdevs)
+                               push(devices, ...resolve_lower_devices(devstatus, device, false));
 
-               return uniq(devices);
+               return sort(uniq(devices));
        },
 
        check_set_types: function() {
@@ -715,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");
 
@@ -723,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();
@@ -827,6 +846,7 @@ return {
                                        return null;
 
                                m = to_mask(b, length(a) == 16);
+                               b = max(-1, b);
                        }
 
                        return [{
@@ -1025,7 +1045,7 @@ return {
                if (!rv)
                        return null;
 
-               let helper = filter(this.state.helpers, h => (h.name == rv.val))[0];
+               let helper = filter(this.state.helpers, h => (h.name == rv.val))?.[0];
 
                return helper ? { ...rv, ...helper } : null;
        },
@@ -1226,26 +1246,21 @@ return {
        },
 
        parse_date: function(val) {
-               let m = match(val, /^([0-9-]+)T([0-9:]+)$/);
-               let d = m ? match(m[1], /^([0-9]{1,4})(-([0-9]{1,2})(-([0-9]{1,2}))?)?$/) : null;
-               let t = this.parse_time(m[2]);
+               let d = match(val, /^([0-9]{4})(-([0-9]{1,2})(-([0-9]{1,2})(T([0-9:]+))?)?)?$/);
 
-               d[3] ||= 1;
-               d[5] ||= 1;
-
-               if (d == null || d[1] < 1970 || d[1] > 2038 || d[3] < 1 || d[3] > 12 || d[5] < 1 || d[5] > 31)
+               if (d == null || d[1] < 1970 || d[1] > 2038 || d[3] > 12 || d[5] > 31)
                        return null;
 
-               if (m[2] && !t)
+               let t = this.parse_time(d[7] ?? "0");
+
+               if (t == null)
                        return null;
 
                return {
                        year:  +d[1],
-                       month: +d[3],
-                       day:   +d[5],
-                       hour:  t ? +t[1] : 0,
-                       min:   t ? +t[3] : 0,
-                       sec:   t ? +t[5] : 0
+                       month: +d[3] || 1,
+                       day:   +d[5] || 1,
+                       ...t
                };
        },
 
@@ -1432,7 +1447,7 @@ return {
                        case 'ipv6_addr':
                                ip = filter(this.parse_subnet(values[i]), a => (a.family == 6));
 
-                               switch(length(ip)) {
+                               switch (length(ip) ?? 0) {
                                case 0: return null;
                                case 1: break;
                                case 2: this.warn("Set entry '%s' resolves to multiple addresses, using first one", values[i]);
@@ -1468,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;
        },
@@ -1631,6 +1669,10 @@ return {
                return sprintf('"%04d-%02d-%02d"', stamp.year, stamp.month, stamp.day);
        },
 
+       datestamp: function(stamp) {
+               return exists(stamp, 'hour') ? this.datetime(stamp) : this.date(stamp);
+       },
+
        time: function(stamp) {
                return sprintf('"%02d:%02d:%02d"', stamp.hour, stamp.min, stamp.sec);
        },
@@ -1640,16 +1682,7 @@ return {
        },
 
        is_loopback_dev: function(dev) {
-               let fd = fs.open(`/sys/class/net/${dev}/flags`, "r");
-
-               if (!fd)
-                       return false;
-
-               let flags = +fd.read("line");
-
-               fd.close();
-
-               return !!(flags & 0x8);
+               return !!(+fs.readfile(`/sys/class/net/${dev}/flags`) & 0x8);
        },
 
        is_loopback_addr: function(addr) {
@@ -1701,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");
 
@@ -1870,9 +1933,11 @@ return {
                        this.warn_section(data, "is disabled, ignoring section");
                        return;
                }
-               else if (zone.helper && !zone.helper.available) {
-                       this.warn_section(data, `uses unavailable ct helper '${zone.helper.name}', ignoring section`);
-                       return;
+
+               for (let helper in zone.helper) {
+                       if (!helper.available) {
+                               this.warn_section(data, `uses unavailable ct helper '${zone.helper.name}'`);
+                       }
                }
 
                if (zone.mtu_fix && this.kernel < 0x040a0000) {
@@ -1894,7 +1959,9 @@ return {
                                push(related_ubus_networks, { invert: false, device: name });
                }
 
-               for (let e in [ ...to_array(zone.network), ...related_ubus_networks ]) {
+               zone.network = [ ...to_array(zone.network), ...related_ubus_networks ];
+
+               for (let e in zone.network) {
                        if (exists(this.state.networks, e.device)) {
                                let net = this.state.networks[e.device];
 
@@ -1939,7 +2006,6 @@ return {
                };
 
                let family = infer_family(zone.family, [
-                       zone.helper, "ct helper",
                        match_subnets, "subnet list"
                ]);
 
@@ -2099,7 +2165,7 @@ return {
                                proto: { any: true }
                        };
 
-                       f.name ||= `Accept ${fwd.src.any ? "any" : fwd.src.zone.name} to ${fwd.dest.any ? "any" : fwd.dest.zone.name} forwarding`;
+                       f.name ||= `Accept ${fwd.src.any ? "any" : fwd.src.zone.name} to ${fwd.dest.any ? "any" : fwd.dest.zone.name} ${family ? `${this.nfproto(family, true)} ` : ''}forwarding`;
                        f.chain = fwd.src.any ? "forward" : `forward_${fwd.src.zone.name}`;
 
                        if (fwd.dest.any)
@@ -2111,31 +2177,15 @@ return {
                };
 
 
-               let family = fwd.family;
-
                /* inherit family restrictions from related zones */
-               if (family === 0 || family === null) {
-                       let f1 = fwd.src.zone ? fwd.src.zone.family : 0;
-                       let f2 = fwd.dest.zone ? fwd.dest.zone.family : 0;
-
-                       if (f1 && f2 && f1 != f2) {
-                               this.warn_section(data,
-                                       `references src ${fwd.src.zone.name} restricted to ${this.nfproto(f1, true)} and dest ${fwd.dest.zone.name} restricted to ${this.nfproto(f2, true)}, ignoring forwarding`);
-
-                               return;
-                       }
-                       else if (f1) {
-                               this.warn_section(data,
-                                       `inheriting ${this.nfproto(f1, true)} restriction from src ${fwd.src.zone.name}`);
-
-                               family = f1;
-                       }
-                       else if (f2) {
-                               this.warn_section(data,
-                                       `inheriting ${this.nfproto(f2, true)} restriction from dest ${fwd.dest.zone.name}`);
+               let family = infer_family(fwd.family, [
+                       fwd.src?.zone, "source zone",
+                       fwd.dest?.zone, "destination zone"
+               ]);
 
-                               family = f2;
-                       }
+               if (type(family) == "string") {
+                       this.warn_section(data, `${family}, skipping`);
+                       return;
                }
 
                add_rule(family, fwd);
@@ -2193,6 +2243,7 @@ return {
                        set_dscp: [ "dscp", null, NO_INVERT ],
 
                        counter: [ "bool", "1" ],
+                       log: [ "string" ],
 
                        target: [ "target" ]
                });
@@ -2227,6 +2278,15 @@ return {
                        return;
                }
 
+               switch (this.parse_bool(rule.log)) {
+               case true:
+                       rule.log = rule.name;
+                       break;
+
+               case false:
+                       delete rule.log;
+               }
+
                let ipset;
 
                if (rule.ipset) {
@@ -2499,6 +2559,7 @@ return {
                        reflection_zone: [ "zone_ref", null, PARSE_LIST ],
 
                        counter: [ "bool", "1" ],
+                       log: [ "string" ],
 
                        target: [ "target", "dnat" ]
                });
@@ -2517,6 +2578,15 @@ return {
                        redir.target = "dnat";
                }
 
+               switch (this.parse_bool(redir.log)) {
+               case true:
+                       redir.log = redir.name;
+                       break;
+
+               case false:
+                       delete redir.log;
+               }
+
                let ipset;
 
                if (redir.ipset) {
@@ -2605,7 +2675,6 @@ return {
                        redir.dest.zone.dflags[redir.target] = true;
                }
 
-
                let add_rule = (family, proto, saddrs, daddrs, raddrs, sport, dport, rport, ipset, redir) => {
                        let r = {
                                ...redir,
@@ -3022,6 +3091,71 @@ 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.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               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" ],
@@ -3108,6 +3242,12 @@ return {
                        interval: interval
                };
 
+               if (s.interval)
+                       push(s.flags ??= [], 'interval');
+
+               if (s.timeout >= 0)
+                       push(s.flags ??= [], 'timeout');
+
                s.entries = filter(map(ipset.entry, (e) => {
                        let v = this.parse_ipsetentry(e, s);