};
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;
}
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':
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();
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');
}
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() {
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");
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();
return null;
m = to_mask(b, length(a) == 16);
+ b = max(-1, b);
}
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;
},
},
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
};
},
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]);
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;
},
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);
},
},
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) {
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");
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) {
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];
};
let family = infer_family(zone.family, [
- zone.helper, "ct helper",
match_subnets, "subnet list"
]);
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)
};
- 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);
set_dscp: [ "dscp", null, NO_INVERT ],
counter: [ "bool", "1" ],
+ log: [ "string" ],
target: [ "target" ]
});
return;
}
+ switch (this.parse_bool(rule.log)) {
+ case true:
+ rule.log = rule.name;
+ break;
+
+ case false:
+ delete rule.log;
+ }
+
let ipset;
if (rule.ipset) {
reflection_zone: [ "zone_ref", null, PARSE_LIST ],
counter: [ "bool", "1" ],
+ log: [ "string" ],
target: [ "target", "dnat" ]
});
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) {
redir.dest.zone.dflags[redir.target] = true;
}
-
let add_rule = (family, proto, saddrs, daddrs, raddrs, sport, dport, rport, ipset, redir) => {
let r = {
...redir,
}
},
+ 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" ],
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);