Initial commit
authorJo-Philipp Wich <jo@mein.io>
Fri, 19 Mar 2021 18:26:04 +0000 (19:26 +0100)
committerJo-Philipp Wich <jo@mein.io>
Fri, 19 Mar 2021 18:26:04 +0000 (19:26 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
39 files changed:
.editorconfig [new file with mode: 0644]
root/etc/config/firewall [new file with mode: 0644]
root/etc/hotplug.d/iface/20-firewall [new file with mode: 0644]
root/etc/init.d/firewall [new file with mode: 0755]
root/etc/nftables.d/10-custom-filter-chains.nft [new file with mode: 0644]
root/etc/nftables.d/README [new file with mode: 0644]
root/usr/sbin/fw3 [new symlink]
root/usr/sbin/fw4 [new file with mode: 0755]
root/usr/share/firewall4/helpers [new file with mode: 0644]
root/usr/share/firewall4/main.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/redirect.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/rule.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/ruleset.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/zone-match.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/zone-mssfix.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/zone-notrack.uc [new file with mode: 0644]
root/usr/share/firewall4/templates/zone-verdict.uc [new file with mode: 0644]
root/usr/share/ucode/fw4.uc [new file with mode: 0644]
run_tests.sh [new file with mode: 0755]
tests/01_configuration/01_ruleset [new file with mode: 0644]
tests/mock.uc [new file with mode: 0644]
tests/mocks/fs/open~_proc_version.txt [new file with mode: 0644]
tests/mocks/fs/open~_sys_class_net_br-lan_flags.txt [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_amanda.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_ftp.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_h323.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_irc.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_netbios_ns.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_pptp.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_rtsp.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_sane.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_sip.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_snmp.json [new file with mode: 0644]
tests/mocks/fs/stat~_sys_module_nf_conntrack_tftp.json [new file with mode: 0644]
tests/mocks/ubus/network.interface~dump.json [new file with mode: 0644]
tests/mocks/ubus/service~get_data~type-firewall.json [new file with mode: 0644]
tests/mocks/uci/firewall.json [new file with mode: 0644]
tests/mocks/uci/helpers.json [new file with mode: 0644]
tests/test-wrapper.uc [new file with mode: 0644]

diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..c881546
--- /dev/null
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = tab
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/root/etc/config/firewall b/root/etc/config/firewall
new file mode 100644 (file)
index 0000000..f4a3322
--- /dev/null
@@ -0,0 +1,191 @@
+config defaults
+       option syn_flood        1
+       option input            ACCEPT
+       option output           ACCEPT
+       option forward          REJECT
+# Uncomment this line to disable ipv6 rules
+#      option disable_ipv6     1
+
+config zone
+       option name             lan
+       list   network          'lan'
+       option input            ACCEPT
+       option output           ACCEPT
+       option forward          ACCEPT
+
+config zone
+       option name             wan
+       list   network          'wan'
+       list   network          'wan6'
+       option input            REJECT
+       option output           ACCEPT
+       option forward          REJECT
+       option masq             1
+       option mtu_fix          1
+
+config forwarding
+       option src              lan
+       option dest             wan
+
+# We need to accept udp packets on port 68,
+# see https://dev.openwrt.org/ticket/4108
+config rule
+       option name             Allow-DHCP-Renew
+       option src              wan
+       option proto            udp
+       option dest_port        68
+       option target           ACCEPT
+       option family           ipv4
+
+# Allow IPv4 ping
+config rule
+       option name             Allow-Ping
+       option src              wan
+       option proto            icmp
+       option icmp_type        echo-request
+       option family           ipv4
+       option target           ACCEPT
+
+config rule
+       option name             Allow-IGMP
+       option src              wan
+       option proto            igmp
+       option family           ipv4
+       option target           ACCEPT
+
+# Allow DHCPv6 replies
+# see https://dev.openwrt.org/ticket/10381
+config rule
+       option name             Allow-DHCPv6
+       option src              wan
+       option proto            udp
+       option src_ip           fc00::/6
+       option dest_ip          fc00::/6
+       option dest_port        546
+       option family           ipv6
+       option target           ACCEPT
+
+config rule
+       option name             Allow-MLD
+       option src              wan
+       option proto            icmp
+       option src_ip           fe80::/10
+       list icmp_type          '130/0'
+       list icmp_type          '131/0'
+       list icmp_type          '132/0'
+       list icmp_type          '143/0'
+       option family           ipv6
+       option target           ACCEPT
+
+# Allow essential incoming IPv6 ICMP traffic
+config rule
+       option name             Allow-ICMPv6-Input
+       option src              wan
+       option proto    icmp
+       list icmp_type          echo-request
+       list icmp_type          echo-reply
+       list icmp_type          destination-unreachable
+       list icmp_type          packet-too-big
+       list icmp_type          time-exceeded
+       list icmp_type          bad-header
+       list icmp_type          unknown-header-type
+       list icmp_type          router-solicitation
+       list icmp_type          neighbour-solicitation
+       list icmp_type          router-advertisement
+       list icmp_type          neighbour-advertisement
+       option limit            1000/sec
+       option family           ipv6
+       option target           ACCEPT
+
+# Allow essential forwarded IPv6 ICMP traffic
+config rule
+       option name             Allow-ICMPv6-Forward
+       option src              wan
+       option dest             *
+       option proto            icmp
+       list icmp_type          echo-request
+       list icmp_type          echo-reply
+       list icmp_type          destination-unreachable
+       list icmp_type          packet-too-big
+       list icmp_type          time-exceeded
+       list icmp_type          bad-header
+       list icmp_type          unknown-header-type
+       option limit            1000/sec
+       option family           ipv6
+       option target           ACCEPT
+
+config rule
+       option name             Allow-IPSec-ESP
+       option src              wan
+       option dest             lan
+       option proto            esp
+       option target           ACCEPT
+
+config rule
+       option name             Allow-ISAKMP
+       option src              wan
+       option dest             lan
+       option dest_port        500
+       option proto            udp
+       option target           ACCEPT
+
+
+### EXAMPLE CONFIG SECTIONS
+# do not allow a specific ip to access wan
+#config rule
+#      option src              lan
+#      option src_ip   192.168.45.2
+#      option dest             wan
+#      option proto    tcp
+#      option target   REJECT
+
+# block a specific mac on wan
+#config rule
+#      option dest             wan
+#      option src_mac  00:11:22:33:44:66
+#      option target   REJECT
+
+# block incoming ICMP traffic on a zone
+#config rule
+#      option src              lan
+#      option proto    ICMP
+#      option target   DROP
+
+# port redirect port coming in on wan to lan
+#config redirect
+#      option src                      wan
+#      option src_dport        80
+#      option dest                     lan
+#      option dest_ip          192.168.16.235
+#      option dest_port        80
+#      option proto            tcp
+
+# port redirect of remapped ssh port (22001) on wan
+#config redirect
+#      option src              wan
+#      option src_dport        22001
+#      option dest             lan
+#      option dest_port        22
+#      option proto            tcp
+
+### FULL CONFIG SECTIONS
+#config rule
+#      option src              lan
+#      option src_ip   192.168.45.2
+#      option src_mac  00:11:22:33:44:55
+#      option src_port 80
+#      option dest             wan
+#      option dest_ip  194.25.2.129
+#      option dest_port        120
+#      option proto    tcp
+#      option target   REJECT
+
+#config redirect
+#      option src              lan
+#      option src_ip   192.168.45.2
+#      option src_mac  00:11:22:33:44:55
+#      option src_port         1024
+#      option src_dport        80
+#      option dest_ip  194.25.2.129
+#      option dest_port        120
+#      option proto    tcp
diff --git a/root/etc/hotplug.d/iface/20-firewall b/root/etc/hotplug.d/iface/20-firewall
new file mode 100644 (file)
index 0000000..c2ed89a
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+[ "$ACTION" = ifup -o "$ACTION" = ifupdate ] || exit 0
+[ "$ACTION" = ifupdate -a -z "$IFUPDATE_ADDRESSES" -a -z "$IFUPDATE_DATA" ] && exit 0
+
+/etc/init.d/firewall enabled || exit 0
+
+fw4 -q network "$INTERFACE" >/dev/null || exit 0
+
+logger -t firewall "Reloading firewall due to $ACTION of $INTERFACE ($DEVICE)"
+fw4 -q reload
diff --git a/root/etc/init.d/firewall b/root/etc/init.d/firewall
new file mode 100755 (executable)
index 0000000..f4bdd99
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/sh /etc/rc.common
+
+START=19
+USE_PROCD=1
+QUIET=""
+
+service_triggers() {
+       procd_add_reload_trigger firewall
+}
+
+restart() {
+       fw4 restart
+}
+
+start_service() {
+       fw4 ${QUIET} start
+}
+
+stop_service() {
+       fw4 flush
+}
+
+reload_service() {
+       fw4 reload
+}
+
+boot() {
+       # Be silent on boot, firewall might be started by hotplug already,
+       # so don't complain in syslog.
+       QUIET=-q
+       start_service
+}
diff --git a/root/etc/nftables.d/10-custom-filter-chains.nft b/root/etc/nftables.d/10-custom-filter-chains.nft
new file mode 100644 (file)
index 0000000..4cb4213
--- /dev/null
@@ -0,0 +1,39 @@
+## The firewall4 input, forward and output chains are registered with
+## priority `filter` (0).
+
+
+## Uncomment the chains below if you want to stage rules *before* the
+## default firewall input, forward and output chains.
+
+# chain user_pre_input {
+#     type filter hook input priority -1; policy accept;
+#     tcp dport ssh ct state new log prefix "SSH connection attempt: "
+# }
+#
+# chain user_pre_forward {
+#     type filter hook forward priority -1; policy accept;
+# }
+#
+# chain user_pre_output {
+#     type filter hook output priority -1; policy accept;
+# }
+
+
+## Uncomment the chains below if you want to stage rules *after* the
+## default firewall input, forward and output chains.
+
+# chain user_post_input {
+#     type filter hook input priority 1; policy accept;
+#     ct state new log prefix "Firewall4 accepted ingress: "
+# }
+#
+# chain user_post_forward {
+#     type filter hook forward priority 1; policy accept;
+#     ct state new log prefix "Firewall4 accepted forward: "
+# }
+#
+# chain user_post_output {
+#     type filter hook output priority 1; policy accept;
+#     ct state new log prefix "Firewall4 accepted egress: "
+# }
+
diff --git a/root/etc/nftables.d/README b/root/etc/nftables.d/README
new file mode 100644 (file)
index 0000000..70f4779
--- /dev/null
@@ -0,0 +1,4 @@
+All *.nft files in this directory are included by the firewall4 ruleset
+within the inet/fw4 table context which allows referencing named sets
+declared and populated by the firewall configuration.
+
diff --git a/root/usr/sbin/fw3 b/root/usr/sbin/fw3
new file mode 120000 (symlink)
index 0000000..8fbcf2c
--- /dev/null
@@ -0,0 +1 @@
+fw4
\ No newline at end of file
diff --git a/root/usr/sbin/fw4 b/root/usr/sbin/fw4
new file mode 100755 (executable)
index 0000000..ac95473
--- /dev/null
@@ -0,0 +1,164 @@
+#!/bin/sh
+
+set -o pipefail
+
+MAIN=/usr/share/firewall4/main.uc
+LOCK=/var/run/fw4.lock
+STATE=/var/run/fw4.state
+VERBOSE=
+
+[ -t 2 ] && export TTY=1
+
+die() {
+       [ -n "$QUIET" ] || echo "$@" >&2
+       exit 1
+}
+
+start() {
+       {
+               flock -x 1000
+
+               case "$1" in
+                       start)
+                               [ -f $STATE ] && die "The fw4 firewall appears to be already loaded."
+                       ;;
+                       reload)
+                               [ ! -f $STATE ] && die "The fw4 firewall does not appear to be loaded."
+
+                               # Delete state to force reloading ubus state
+                               rm -f $STATE
+                       ;;
+               esac
+
+               ACTION=start \
+                       ucode -S -m fs -m uci -m ubus -m fw4 -i $MAIN | nft $VERBOSE -f /proc/self/fd/0
+       } 1000>$LOCK
+}
+
+print() {
+       ACTION=print \
+               ucode -S -m fs -m uci -m ubus -m fw4 -i $MAIN
+}
+
+stop() {
+       {
+               flock -x 1000
+
+               if nft list tables inet | grep -sq "table inet fw4"; then
+                       nft delete table inet fw4
+                       rm -f $STATE
+               else
+                       die "The fw4 firewall does not appear to be loaded, try fw4 flush to delete all rules."
+               fi
+       } 1000>$LOCK
+}
+
+flush() {
+       {
+               flock -x 1000
+
+               local dummy family table
+               nft list tables | while read dummy family table; do
+                       nft delete table "$family" "$table"
+               done
+
+               rm -f $STATE
+       } 1000>$LOCK
+}
+
+reload_sets() {
+       ACTION=reload-sets \
+               flock -x $LOCK ucode -S -m fs -m uci -m ubus -m fw4 -i $MAIN | nft $VERBOSE -f /proc/self/fd/0
+}
+
+lookup() {
+       ACTION=$1 OBJECT=$2 DEVICE=$3 \
+               flock -x $LOCK ucode -S -m fs -m uci -m ubus -m fw4 -i $MAIN
+}
+
+while [ -n "$1" ]; do
+       case "$1" in
+               -q)
+                       export QUIET=1
+                       shift
+               ;;
+               -v)
+                       export VERBOSE=-e
+                       shift
+               ;;
+               *)
+                       break
+               ;;
+       esac
+done
+
+case "$1" in
+       start|reload)
+               start "$1"
+       ;;
+       stop)
+               stop
+       ;;
+       flush)
+               flush
+       ;;
+       restart)
+               stop
+               start
+       ;;
+       print)
+               print
+       ;;
+       reload-sets)
+               reload_sets
+       ;;
+       network|device|zone)
+               lookup "$@"
+       ;;
+       *)
+               cat <<EOT
+Usage:
+
+  $0 [-v] [-q] start|stop|flush|restart|reload
+
+    Start, stop, flush, restart or reload the firewall respectively.
+
+
+  $0 [-v] [-q] reload-sets
+
+    Reload the contents of all declared sets but do not touch the
+    ruleset.
+
+
+  $0 [-q] print
+
+    Print the rendered ruleset.
+
+
+  $0 [-q] network {net}
+
+    Print the name of the firewall zone covering the given network.
+
+    Exits with code 1 if the network is not found or if no zone is
+    covering it.
+
+
+  $0 [-q] device {dev}
+
+    Print the name of the firewall zone covering the given device.
+
+    Exits with code 1 if the device is not found or if no zone is
+    covering it.
+
+
+  $0 [-q] zone {zone} [dev]
+
+    Print all covered devices of the given zone, optionally restricted
+    to only the given device name.
+
+    Exits with code 1 if zone is not found or if a device is specified
+    and not covered by the given zone.
+
+EOT
+       ;;
+esac
diff --git a/root/usr/share/firewall4/helpers b/root/usr/share/firewall4/helpers
new file mode 100644 (file)
index 0000000..5591a8f
--- /dev/null
@@ -0,0 +1,95 @@
+config helper
+       option name 'amanda'
+       option description 'Amanda backup and archiving proto'
+       option module 'nf_conntrack_amanda'
+       option family 'any'
+       option proto 'udp'
+       option port '10080'
+
+config helper
+       option name 'ftp'
+       option description 'FTP passive connection tracking'
+       option module 'nf_conntrack_ftp'
+       option family 'any'
+       option proto 'tcp'
+       option port '21'
+
+config helper
+       option name 'RAS'
+       option description 'RAS proto tracking'
+       option module 'nf_conntrack_h323'
+       option family 'any'
+       option proto 'udp'
+       option port '1719'
+
+config helper
+       option name 'Q.931'
+       option description 'Q.931 proto tracking'
+       option module 'nf_conntrack_h323'
+       option family 'any'
+       option proto 'tcp'
+       option port '1720'
+
+config helper
+       option name 'irc'
+       option description 'IRC DCC connection tracking'
+       option module 'nf_conntrack_irc'
+       option family 'ipv4'
+       option proto 'tcp'
+       option port '6667'
+
+config helper
+       option name 'netbios-ns'
+       option description 'NetBIOS name service broadcast tracking'
+       option module 'nf_conntrack_netbios_ns'
+       option family 'ipv4'
+       option proto 'udp'
+       option port '137'
+
+config helper
+       option name 'pptp'
+       option description 'PPTP VPN connection tracking'
+       option module 'nf_conntrack_pptp'
+       option family 'ipv4'
+       option proto 'tcp'
+       option port '1723'
+
+config helper
+       option name 'sane'
+       option description 'SANE scanner connection tracking'
+       option module 'nf_conntrack_sane'
+       option family 'any'
+       option proto 'tcp'
+       option port '6566'
+
+config helper
+       option name 'sip'
+       option description 'SIP VoIP connection tracking'
+       option module 'nf_conntrack_sip'
+       option family 'any'
+       option proto 'udp'
+       option port '5060'
+
+config helper
+       option name 'snmp'
+       option description 'SNMP monitoring connection tracking'
+       option module 'nf_conntrack_snmp'
+       option family 'ipv4'
+       option proto 'udp'
+       option port '161'
+
+config helper
+       option name 'tftp'
+       option description 'TFTP connection tracking'
+       option module 'nf_conntrack_tftp'
+       option family 'any'
+       option proto 'udp'
+       option port '69'
+
+config helper
+       option name 'rtsp'
+       option description 'RTSP connection tracking'
+       option module 'nf_conntrack_rtsp'
+       option family 'ipv4'
+       option proto 'tcp'
+       option port '554'
diff --git a/root/usr/share/firewall4/main.uc b/root/usr/share/firewall4/main.uc
new file mode 100644 (file)
index 0000000..d71e876
--- /dev/null
@@ -0,0 +1,177 @@
+{%
+
+/* Find existing sets.
+ *
+ * Unfortunately, terse mode (-t) is incompatible with JSON output so
+ * we either need to parse a potentially huge JSON just to get the set
+ * header data or scrape the ordinary nft output to obtain the same
+ * information. Opt for the latter to avoid parsing potentially huge
+ * JSON documents.
+ */
+function find_existing_sets() {
+       let fd = fs.popen("nft -t list sets inet", "r");
+
+       if (!fd) {
+               warn(sprintf("Unable to execute 'nft' for listing existing sets: %s\n",
+                            fs.error()));
+               return {};
+       }
+
+       let line, table, set;
+       let sets = {};
+
+       while ((line = fd.read("line")) !== "") {
+               let m;
+
+               if ((m = match(line, /^table inet (.+) \{\n$/)) != null) {
+                       table = m[1];
+               }
+               else if ((m = match(line, /^\tset (.+) \{\n$/)) != null) {
+                       set = m[1];
+               }
+               else if ((m = match(line, /^\t\ttype (.+)\n$/)) != null) {
+                       if (table == "fw4" && set)
+                               sets[set] = split(m[1], " . ");
+
+                       set = null;
+               }
+       }
+
+       fd.close();
+
+       return sets;
+}
+
+function read_state() {
+       let state = fw4.read_state();
+
+       if (!state) {
+               warn("Unable to read firewall state - do you need to start the firewall?\n");
+               exit(1);
+       }
+
+       return state;
+}
+
+function reload_sets() {
+       let state = read_state(),
+           sets = find_existing_sets();
+
+       for (let set in state.ipsets) {
+               if (!set.loadfile || !length(set.entries))
+                       continue;
+
+               if (!exists(sets, set.name)) {
+                       warn(sprintf("Named set '%s' does not exist - do you need to restart the firewall?\n",
+                                    set.name));
+                       continue;
+               }
+               else if (fw4.concat(sets[set.name]) != fw4.concat(set.types)) {
+                       warn(sprintf("Named set '%s' has a different type - want '%s' but is '%s' - do you need to restart the firewall?\n",
+                                    set.name, fw4.concat(set.types), fw4.concat(sets[set.name])));
+                       continue;
+               }
+
+               let first = true;
+               let printer = (entry) => {
+                       if (first) {
+                               print("add element inet fw4 ", set.name, " {\n");
+                               first = false;
+                       }
+
+                       print("\t", join(" . ", entry), ",\n");
+               };
+
+               print("flush set inet fw4 ", set.name, "\n");
+
+               map(set.entries, printer);
+               fw4.parse_setfile(set, printer);
+
+               if (!first)
+                       print("}\n\n");
+       }
+}
+
+function render_ruleset(use_statefile) {
+       fw4.load(use_statefile);
+
+       include("templates/ruleset.uc", { fw4, type, exists, length, include });
+}
+
+function lookup_network(net) {
+       let state = read_state();
+
+       for (let zone in state.zones) {
+               for (let network in (zone.network || [])) {
+                       if (network.device == net) {
+                               print(zone.name, "\n");
+                               exit(0);
+                       }
+               }
+       }
+
+       exit(1);
+}
+
+function lookup_device(dev) {
+       let state = read_state();
+
+       for (let zone in state.zones) {
+               for (let rule in (zone.match_rules || [])) {
+                       if (dev in rule.devices_pos) {
+                               print(zone.name, "\n");
+                               exit(0);
+                       }
+               }
+       }
+
+       exit(1);
+}
+
+function lookup_zone(name, dev) {
+       let state = read_state();
+
+       for (let zone in state.zones) {
+               if (zone.name == name) {
+                       let devices = [];
+                       map(zone.match_rules, (r) => push(devices, ...(r.devices_pos || [])));
+
+                       if (dev) {
+                               if (dev in devices) {
+                                       print(dev, "\n");
+                                       exit(0);
+                               }
+
+                               exit(1);
+                       }
+
+                       if (length(devices))
+                               print(join("\n", devices), "\n");
+
+                       exit(0);
+               }
+       }
+
+       exit(1);
+}
+
+
+switch (getenv("ACTION")) {
+case "start":
+       return render_ruleset(true);
+
+case "print":
+       return render_ruleset(false);
+
+case "reload-sets":
+       return reload_sets();
+
+case "network":
+       return lookup_network(getenv("OBJECT"));
+
+case "device":
+       return lookup_device(getenv("OBJECT"));
+
+case "zone":
+       return lookup_zone(getenv("OBJECT"), getenv("DEVICE"));
+}
diff --git a/root/usr/share/firewall4/templates/redirect.uc b/root/usr/share/firewall4/templates/redirect.uc
new file mode 100644 (file)
index 0000000..b710a04
--- /dev/null
@@ -0,0 +1,67 @@
+{%+ if (redirect.family && !redirect.has_addrs): -%}
+       meta nfproto {{ fw4.nfproto(redirect.family) }} {%+ endif -%}
+{%+ if (!redirect.proto.any && !redirect.has_ports): -%}
+       meta l4proto {{
+               (redirect.proto.name == 'icmp' && redirect.family == 6) ? 'ipv6-icmp' : redirect.proto.name
+       }} {%+ endif -%}
+{%+ if (redirect.device): -%}
+       oifname {{ fw4.quote(redirect.device, true) }} {%+ endif -%}
+{%+ if (redirect.saddrs_pos): -%}
+       {{ fw4.ipproto(redirect.family) }} saddr {{ fw4.set(redirect.saddrs_pos) }} {%+ endif -%}
+{%+ if (redirect.saddrs_neg): -%}
+       {{ fw4.ipproto(redirect.family) }} saddr != {{ fw4.set(redirect.saddrs_neg) }} {%+ endif -%}
+{%+ if (redirect.daddrs_pos): -%}
+       {{ fw4.ipproto(redirect.family) }} daddr {{ fw4.set(redirect.daddrs_pos) }} {%+ endif -%}
+{%+ if (redirect.daddrs_neg): -%}
+       {{ fw4.ipproto(redirect.family) }} daddr != {{ fw4.set(redirect.daddrs_neg) }} {%+ endif -%}
+{%+ if (redirect.sports_pos): -%}
+       {{ redirect.proto.name }} sport {{ fw4.set(redirect.sports_pos) }} {%+ endif -%}
+{%+ if (redirect.sports_neg): -%}
+       {{ redirect.proto.name }} sport != {{ fw4.set(redirect.sports_neg) }} {%+ endif -%}
+{%+ if (redirect.dports_pos): -%}
+       {{ redirect.proto.name }} dport {{ fw4.set(redirect.dports_pos) }} {%+ endif -%}
+{%+ if (redirect.dports_neg): -%}
+       {{ redirect.proto.name }} dport != {{ fw4.set(redirect.dports_neg) }} {%+ endif -%}
+{%+ if (redirect.smacs_pos): -%}
+       ether saddr {{ fw4.set(redirect.smacs_pos) }} {%+ endif -%}
+{%+ if (redirect.smacs_neg): -%}
+       ether saddr != {{ fw4.set(redirect.smacs_neg) }} {%+ endif -%}
+{%+ if (redirect.helper): -%}
+       ct helper{% if (redirect.helper.invert): %} !={% endif %} {{ fw4.quote(redirect.helper.name, true) }} {%+ endif -%}
+{%+ if (redirect.limit): -%}
+       limit rate {{ redirect.limit.rate }}/{{ redirect.limit.unit }}
+       {%- if (redirect.limit_burst): %} burst {{ redirect.limit_burst }} packets{% endif %} {%+ endif -%}
+{%+ if (redirect.start_date): -%}
+       meta time >= {{
+               exists(redirect.start_date, "hour") ? fw4.datetime(redirect.start_date) : fw4.date(redirect.start_date)
+       }} {%+ endif -%}
+{%+ if (redirect.stop_date): -%}
+       meta time <= {{
+               exists(redirect.stop_date, "hour") ? fw4.datetime(redirect.stop_date) : fw4.date(redirect.stop_date)
+       }} {%+ endif -%}
+{%+ if (redirect.start_time): -%}
+       meta hour >= {{ fw4.time(redirect.start_time) }} {%+ endif -%}
+{%+ if (redirect.stop_time): -%}
+       meta hour <= {{ fw4.time(redirect.stop_time) }} {%+ endif -%}
+{%+ if (redirect.weekdays): -%}
+       meta day{% if (redirect.weekdays.invert): %} !={% endif %} {{ fw4.set(redirect.weekdays.days) }} {%+ endif -%}
+{%+ if (redirect.mark && redirect.mark.mask < 0xFFFFFFFF): -%}
+       meta mark and {{ fw4.hex(redirect.mark.mask) }} {{
+               redirect.mark.invert ? '!=' : '=='
+       }} {{ fw4.hex(redirect.mark.mark) }} {%+ endif -%}
+{%+ if (redirect.mark && redirect.mark.mask == 0xFFFFFFFF): -%}
+       meta mark{% if (redirect.mark.invert): %} !={% endif %} {{ fw4.hex(redirect.mark.mark) }} {%+ endif -%}
+{%+ if (redirect.ipset): -%}
+       {{ fw4.concat(redirect.ipset.fields) }}{{
+               redirect.ipset.invert ? ' !=' : ''
+       }} @{{ redirect.ipset.name }} {%+ endif -%}
+{%+ if (redirect.counter): -%}
+       counter {%+ endif -%}
+{% if (redirect.target == "redirect"): -%}
+       redirect to {{ fw4.port(redirect.rport) }}
+{%- elif (redirect.target == "accept" || redirect.target == "masquerade"): -%}
+       {{ redirect.target }}
+{%- else -%}
+       {{ redirect.target }} {{ redirect.raddr ? fw4.host(redirect.raddr) : '' }}
+       {%- if (redirect.rport): %}:{{ fw4.port(redirect.rport) }}{% endif %}
+{% endif %} comment {{ fw4.quote("!fw4: " + redirect.name, true) }}
diff --git a/root/usr/share/firewall4/templates/rule.uc b/root/usr/share/firewall4/templates/rule.uc
new file mode 100644 (file)
index 0000000..1f01d5f
--- /dev/null
@@ -0,0 +1,90 @@
+{%+ if (rule.family && !rule.has_addrs): -%}
+       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+{%+ if (!rule.proto.any && !rule.has_ports && !rule.icmp_types && !rule.icmp_codes): -%}
+       meta l4proto {{
+               (rule.proto.name == 'icmp' && rule.family == 6) ? 'ipv6-icmp' : rule.proto.name
+       }} {%+ endif -%}
+{%+ if (rule.saddrs_pos): -%}
+       {{ fw4.ipproto(rule.family) }} saddr {{ fw4.set(rule.saddrs_pos) }} {%+ endif -%}
+{%+ if (rule.saddrs_neg): -%}
+       {{ fw4.ipproto(rule.family) }} saddr != {{ fw4.set(rule.saddrs_neg) }} {%+ endif -%}
+{%+ if (rule.daddrs_pos): -%}
+       {{ fw4.ipproto(rule.family) }} daddr {{ fw4.set(rule.daddrs_pos) }} {%+ endif -%}
+{%+ if (rule.daddrs_neg): -%}
+       {{ fw4.ipproto(rule.family) }} daddr != {{ fw4.set(rule.daddrs_neg) }} {%+ endif -%}
+{%+ if (rule.sports_pos): -%}
+       {{ rule.proto.name }} sport {{ fw4.set(rule.sports_pos) }} {%+ endif -%}
+{%+ if (rule.sports_neg): -%}
+       {{ rule.proto.name }} sport != {{ fw4.set(rule.sports_neg) }} {%+ endif -%}
+{%+ if (rule.dports_pos): -%}
+       {{ rule.proto.name }} dport {{ fw4.set(rule.dports_pos) }} {%+ endif -%}
+{%+ if (rule.dports_neg): -%}
+       {{ rule.proto.name }} dport != {{ fw4.set(rule.dports_neg) }} {%+ endif -%}
+{%+ if (rule.smacs_pos): -%}
+       ether saddr {{ fw4.set(rule.smacs_pos) }} {%+ endif -%}
+{%+ if (rule.smacs_neg): -%}
+       ether saddr != {{ fw4.set(rule.smacs_neg) }} {%+ endif -%}
+{%+ if (rule.icmp_types): -%}
+       {{ (rule.family == 4) ? "icmp" : "icmpv6" }} type {{ fw4.set(rule.icmp_types) }} {%+ endif -%}
+{%+ if (rule.icmp_codes): -%}
+       {{ (rule.family == 4) ? "icmp" : "icmpv6" }} type . {{ (rule.family == 4) ? "icmp" : "icmpv6" }} code {{
+               fw4.set(rule.icmp_codes)
+       }} {%+ endif -%}
+{%+ if (rule.helper): -%}
+       ct helper{% if (rule.helper.invert): %} !={% endif %} {{ fw4.quote(rule.helper.name, true) }} {%+ endif -%}
+{%+ if (rule.limit): -%}
+       limit rate {{ rule.limit.rate }}/{{ rule.limit.unit }}
+       {%- if (rule.limit_burst): %} burst {{ rule.limit_burst }} packets{% endif %} {%+ endif -%}
+{%+ if (rule.start_date): -%}
+       meta time >= {{
+               exists(rule.start_date, "hour") ? fw4.datetime(rule.start_date) : fw4.date(rule.start_date)
+       }} {%+ endif -%}
+{%+ if (rule.stop_date): -%}
+       meta time <= {{
+               exists(rule.stop_date, "hour") ? fw4.datetime(rule.stop_date) : fw4.date(rule.stop_date)
+       }} {%+ endif -%}
+{%+ if (rule.start_time): -%}
+       meta hour >= {{ fw4.time(rule.start_time) }} {%+ endif -%}
+{%+ if (rule.stop_time): -%}
+       meta hour <= {{ fw4.time(rule.stop_time) }} {%+ endif -%}
+{%+ if (rule.weekdays): -%}
+       meta day{% if (rule.weekdays.invert): %} !={% endif %} {{ fw4.set(rule.weekdays.days) }} {%+ endif -%}
+{%+ if (rule.mark && rule.mark.mask < 0xFFFFFFFF): -%}
+       meta mark and {{ fw4.hex(rule.mark.mask) }} {{
+               rule.mark.invert ? '!=' : '=='
+       }} {{ fw4.hex(rule.mark.mark) }} {%+ endif -%}
+{%+ if (rule.mark && rule.mark.mask == 0xFFFFFFFF): -%}
+       meta mark{% if (rule.mark.invert): %} !={% endif %} {{ fw4.hex(rule.mark.mark) }} {%+ endif -%}
+{%+ if (rule.dscp): -%}
+       dscp{% if (rule.dscp.invert): %} !={% endif %} {{ fw4.hex(rule.dscp.dscp) }} {%+ endif -%}
+{%+ if (rule.ipset): -%}
+       {{ fw4.concat(rule.ipset.fields) }}{{
+               rule.ipset.invert ? ' !=' : ''
+       }} @{{ rule.ipset.name }} {%+ endif -%}
+{%+ if (rule.counter): -%}
+       counter {%+ endif -%}
+{%+ if (rule.log): -%}
+       log prefix {{ fw4.quote(rule.log, true) }} {%+ endif -%}
+{% if (rule.target == "mark"): -%}
+       meta mark set {{
+               (rule.set_xmark.mask == 0xFFFFFFFF)
+                       ? fw4.hex(rule.set_xmark.mark)
+                       : (rule.set_xmark.mark == 0)
+                               ? 'mark and ' + fw4.hex(~rule.set_xmark.mask & 0xFFFFFFFF)
+                               : (rule.set_xmark.mark == rule.set_xmark.mask)
+                                       ? 'mark or ' + fw4.hex(rule.set_xmark.mark)
+                                       : (rule.set_xmark.mask == 0)
+                                               ? 'mark xor ' + fw4.hex(rule.set_xmark.mark)
+                                               : 'mark and ' + fw4.hex(~r.set_xmark.mask & 0xFFFFFFFF) + ' xor ' + fw4.hex(r.set_xmark.mark)
+       }}
+{%- elif (rule.target == "dscp"): -%}
+       {{ fw4.ipproto(rule.family) }} dscp set {{ fw4.hex(rule.set_dscp.dscp) }}
+{%- elif (rule.target == "notrack"): -%}
+       notrack
+{%- elif (rule.target == "helper"): -%}
+       ct helper set {{ fw4.quote(rule.set_helper.name, true) }}
+{%- elif (rule.jump_chain): -%}
+       jump {{ rule.jump_chain }}
+{%- else -%}
+       {{ rule.target }}
+{%- endif %} comment {{ fw4.quote("!fw4: " + rule.name, true) }}
diff --git a/root/usr/share/firewall4/templates/ruleset.uc b/root/usr/share/firewall4/templates/ruleset.uc
new file mode 100644 (file)
index 0000000..b2a996d
--- /dev/null
@@ -0,0 +1,386 @@
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+{% for (let set in fw4.ipsets()): %}
+       set {{ set.name }} {
+               type {{ fw4.concat(set.types) }}
+{%  if (set.maxelem > 0): %}
+               size {{ set.maxelem }}
+{%  endif %}
+{%  if (set.interval): %}
+               flags interval
+{%  endif %}
+{%  fw4.print_setentries(set) %}
+       }
+
+{% endfor %}
+
+       #
+       # Defines
+       #
+
+{% for (let zone in fw4.zones()): %}
+{%  if (length(zone.match_devices)): %}
+       define {{ zone.name }}_devices = {{ fw4.set(zone.match_devices, true) }}
+{%  endif %}
+{%  if (length(zone.match_subnets)): %}
+       define {{ zone.name }}_subnets = {{ fw4.set(zone.match_subnets, true) }}
+{%  endif %}
+
+{% endfor %}
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy {{ fw4.input_policy(true) }};
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               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"
+{% endif %}
+
+{% if (fw4.default_option("synflood_protect")): %}
+               tcp flags & (fin | syn | rst | ack) == syn jump syn_flood comment "!fw4: Rate limit TCP syn packets"
+{% endif %}
+
+{% for (local rule in fw4.rules("input")): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+
+{% for (local zone in fw4.zones()): for (local rule in zone.match_rules): %}
+               {%+ include("zone-match.uc", { fw4, zone, rule, direction: "input" }) %}
+{% endfor; endfor %}
+
+{% if (fw4.input_policy() == "reject"): %}
+               jump handle_reject
+{% endif %}
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy {{ fw4.forward_policy(true) }};
+
+               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"
+{% endif %}
+
+{% for (local rule in fw4.rules("forward")): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+
+{% for (local zone in fw4.zones()): for (local rule in zone.match_rules): %}
+               {%+ include("zone-match.uc", { fw4, zone, rule, direction: "forward" }) %}
+{% endfor; endfor %}
+
+{% if (fw4.forward_policy() == "reject"): %}
+               jump handle_reject
+{% endif %}
+       }
+
+       chain output {
+               type filter hook output priority filter; policy {{ fw4.output_policy(true) }};
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               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"
+{% endif %}
+
+{% for (local rule in fw4.rules("output")): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+
+{% for (local zone in fw4.zones()): for (local rule in zone.match_rules): %}
+               {%+ include("zone-match.uc", { fw4, zone, rule, direction: "output" }) %}
+{% endfor; endfor %}
+
+{% if (fw4.output_policy() == "reject"): %}
+               jump handle_reject
+{% endif %}
+       }
+
+       chain handle_reject {
+               meta l4proto tcp reject with {{
+                       (fw4.default_option("tcp_reject_code") != "tcp-reset")
+                               ? "icmpx type " + fw4.default_option("tcp_reject_code")
+                               : "tcp reset"
+               }} comment "!fw4: Reject TCP traffic"
+               reject with {{
+                       (fw4.default_option("any_reject_code") != "tcp-reset")
+                               ? "icmpx type " + fw4.default_option("any_reject_code")
+                               : "tcp reset"
+               }} comment "!fw4: Reject any other traffic"
+       }
+
+{% if (fw4.default_option("synflood_protect")):
+       local r = fw4.default_option("synflood_rate");
+       local b = fw4.default_option("synflood_burst");
+%}
+       chain syn_flood {
+               tcp flags & (fin | syn | rst | ack) == syn
+               {%- if (r): %} limit rate {{ r.rate }}/{{ r.unit }}{% endif %}
+               {%- if (b): %} burst {{ b }} packets{% endif %} return comment "!fw4: Accept SYN packets below rate-limit"
+               drop comment "!fw4: Drop excess packets"
+       }
+
+{% endif %}
+
+{% for (local zone in fw4.zones()): %}
+       chain input_{{ zone.name }} {
+{%  for (local 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 %}
+               jump {{ zone.input }}_from_{{ zone.name }}
+       }
+
+       chain output_{{ zone.name }} {
+{%  for (local rule in fw4.rules("output_"+zone.name)): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{%  endfor %}
+               jump {{ zone.output }}_to_{{ zone.name }}
+       }
+
+       chain forward_{{ zone.name }} {
+{%  for (local 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 %}
+               jump {{ zone.forward }}_to_{{ zone.name }}
+       }
+
+{%  for (local verdict in ["accept", "reject", "drop"]): %}
+{%   if (zone.sflags[verdict]): %}
+       chain {{ verdict }}_from_{{ zone.name }} {
+{%    for (local rule in zone.match_rules): %}
+               {%+ include("zone-verdict.uc", { fw4, zone, rule, egress: false, verdict }) %}
+{%    endfor %}
+       }
+
+{%   endif %}
+{%   if (zone.dflags[verdict]): %}
+       chain {{ verdict }}_to_{{ zone.name }} {
+{%   for (local rule in zone.match_rules): %}
+               {%+ include("zone-verdict.uc", { fw4, zone, rule, egress: true, verdict }) %}
+{%   endfor %}
+       }
+
+{%   endif %}
+{%  endfor %}
+{% endfor %}
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+
+{% for (let zone in fw4.zones()): %}
+{%  if (zone.dflags.dnat): %}
+{%   for (let rule in zone.match_rules): %}
+               {%+ include("zone-match.uc", { fw4, zone, rule, direction: "dstnat" }) %}
+{%   endfor %}
+{%  endif %}
+{% endfor %}
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+
+{% for (let redirect in fw4.redirects("srcnat")): %}
+               {%+ include("redirect.uc", { fw4, redirect }) %}
+{% endfor %}
+{% for (let zone in fw4.zones()): %}
+{%  if (zone.dflags.snat): %}
+{%   for (let rule in zone.match_rules): %}
+               {%+ include("zone-match.uc", { fw4, zone, rule, direction: "srcnat" }) %}
+{%   endfor %}
+{%  endif %}
+{% endfor %}
+       }
+
+{% for (let zone in fw4.zones()): %}
+{%  if (zone.dflags.dnat): %}
+       chain dstnat_{{ zone.name }} {
+{% for (let redirect in fw4.redirects("dstnat_"+zone.name)): %}
+               {%+ include("redirect.uc", { fw4, redirect }) %}
+{% endfor %}
+       }
+
+{%  endif %}
+{%  if (zone.dflags.snat): %}
+       chain srcnat_{{ zone.name }} {
+{% for (let redirect in fw4.redirects("srcnat_"+zone.name)): %}
+               {%+ include("redirect.uc", { fw4, redirect }) %}
+{% endfor %}
+{%   if (zone.masq): %}
+               meta nfproto ipv4 {%+ if (zone.masq4_src_pos): -%}
+                       ip saddr {{ fw4.set(zone.masq4_src_pos) }} {%+ endif -%}
+               {%+ if (zone.masq4_src_neg): -%}
+                       ip saddr != {{ fw4.set(zone.masq4_src_neg) }} {%+ endif -%}
+               {%+ if (zone.masq4_dest_pos): -%}
+                       ip daddr {{ fw4.set(zone.masq4_dest_pos) }} {%+ endif -%}
+               {%+ if (zone.masq4_dest_neg): -%}
+                       ip daddr != {{ fw4.set(zone.masq4_dest_neg) }} {%+ endif -%}
+               masquerade comment "!fw4: Masquerade IPv4 {{ zone.name }} traffic"
+{%   endif %}
+{%   if (zone.masq6): %}
+               meta nfproto ipv6 {%+ if (zone.masq6_src_pos): -%}
+                       ip6 saddr {{ fw4.set(zone.masq6_src_pos) }} {%+ endif -%}
+               {%+ if (zone.masq6_src_neg): -%}
+                       ip6 saddr != {{ fw4.set(zone.masq6_src_neg) }} {%+ endif -%}
+               {%+ if (zone.masq6_dest_pos): -%}
+                       ip6 daddr {{ fw4.set(zone.masq6_dest_pos) }} {%+ endif -%}
+               {%+ if (zone.masq6_dest_neg): -%}
+                       ip6 daddr != {{ fw4.set(zone.masq6_dest_neg) }} {%+ endif -%}
+               masquerade comment "!fw4: Masquerade IPv6 {{ zone.name }} traffic"
+{%   endif %}
+       }
+
+{%  endif %}
+{% endfor %}
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+
+{% for (let target in ["helper", "notrack"]): %}
+{%  for (let zone in fw4.zones()): %}
+{%   if (zone.dflags[target]): %}
+{%    for (let rule in zone.match_rules): %}
+{%     let devs = fw4.filter_loopback_devs(rule.devices_pos, false); %}
+{%     let nets = fw4.filter_loopback_addrs(rule.subnets_pos, false); %}
+{%     if (rule.devices_neg || rule.subnets_neg || length(devs) || length(nets)): %}
+               {%+ if (rule.family): -%}
+                       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+               {%+ if (length(devs)): -%}
+                       iifname {{ fw4.set(devs) }} {%+ endif -%}
+               {%+ if (rule.devices_neg): -%}
+                       iifname != {{ fw4.set(rule.devices_neg) }} {%+ endif -%}
+               {%+ if (length(nets)): -%}
+                       {{ fw4.ipproto(rule.family) }} saddr {{ fw4.set(nets) }} {%+ endif -%}
+               {%+ if (rule.subnets_neg): -%}
+                       {{ fw4.ipproto(rule.family) }} saddr != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+               jump {{ target }}_{{ zone.name }} comment "!fw4: {{ zone.name }} {{ fw4.nfproto(rule.family, true) }} {{
+                       (target == "helper") ? "CT helper assignment" : "CT bypass"
+               }}"
+{%     endif %}
+{%    endfor %}
+{%   endif %}
+{%  endfor %}
+{% endfor %}
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+
+{% for (let target in ["helper", "notrack"]): %}
+{%  for (let zone in fw4.zones()): %}
+{%   if (zone.dflags[target]): %}
+{%    for (let rule in zone.match_rules): %}
+{%     let devs = fw4.filter_loopback_devs(rule.devices_pos, true); %}
+{%     let nets = fw4.filter_loopback_addrs(rule.subnets_pos, true); %}
+{%     if (length(devs) || length(nets)): %}
+               {%+ if (rule.family): -%}
+                       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+               {%+ if (length(devs)): -%}
+                       iifname {{ fw4.set(devs) }} {%+ endif -%}
+               {%+ if (length(nets)): -%}
+                       {{ fw4.ipproto(rule.family) }} saddr {{ fw4.set(nets) }} {%+ endif -%}
+               jump {{ target }}_{{ zone.name }} comment "!fw4: {{ zone.name }} {{ fw4.nfproto(rule.family, true) }} {{
+                       (target == "helper") ? "CT helper assignment" : "CT bypass"
+               }}"
+{%     endif %}
+{%    endfor %}
+{%   endif %}
+{%  endfor %}
+{% endfor %}
+       }
+
+{% for (let helper in fw4.helpers()): %}
+{%  if (helper.available): %}
+{%   for (let proto in helper.proto): %}
+       ct helper {{ helper.name }} {
+               type {{ fw4.quote(helper.name, true) }} protocol {{ proto.name }};
+       }
+
+{%   endfor %}
+{%  endif %}
+{% endfor %}
+
+{% for (let target in ["helper", "notrack"]): %}
+{%  for (let zone in fw4.zones()): %}
+{%   if (zone.dflags[target]): %}
+       chain {{ target }}_{{ zone.name }} {
+{% for (let rule in fw4.rules(target+"_"+zone.name)): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+       }
+
+{%   endif %}
+{%  endfor %}
+{% endfor %}
+
+
+       #
+       # Mangle rules
+       #
+
+       chain mangle_prerouting {
+               type filter hook prerouting priority mangle; policy accept;
+
+{% for (let rule in fw4.rules("mangle_prerouting")): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+       }
+
+       chain mangle_output {
+               type filter hook output priority mangle; policy accept;
+
+{% for (let rule in fw4.rules("mangle_output")): %}
+               {%+ include("rule.uc", { fw4, rule }) %}
+{% endfor %}
+       }
+
+       chain mangle_forward {
+               type filter hook forward priority mangle; policy accept;
+
+{% for (let zone in fw4.zones()): %}
+{%  if (zone.mtu_fix): %}
+{%   for (let rule in zone.match_rules): %}
+               {%+ include("zone-mssfix.uc", { fw4, zone, rule, egress: false }) %}
+               {%+ include("zone-mssfix.uc", { fw4, zone, rule, egress: true }) %}
+{%   endfor %}
+{%  endif %}
+{% endfor %}
+       }
+}
diff --git a/root/usr/share/firewall4/templates/zone-match.uc b/root/usr/share/firewall4/templates/zone-match.uc
new file mode 100644 (file)
index 0000000..656c568
--- /dev/null
@@ -0,0 +1,20 @@
+{%+ if (rule.family): -%}
+       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+{%+ if (rule.devices_pos): -%}
+       {{ (direction in ["output", "srcnat"])
+               ? "oifname" : "iifname" }} {{ fw4.set(rule.devices_pos) }} {%+ endif -%}
+{%+ if (rule.devices_neg): -%}
+       {{ (direction in ["output", "srcnat"])
+               ? "oifname" : "iifname"
+       }} != {{ fw4.set(rule.devices_neg) }} {%+ endif -%}
+{%+ if (rule.subnets_pos): -%}
+       {{ fw4.ipproto(rule.family) }} {{
+               (direction in ["output", "srcnat"]) ? "daddr" : "saddr"
+       }} {{ fw4.set(rule.subnets_pos) }} {%+ endif -%}
+{%+ if (rule.subnets_neg): -%}
+       {{ fw4.ipproto(rule.family) }} {{
+               (direction in ["output", "srcnat"]) ? "daddr" : "saddr"
+       }} != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+jump {{ direction }}_{{ zone.name }} comment "!fw4: Handle {{ zone.name }} {{
+       fw4.nfproto(rule.family, true)
+}} {{ direction }} traffic"
diff --git a/root/usr/share/firewall4/templates/zone-mssfix.uc b/root/usr/share/firewall4/templates/zone-mssfix.uc
new file mode 100644 (file)
index 0000000..dd5766b
--- /dev/null
@@ -0,0 +1,15 @@
+{%+ if (rule.family): -%}
+       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+{%+ if (rule.devices_pos): -%}
+       {{ egress ? "oifname" : "iifname" }} {{ fw4.set(rule.devices_pos) }} {%+ endif -%}
+{%+ if (rule.devices_neg): -%}
+       {{ egress ? "oifname" : "iifname" }} != {{ fw4.set(rule.devices_neg) }} {%+ endif -%}
+{%+ if (rule.subnets_pos): -%}
+       {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} {{ fw4.set(rule.subnets_pos) }} {%+ endif -%}
+{%+ if (rule.subnets_neg): -%}
+       {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+tcp flags syn tcp option maxseg size set rt mtu {%+ if (zone.log & 2): -%}
+       log prefix "MSSFIX {{ zone.name }} out: " {%+ endif -%}
+comment "!fw4: Zone {{ zone.name }} {{
+       fw4.nfproto(rule.family, true)
+}} {{ egress ? "egress" : "ingress" }} MTU fixing"
diff --git a/root/usr/share/firewall4/templates/zone-notrack.uc b/root/usr/share/firewall4/templates/zone-notrack.uc
new file mode 100644 (file)
index 0000000..fe31eb2
--- /dev/null
@@ -0,0 +1,19 @@
+{%+
+       local devs = fw4.filter_loopback_devs(fw4.devices_pos, output),
+             nets = fw4.filter_loopback_addrs(fw4.subnets_pos, output);
+
+       if (!((output && (length(devs) || length(nets))) ||
+             (!output && (rule.devices_neg || rule.subnets_neg || length(devs) || length(nets)))))
+           return;
+-%}
+{%+ if (rule.family): -%}
+       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+{%+ if (length(devs)): -%}
+       iifname {{ fw4.set(devs) }} {%+ endif -%}
+{%+ if (rule.devices_neg): -%}
+       iifname != {{ fw4.set(rule.devices_neg) }} {%+ endif -%}
+{%+ if (length(nets)): -%}
+       {{ fw4.ipproto(rule.family) }} saddr {{ fw4.set(nets) }} {%+ endif -%}
+{%+ if (rule.subnets_neg): -%}
+       {{ fw4.ipproto(rule.family) }} saddr != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+jump notrack_{{ zone.name }} comment "!fw4: {{ zone.name }} {{ fw4.nfproto(rule.family, true) }} CT bypass"
diff --git a/root/usr/share/firewall4/templates/zone-verdict.uc b/root/usr/share/firewall4/templates/zone-verdict.uc
new file mode 100644 (file)
index 0000000..c8f5667
--- /dev/null
@@ -0,0 +1,20 @@
+{%+ if (rule.family): -%}
+       meta nfproto {{ fw4.nfproto(rule.family) }} {%+ endif -%}
+{%+ if (rule.devices_pos): -%}
+       {{ egress ? "oifname" : "iifname" }} {{ fw4.set(rule.devices_pos) }} {%+ endif -%}
+{%+ if (rule.devices_neg): -%}
+       {{ egress ? "oifname" : "iifname"
+       }} != {{ fw4.set(rule.devices_neg) }} {%+ endif -%}
+{%+ if (rule.subnets_pos): -%}
+       {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} {{ fw4.set(rule.subnets_pos) }} {%+ endif -%}
+{%+ if (rule.subnets_neg): -%}
+       {{ fw4.ipproto(rule.family) }} {{ egress ? "daddr" : "saddr" }} != {{ fw4.set(rule.subnets_neg) }} {%+ endif -%}
+{%+ if (zone.counter): -%}
+       counter {%+ endif -%}
+{%+ if (verdict != "accept" && (zone.log & 1)): -%}
+       log prefix "{{ verdict }} {{ zone.name }} {{ egress ? "out" : "in" }}: " {%+ endif -%}
+{% if (verdict == "reject"): -%}
+       jump handle_reject comment "!fw4: reject {{ zone.name }} {{ fw4.nfproto(rule.family, true) }} traffic"
+{% else -%}
+       {{ verdict }} comment "!fw4: {{ verdict }} {{ zone.name }} {{ fw4.nfproto(rule.family, true) }} traffic"
+{% endif -%}
diff --git a/root/usr/share/ucode/fw4.uc b/root/usr/share/ucode/fw4.uc
new file mode 100644 (file)
index 0000000..46dff9a
--- /dev/null
@@ -0,0 +1,2831 @@
+{%
+
+let STATEFILE = "/var/run/fw4.state";
+
+let PARSE_LIST   = 0x01;
+let FLATTEN_LIST = 0x02;
+let NO_INVERT    = 0x04;
+let UNSUPPORTED  = 0x08;
+let REQUIRED     = 0x10;
+
+let ipv4_icmptypes = {
+       "any": [ 0xFF, 0, 0xFF ],
+       "echo-reply": [ 0, 0, 0xFF ],
+       "pong": [ 0, 0, 0xFF ], /* Alias */
+
+       "destination-unreachable": [ 3, 0, 0xFF ],
+       "network-unreachable": [ 3, 0, 0 ],
+       "host-unreachable": [ 3, 1, 1 ],
+       "protocol-unreachable": [ 3, 2, 2 ],
+       "port-unreachable": [ 3, 3, 3 ],
+       "fragmentation-needed": [ 3, 4, 4 ],
+       "source-route-failed": [ 3, 5, 5 ],
+       "network-unknown": [ 3, 6, 6 ],
+       "host-unknown": [ 3, 7, 7 ],
+       "network-prohibited": [ 3, 9, 9 ],
+       "host-prohibited": [ 3, 10, 10 ],
+       "TOS-network-unreachable": [ 3, 11, 11 ],
+       "TOS-host-unreachable": [ 3, 12, 12 ],
+       "communication-prohibited": [ 3, 13, 13 ],
+       "host-precedence-violation": [ 3, 14, 14 ],
+       "precedence-cutoff": [ 3, 15, 15 ],
+
+       "source-quench": [ 4, 0, 0xFF ],
+
+       "redirect": [ 5, 0, 0xFF ],
+       "network-redirect": [ 5, 0, 0 ],
+       "host-redirect": [ 5, 1, 1 ],
+       "TOS-network-redirect": [ 5, 2, 2 ],
+       "TOS-host-redirect": [ 5, 3, 3 ],
+
+       "echo-request": [ 8, 0, 0xFF ],
+       "ping": [ 8, 0, 0xFF ], /* Alias */
+
+       "router-advertisement": [ 9, 0, 0xFF ],
+
+       "router-solicitation": [ 10, 0, 0xFF ],
+
+       "time-exceeded": [ 11, 0, 0xFF ],
+       "ttl-exceeded": [ 11, 0, 0xFF ], /* Alias */
+       "ttl-zero-during-transit": [ 11, 0, 0 ],
+       "ttl-zero-during-reassembly": [ 11, 1, 1 ],
+
+       "parameter-problem": [ 12, 0, 0xFF ],
+       "ip-header-bad": [ 12, 0, 0 ],
+       "required-option-missing": [ 12, 1, 1 ],
+
+       "timestamp-request": [ 13, 0, 0xFF ],
+
+       "timestamp-reply": [ 14, 0, 0xFF ],
+
+       "address-mask-request": [ 17, 0, 0xFF ],
+
+       "address-mask-reply": [ 18, 0, 0xFF ]
+};
+
+let ipv6_icmptypes = {
+       "destination-unreachable": [ 1, 0, 0xFF ],
+       "no-route": [ 1, 0, 0 ],
+       "communication-prohibited": [ 1, 1, 1 ],
+       "address-unreachable": [ 1, 3, 3 ],
+       "port-unreachable": [ 1, 4, 4 ],
+
+       "packet-too-big": [ 2, 0, 0xFF ],
+
+       "time-exceeded": [ 3, 0, 0xFF ],
+       "ttl-exceeded": [ 3, 0, 0xFF ], /* Alias */
+       "ttl-zero-during-transit": [ 3, 0, 0 ],
+       "ttl-zero-during-reassembly": [ 3, 1, 1 ],
+
+       "parameter-problem": [ 4, 0, 0xFF ],
+       "bad-header": [ 4, 0, 0 ],
+       "unknown-header-type": [ 4, 1, 1 ],
+       "unknown-option": [ 4, 2, 2 ],
+
+       "echo-request": [ 128, 0, 0xFF ],
+       "ping": [ 128, 0, 0xFF ], /* Alias */
+
+       "echo-reply": [ 129, 0, 0xFF ],
+       "pong": [ 129, 0, 0xFF ], /* Alias */
+
+       "router-solicitation": [ 133, 0, 0xFF ],
+
+       "router-advertisement": [ 134, 0, 0xFF ],
+
+       "neighbour-solicitation": [ 135, 0, 0xFF ],
+       "neighbor-solicitation": [ 135, 0, 0xFF ], /* Alias */
+
+       "neighbour-advertisement": [ 136, 0, 0xFF ],
+       "neighbor-advertisement": [ 136, 0, 0xFF ], /* Alias */
+
+       "redirect": [ 137, 0, 0xFF ]
+};
+
+let dscp_classes = {
+       "CS0": 0x00,
+       "CS1": 0x08,
+       "CS2": 0x10,
+       "CS3": 0x18,
+       "CS4": 0x20,
+       "CS5": 0x28,
+       "CS6": 0x30,
+       "CS7": 0x38,
+       "BE": 0x00,
+       "AF11": 0x0a,
+       "AF12": 0x0c,
+       "AF13": 0x0e,
+       "AF21": 0x12,
+       "AF22": 0x14,
+       "AF23": 0x16,
+       "AF31": 0x1a,
+       "AF32": 0x1c,
+       "AF33": 0x1e,
+       "AF41": 0x22,
+       "AF42": 0x24,
+       "AF43": 0x26,
+       "EF": 0x2e
+};
+
+/* cache used functions as upvalues */
+let _arrtoip = arrtoip;
+let _delete = delete;
+let _exists = exists;
+let _filter = filter;
+let _getenv = getenv;
+let _hex = hex;
+let _index = index;
+let _iptoarr = iptoarr;
+let _join = join;
+let _json = json;
+let _keys = keys;
+let _lc = lc;
+let _length = length;
+let _map = map;
+let _match = match;
+let _ord = ord;
+let _print = print;
+let _push = push;
+let _replace = replace;
+let _splice = splice;
+let _split = split;
+let _sprintf = sprintf;
+let _substr = substr;
+let _trim = trim;
+let _type = type;
+let _uc = uc;
+let _warn = warn;
+
+let _fs = fs;
+
+function to_mask(bits, v6) {
+       let m = [];
+
+       if (bits < 0 || 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;
+               bits -= b;
+       }
+
+       return _arrtoip(m);
+}
+
+function to_bits(mask) {
+       let a = _iptoarr(mask);
+
+       if (!a)
+               return null;
+
+       let bits = 0;
+
+       for (let i = 0, z = false; i < _length(a); i++) {
+               z = z || !a[i];
+
+               while (!z && (a[i] & 0x80)) {
+                       a[i] = (a[i] << 1) & 0xff;
+                       bits++;
+               }
+
+               if (a[i])
+                       return null;
+       }
+
+       return bits;
+}
+
+function apply_mask(addr, mask) {
+       let a = _iptoarr(addr);
+
+       if (!a)
+               return null;
+
+       if (_type(mask) == "int") {
+               for (let i = 0; i < _length(a); i++) {
+                       let b = (mask < 8) ? mask : 8;
+                       a[i] &= (0xff << (8 - b)) & 0xff;
+                       mask -= b;
+               }
+       }
+       else {
+               let m = _iptoarr(mask);
+
+               if (!m || _length(a) != _length(m))
+                       return null;
+
+               for (let i = 0; i < _length(a); i++)
+                       a[i] &= m[i];
+       }
+
+       return _arrtoip(a);
+}
+
+function to_array(x) {
+       if (_type(x) == "array")
+               return x;
+
+       if (x == null)
+               return [];
+
+       if (_type(x) == "object")
+               return [ x ];
+
+       x = _trim("" + x);
+
+       return (x == "") ? [] : _split(x, /[ \t]+/);
+}
+
+function filter_pos(x) {
+       let rv = _filter(x, e => !e.invert);
+       return _length(rv) ? rv : null;
+}
+
+function filter_neg(x) {
+       let rv = _filter(x, e => e.invert);
+       return _length(rv) ? rv : null;
+}
+
+function subnets_split_af(x) {
+       let rv = [];
+
+       for (let ag in to_array(x)) {
+               for (let a in _filter(ag.addrs, a => (a.family == 4))) {
+                       rv[0] = rv[0] || [];
+                       _push(rv[0], { ...a, invert: ag.invert });
+               }
+
+               for (let a in _filter(ag.addrs, a => (a.family == 6))) {
+                       rv[1] = rv[1] || [];
+                       _push(rv[1], { ...a, invert: ag.invert });
+               }
+       }
+
+       return rv;
+}
+
+function ensure_tcpudp(x) {
+       if (_length(_filter(x, p => (p.name == "tcp" || p.name == "udp"))))
+               return true;
+
+       let rest = _filter(x, p => !p.any),
+           any = _filter(x, p => p.any);
+
+       if (_length(any) && !_length(rest)) {
+               _splice(x, 0);
+               _push(x, { name: "tcp" }, { name: "udp" });
+               return true;
+       }
+
+       return false;
+}
+
+function is_family(x, v) { x.family == 0 || x.family == v }
+function family_is_ipv4(x) { x.family == 0 || x.family == 4 }
+function family_is_ipv6(x) { x.family == 0 || x.family == 6 }
+
+function infer_family(f, objects) {
+       let res = f;
+       let by = null;
+
+       for (let i = 0; i < _length(objects); i += 2) {
+               let objs = to_array(objects[i]),
+                   desc = objects[i + 1];
+
+               for (let obj in objs) {
+                       if (!obj || obj.family == 0 || obj.family == res)
+                               continue;
+
+                       if (res == 0) {
+                               res = obj.family;
+                               by = obj.desc;
+                               continue;
+                       }
+
+                       return by
+                               ? _sprintf('references IPv%d only %s but is restricted to IPv%d by %s', obj.family, desc, res, by)
+                               : _sprintf('is restricted to IPv%d but referenced %s is IPv%d only', res, desc, obj.family);
+               }
+       }
+
+       return res;
+}
+
+function map_setmatch(set, match, proto) {
+       if (!set || (('inet_service' in set.types) && proto != 'tcp' && proto != 'udp'))
+               return null;
+
+       let fields = [];
+
+       for (let i, t in set.types) {
+               let dir = (((match.dir && match.dir[i]) || set.directions[i] || 'src') == 'src' ? 's' : 'd');
+
+               switch (t) {
+               case 'ipv4_addr':
+                       fields[i] = _sprintf('ip %saddr', dir);
+                       break;
+
+               case 'ipv6_addr':
+                       fields[i] = _sprintf('ip6 %saddr', dir);
+                       break;
+
+               case 'ether_addr':
+                       if (dir != 's')
+                               return NaN;
+
+                       fields[i] = 'ether saddr';
+                       break;
+
+               case 'inet_service':
+                       fields[i] = _sprintf('%s %sport', proto, dir);
+                       break;
+               }
+       }
+
+       return fields;
+}
+
+
+return {
+       read_kernel_version: function() {
+               let fd = _fs.open("/proc/version", "r"),
+                   v = 0;
+
+               if (fd) {
+                   let m = _match(fd.read("line"), /^Linux version ([0-9]+)\.([0-9]+)\.([0-9]+)/);
+
+                   v = m ? (+m[1] << 24) | (+m[2] << 16) | (+m[3] << 8) : 0;
+                   fd.close();
+               }
+
+               return v;
+       },
+
+       read_state: function() {
+               let fd = _fs.open(STATEFILE, "r");
+               let state = null;
+
+               if (fd) {
+                       try {
+                               state = _json(fd.read("all"));
+                       }
+                       catch (e) {
+                               _warn(_sprintf("Unable to parse '%s': %s\n", STATEFILE, e));
+                       }
+
+                       fd.close();
+               }
+
+               return state;
+       },
+
+       read_ubus: function() {
+               let self = this,
+                   ifaces, services,
+                   rules = [], networks = {},
+                   bus = ubus.connect();
+
+               if (bus) {
+                       ifaces = bus.call("network.interface", "dump");
+                   services = bus.call("service", "get_data", { "type": "firewall" });
+
+                   bus.disconnect();
+               }
+               else {
+                       _warn(_sprintf("Unable to connect to ubus: %s\n", ubus.error()));
+               }
+
+
+               //
+               // Gather logical network information from ubus
+               //
+
+               if (_type(ifaces) == "object" && _type(ifaces.interface) == "array") {
+                       for (let ifc in ifaces.interface) {
+                               let net = {
+                                       up: ifc.up,
+                                       device: ifc.l3_device
+                               };
+
+                               if (_type(ifc["ipv4-address"]) == "array") {
+                                       for (let addr in ifc["ipv4-address"]) {
+                                               net.ipaddrs = net.ipaddrs || [];
+                                               _push(net.ipaddrs, {
+                                                       family: 4,
+                                                       addr: addr.address,
+                                                       mask: to_mask(addr.mask, false),
+                                                       bits: addr.mask
+                                               });
+                                       }
+                               }
+
+                               if (_type(ifc["ipv6-address"]) == "array") {
+                                       for (let addr in ifc["ipv6-address"]) {
+                                               net.ipaddrs = net.ipaddrs || [];
+                                               _push(net.ipaddrs, {
+                                                       family: 6,
+                                                       addr: addr.address,
+                                                       mask: to_mask(addr.mask, true),
+                                                       bits: addr.mask
+                                               });
+                                       }
+                               }
+
+                               if (_type(ifc["ipv6-prefix-assignment"]) == "array") {
+                                       for (let addr in ifc["ipv6-prefix-assignment"]) {
+                                               if (addr["local-address"]) {
+                                                       net.ipaddrs = net.ipaddrs || [];
+                                                       _push(net.ipaddrs, {
+                                                               family: 6,
+                                                               addr: addr["local-address"].address,
+                                                               mask: to_mask(addr["local-address"].mask, true),
+                                                               bits: addr["local-address"].mask
+                                                       });
+                                               }
+                                       }
+                               }
+
+                               if (_type(ifc.data) == "object" && _type(ifc.data.firewall) == "array") {
+                                       let n = 0;
+
+                                       for (let rulespec in ifc.data.firewall) {
+                                               _push(rules, {
+                                                       ...rulespec,
+
+                                                       name: (rulespec.type != 'ipset') ? _sprintf('ubus:%s[%s] %s %d', ifc.interface, ifc.proto, rulespec.type || 'rule', n) : rulespec.name,
+                                                       device: rulespec.device || ifc.l3_device
+                                               });
+
+                                               n++;
+                                       }
+                               }
+
+                               networks[ifc.interface] = net;
+                       }
+               }
+
+
+               //
+               // Gather firewall rule definitions from ubus services
+               //
+
+               if (_type(services) == "object") {
+                       for (let svcname, service in services) {
+                               if (_type(service) == "object" && _type(service.firewall) == "array") {
+                                       let n = 0;
+
+                                       for (let rulespec in services[svcname].firewall) {
+                                               _push(rules, {
+                                                       ...rulespec,
+
+                                                       name: (rulespec.type != 'ipset') ? _sprintf('ubus:%s %s %d', svcname, rulespec.type || 'rule', n) : rulespec.name
+                                               });
+
+                                               n++;
+                                       }
+                               }
+
+                               for (let svcinst, instance in service) {
+                                       if (_type(instance) == "object" && _type(instance.firewall) == "array") {
+                                               let n = 0;
+
+                                               for (let rulespec in instance.firewall) {
+                                                       _push(rules, {
+                                                               ...rulespec,
+
+                                                               name: (rulespec.type != 'ipset') ? _sprintf('ubus:%s[%s] %s %d', svcname, svcinst, rulespec.type || 'rule', n) : rulespec.name
+                                                       });
+
+                                                       n++;
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               return {
+                       networks: networks,
+                       ubus_rules: rules
+               };
+       },
+
+       load: function(use_statefile) {
+               let self = this;
+
+               this.state = use_statefile ? this.read_state() : null;
+
+               this.cursor = uci.cursor();
+               this.cursor.load("firewall");
+               this.cursor.load("/usr/share/firewall4/helpers");
+
+               if (!this.state)
+                       this.state = this.read_ubus();
+
+               this.kernel = this.read_kernel_version();
+
+
+               //
+               // Read helper mapping
+               //
+
+               this.cursor.foreach("helpers", "helper", h => self.parse_helper(h));
+
+
+               //
+               // Read default policies
+               //
+
+               this.cursor.foreach("firewall", "defaults", d => self.parse_defaults(d));
+
+
+               //
+               // Build list of ipsets
+               //
+
+               if (!this.state.ipsets) {
+                       _map(_filter(this.state.ubus_rules, n => (n.type == "ipset")), s => self.parse_ipset(s));
+                       this.cursor.foreach("firewall", "ipset", s => self.parse_ipset(s));
+               }
+
+
+               //
+               // Build list of logical zones
+               //
+
+               if (!this.state.zones)
+                       this.cursor.foreach("firewall", "zone", z => self.parse_zone(z));
+
+
+               //
+               // Build list of forwardings
+               //
+
+               this.cursor.foreach("firewall", "forwarding", f => self.parse_forwarding(f));
+
+
+               //
+               // Build list of rules
+               //
+
+               _map(_filter(this.state.ubus_rules, r => (r.type == "rule")), r => self.parse_rule(r));
+               this.cursor.foreach("firewall", "rule", r => self.parse_rule(r));
+
+
+               //
+               // Build list of redirects
+               //
+
+               _map(_filter(this.state.ubus_rules, r => (r.type == "redirect")), r => self.parse_redirect(r));
+               this.cursor.foreach("firewall", "redirect", r => self.parse_redirect(r));
+
+
+               //
+               // Build list of snats
+               //
+
+               _map(_filter(this.state.ubus_rules, n => (n.type == "nat")), n => self.parse_nat(n));
+               this.cursor.foreach("firewall", "nat", n => self.parse_nat(n));
+
+
+               if (use_statefile) {
+                       let fd = _fs.open(STATEFILE, "w");
+
+                       if (fd) {
+                               fd.write({
+                                       zones: this.state.zones,
+                                       ipsets: this.state.ipsets,
+                                       networks: this.state.networks,
+                                       ubus_rules: this.state.ubus_rules
+                               });
+
+                               fd.close();
+                       }
+                       else {
+                               _warn("Unable to write '%s': %s\n", STATEFILE, _fs.error());
+                       }
+               }
+       },
+
+       warn: function(fmt, ...args) {
+               if (_getenv("QUIET"))
+                       return;
+
+               let msg = _sprintf(fmt, ...args);
+
+               if (_getenv("TTY"))
+                       _warn("\033[33m", msg, "\033[m\n");
+               else
+                       _warn("[!] ", msg, "\n");
+       },
+
+       get: function(sid, opt) {
+               return this.cursor.get("firewall", sid, opt);
+       },
+
+       get_all: function(sid) {
+               return this.cursor.get_all("firewall", sid);
+       },
+
+       parse_options: function(s, spec) {
+               let rv = {};
+
+               for (let key, val in spec) {
+                       let datatype = "parse_" + val[0],
+                           defval = val[1],
+                           flags = val[2] || 0,
+                           parsefn = (flags & PARSE_LIST) ? "parse_list" : "parse_opt";
+
+                       let res = this[parsefn](s, key, datatype, defval, flags);
+
+                       if (res !== res)
+                               return false;
+
+                       if (_type(res) == "object" && res.invert && (flags & NO_INVERT)) {
+                               this.warn_section(s, "option '" + key + '" must not be negated');
+                               return false;
+                       }
+
+                       if (res != null) {
+                               if (flags & UNSUPPORTED)
+                                       this.warn_section(s, "option '" + key + "' is not supported by fw4");
+                               else
+                                       rv[key] = res;
+                       }
+               }
+
+               for (let opt in s) {
+                       if (_index(opt, '.') != 0 && opt != 'type' && !_exists(spec, opt)) {
+                               this.warn_section(s, "specifies unknown option '" + opt + "'");
+                               return false;
+                       }
+               }
+
+               return rv;
+       },
+
+       parse_subnet: function(subnet) {
+               let parts = _split(subnet, "/");
+               let a, b, m, n;
+
+               switch (_length(parts)) {
+               case 2:
+                       a = _iptoarr(parts[0]);
+                       m = _iptoarr(parts[1]);
+
+                       if (!a)
+                               return null;
+
+                       if (m) {
+                               if (_length(a) != _length(m))
+                                       return null;
+
+                               b = to_bits(parts[1]);
+
+                               if (b == null)
+                                       return null;
+
+                               m = _arrtoip(m);
+                       }
+                       else {
+                               b = +parts[1];
+
+                               if (_type(b) != "int")
+                                       return null;
+
+                               m = to_mask(b, _length(a) == 16);
+                       }
+
+                       return [{
+                               family: (_length(a) == 16) ? 6 : 4,
+                               addr: _arrtoip(a),
+                               mask: m,
+                               bits: b
+                       }];
+
+               case 1:
+                       parts = _split(parts[0], "-");
+
+                       switch (_length(parts)) {
+                       case 2:
+                               a = _iptoarr(parts[0]);
+                               b = _iptoarr(parts[1]);
+
+                               if (a && b && _length(a) == _length(b)) {
+                                       return [{
+                                               family: (_length(a) == 16) ? 6 : 4,
+                                               addr: _arrtoip(a),
+                                               addr2: _arrtoip(b),
+                                               range: true
+                                       }];
+                               }
+
+                               break;
+
+                       case 1:
+                               a = _iptoarr(parts[0]);
+
+                               if (a) {
+                                       return [{
+                                               family: (_length(a) == 16) ? 6 : 4,
+                                               addr: _arrtoip(a),
+                                               mask: to_mask(_length(a) * 8, _length(a) == 16),
+                                               bits: _length(a) * 8
+                                       }];
+                               }
+
+                               n = this.state.networks[parts[0]];
+
+                               if (n)
+                                       return [ ...(n.ipaddrs || []) ];
+                       }
+               }
+
+               return null;
+       },
+
+       parse_enum: function(val, choices) {
+               if (_type(val) == "string") {
+                       val = _lc(val);
+
+                       for (let i = 0; i < _length(choices); i++)
+                               if (_substr(choices[i], 0, _length(val)) == val)
+                                       return choices[i];
+               }
+
+               return null;
+       },
+
+       section_id: function(sid) {
+               let s = this.get_all(sid);
+
+               if (!s)
+                       return null;
+
+               if (s[".anonymous"]) {
+                       let c = 0;
+
+                       this.cursor.foreach("firewall", s[".type"], function(ss) {
+                               if (ss[".name"] == s[".name"])
+                                       return false;
+
+                               c++;
+                       });
+
+                       return _sprintf("@%s[%d]", s[".type"], c);
+               }
+
+               return s[".name"];
+       },
+
+       warn_section: function(s, msg) {
+               if (s[".name"]) {
+                       if (s.name)
+                               this.warn("Section %s (%s) %s", this.section_id(s[".name"]), s.name, msg);
+                       else
+                               this.warn("Section %s %s", this.section_id(s[".name"]), msg);
+               }
+               else {
+                       if (s.name)
+                               this.warn("ubus %s (%s) %s", s.type || "rule", s.name, msg);
+                       else
+                               this.warn("ubus %s %s", s.type || "rule", msg);
+               }
+       },
+
+       parse_policy: function(val) {
+               return this.parse_enum(val, [
+                       "accept",
+                       "reject",
+                       "drop"
+               ]);
+       },
+
+       parse_bool: function(val) {
+               if (val == "1" || val == "on" || val == "true" || val == "yes")
+                       return true;
+               else if (val == "0" || val == "off" || val == "false" || val == "no")
+                       return false;
+               else
+                       return null;
+       },
+
+       parse_family: function(val) {
+               if (val == 'any' || val == 'all' || val == '*')
+                       return 0;
+               else if (val == 'inet' || _index(val, '4') > -1)
+                       return 4;
+               else if (_index(val, '6') > -1)
+                       return 6;
+
+               return null;
+       },
+
+       parse_zone_ref: function(val) {
+               if (val == null)
+                       return null;
+
+               if (val == '*')
+                       return { any: true };
+
+               for (let zone in this.state.zones) {
+                       if (zone.name == val) {
+                               return {
+                                       any: false,
+                                       zone: zone
+                               };
+                       }
+               }
+
+               return null;
+       },
+
+       parse_device: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               if (rv.val == '*')
+                       rv.any = true;
+               else
+                       rv.device = rv.val;
+
+               return rv;
+       },
+
+       parse_direction: function(val) {
+               if (val == 'in' || val == 'ingress')
+                       return true;
+               else if (val == 'out' || val == 'egress')
+                       return false;
+
+               return null;
+       },
+
+       parse_setmatch: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               rv.val = _trim(_replace(rv.val, /^[^ \t]+/, function(m) {
+                       rv.name = m;
+                       return '';
+               }));
+
+               let dir = _split(rv.val, /[ \t,]/);
+
+               for (let i = 0; i < 3 && i < _length(dir); i++) {
+                       if (dir[i] == "dst" || dir[i] == "dest") {
+                               rv.dir = rv.dir || [];
+                               rv.dir[i] = "dst";
+                       }
+                       else if (dir[i] == "src") {
+                               rv.dir = rv.dir || [];
+                               rv.dir[i] = "src";
+                       }
+               }
+
+               return _length(rv.name) ? rv : null;
+       },
+
+       parse_cthelper: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               let helper = _filter(this.state.helpers, h => (h.name == rv.val))[0];
+
+               return helper ? { ...rv, ...helper } : null;
+       },
+
+       parse_protocol: function(val) {
+               let p = this.parse_invert(val);
+
+               if (!p)
+                       return null;
+
+               p.val = _lc(p.val);
+
+               switch (p.val) {
+               case 'all':
+               case 'any':
+               case '*':
+                       p.any = true;
+                       break;
+
+               case '1':
+               case 'icmp':
+                       p.name = 'icmp';
+                       break;
+
+               case '58':
+               case 'icmpv6':
+               case 'ipv6-icmp':
+                       p.name = 'ipv6-icmp';
+                       break;
+
+               case 'tcpudp':
+                       return [
+                               { invert: p.invert, name: 'tcp' },
+                               { invert: p.invert, name: 'udp' }
+                       ];
+
+               case '6':
+                       p.name = 'tcp';
+                       break;
+
+               case '17':
+                       p.name = 'udp';
+                       break;
+
+               default:
+                       p.name = p.val;
+               }
+
+               return (p.any || _length(p.name)) ? p : null;
+       },
+
+       parse_mac: function(val) {
+               let mac = this.parse_invert(val);
+               let m = mac ? _match(mac.val, /^([0-9a-f]{1,2})[:-]([0-9a-f]{1,2})[:-]([0-9a-f]{1,2})[:-]([0-9a-f]{1,2})[:-]([0-9a-f]{1,2})[:-]([0-9a-f]{1,2})$/i) : null;
+
+               if (!m)
+                       return null;
+
+               mac.mac = _sprintf('%02x:%02x:%02x:%02x:%02x:%02x',
+                                 _hex(m[1]), _hex(m[2]), _hex(m[3]),
+                                 _hex(m[4]), _hex(m[5]), _hex(m[6]));
+
+               return mac;
+       },
+
+       parse_port: function(val) {
+               let port = this.parse_invert(val);
+               let m = port ? _match(port.val, /^([0-9]{1,5})([-:]([0-9]{1,5}))?$/i) : null;
+
+               if (!m)
+                       return null;
+
+               if (m[3]) {
+                       let min_port = +m[1];
+                       let max_port = +m[3];
+
+                       if (min_port > max_port ||
+                           min_port < 0 || max_port < 0 ||
+                           min_port > 65535 || max_port > 65535)
+                               return null;
+
+                       port.min = min_port;
+                       port.max = max_port;
+               }
+               else {
+                       let pn = +m[1];
+
+                       if (pn != pn || pn < 0 || pn > 65535)
+                               return null;
+
+                       port.min = pn;
+                       port.max = pn;
+               }
+
+               return port;
+       },
+
+       parse_network: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               let nets = this.parse_subnet(rv.val);
+
+               if (nets === null)
+                       return false;
+
+               if (_length(nets))
+                       rv.addrs = [ ...nets ];
+
+               return rv;
+       },
+
+       parse_icmptype: function(val) {
+               let rv = {};
+
+               if (_exists(ipv4_icmptypes, val)) {
+                       rv.family = 4;
+
+                       rv.type = ipv4_icmptypes[val][0];
+                       rv.code_min = ipv4_icmptypes[val][1];
+                       rv.code_max = ipv4_icmptypes[val][2];
+               }
+
+               if (_exists(ipv6_icmptypes, val)) {
+                       rv.family = rv.family ? 0 : 6;
+
+                       rv.type6 = ipv6_icmptypes[val][0];
+                       rv.code6_min = ipv6_icmptypes[val][1];
+                       rv.code6_max = ipv6_icmptypes[val][2];
+               }
+
+               if (!_exists(rv, "family")) {
+                       let m = _match(val, /^([0-9]+)(\/([0-9]+))?$/);
+
+                       if (!m)
+                               return null;
+
+                       if (m[3]) {
+                               rv.type = +m[1];
+                               rv.code_min = +m[3];
+                               rv.code_max = rv.code_min;
+                       }
+                       else {
+                               rv.type = +m[1];
+                               rv.code_min = 0;
+                               rv.code_max = 0xFF;
+                       }
+
+                       if (rv.type > 0xFF || rv.code_min > 0xFF || rv.code_max > 0xFF)
+                               return null;
+
+                       rv.family = 0;
+
+                       rv.type6 = rv.type;
+                       rv.code6_min = rv.code_min;
+                       rv.code6_max = rv.code_max;
+               }
+
+               return rv;
+       },
+
+       parse_invert: function(val) {
+               if (val == null)
+                       return null;
+
+               let rv = { invert: false };
+
+               rv.val = _trim(_replace(val, /^[ \t]*!/, () => (rv.invert = true, '')));
+
+               return _length(rv.val) ? rv : null;
+       },
+
+       parse_limit: function(val) {
+               let rv = this.parse_invert(val);
+               let m = rv ? _match(rv.val, /^([0-9]+)(\/([a-z]+))?$/) : null;
+
+               if (!m)
+                       return null;
+
+               let n = +m[1];
+               let u = m[3] ? this.parse_enum(m[3], [ "second", "minute", "hour", "day" ]) : "second";
+
+               if (!u)
+                       return null;
+
+               rv.rate = n;
+               rv.unit = u;
+
+               return rv;
+       },
+
+       parse_int: function(val) {
+               let n = +val;
+
+               return (n == n) ? n : 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]);
+
+               d[3] = d[3] || 1;
+               d[5] = d[5] || 1;
+
+               if (d == null || d[1] < 1970 || d[1] > 2038 || d[3] < 1 || d[3] > 12 || d[5] < 1 || d[5] > 31)
+                       return null;
+
+               if (m[2] && !t)
+                       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
+               };
+       },
+
+       parse_time: function(val) {
+               let t = _match(val, /^([0-9]{1,2})(:([0-9]{1,2})(:([0-9]{1,2}))?)?$/);
+
+               if (t == null || t[1] > 23 || t[3] > 59 || t[5] > 59)
+                       return null;
+
+               return {
+                       hour: +t[1],
+                       min:  +t[3],
+                       sec:  +t[5]
+               };
+       },
+
+       parse_weekdays: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               for (let day in to_array(rv.val)) {
+                       day = this.parse_enum(day, [
+                               "monday",
+                               "tuesday",
+                               "wednesday",
+                               "thursday",
+                               "friday",
+                               "saturday",
+                               "sunday"
+                       ]);
+
+                       if (!day)
+                               return null;
+
+                       rv.days = rv.days || {};
+                       rv.days[day] = true;
+               }
+
+               rv.days = _keys(rv.days);
+
+               return rv.days ? rv : null;
+       },
+
+       parse_monthdays: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               for (let day in to_array(rv.val)) {
+                       day = +day;
+
+                       if (day < 1 || day > 31)
+                               return null;
+
+                       rv.days = rv.days || [];
+                       rv.days[day] = true;
+               }
+
+               return rv.days ? rv : null;
+       },
+
+       parse_mark: function(val) {
+               let rv = this.parse_invert(val);
+               let m = rv ? _match(rv.val, /^(0?x?[0-9a-f]+)(\/(0?x?[0-9a-f]+))?$/i) : null;
+
+               if (!m)
+                       return null;
+
+               let n = +m[1];
+
+               if (n != n || n > 0xFFFFFFFF)
+                       return null;
+
+               rv.mark = n;
+               rv.mask = 0xFFFFFFFF;
+
+               if (m[3]) {
+                       n = +m[3];
+
+                       if (n != n || n > 0xFFFFFFFF)
+                               return null;
+
+                       rv.mask = n;
+               }
+
+               return rv;
+       },
+
+       parse_dscp: function(val) {
+               let rv = this.parse_invert(val);
+
+               if (!rv)
+                       return null;
+
+               rv.val = _uc(rv.val);
+
+               if (_exists(dscp_classes, rv.val)) {
+                       rv.dscp = dscp_classes[rv.val];
+               }
+               else {
+                       let n = +val;
+
+                       if (n != n || n < 0 || n > 0x3F)
+                               return null;
+
+                       rv.dscp = n;
+               }
+
+               return rv;
+       },
+
+       parse_target: function(val) {
+               return this.parse_enum(val, [
+                       "accept",
+                       "reject",
+                       "drop",
+                       "notrack",
+                       "helper",
+                       "mark",
+                       "dscp",
+                       "dnat",
+                       "snat",
+                       "masquerade",
+                       "accept",
+                       "reject",
+                       "drop"
+               ]);
+       },
+
+       parse_reject_code: function(val) {
+               return this.parse_enum(val, [
+                       "tcp-reset",
+                       "port-unreachable",
+                       "admin-prohibited",
+                       "host-unreachable",
+                       "no-route"
+               ]);
+       },
+
+       parse_reflection_source: function(val) {
+               return this.parse_enum(val, [
+                       "internal",
+                       "external"
+               ]);
+       },
+
+       parse_ipsettype: function(val) {
+               let m = _match(val, /^(src|dst|dest)_(.+)$/);
+               let t = this.parse_enum(m ? m[2] : val, [
+                       "ip",
+                       "port",
+                       "mac",
+                       "net",
+                       "set"
+               ]);
+
+               return t ? [ (!m || m[1] == 'src') ? 'src' : 'dst', t ] : null;
+       },
+
+       parse_ipsetentry: function(val, set) {
+               let values = _split(val, /[ \t]+/);
+
+               if (_length(values) != _length(set.types))
+                       return null;
+
+               let rv = [];
+               let ip, mac, port;
+
+               for (let i, t in set.types) {
+                       switch (t) {
+                       case 'ipv4_addr':
+                               ip = _iptoarr(values[i]);
+
+                               if (_length(ip) != 4)
+                                       return null;
+
+                               rv[i] = _arrtoip(ip);
+                               break;
+
+                       case 'ipv6_addr':
+                               ip = _iptoarr(values[i]);
+
+                               if (_length(ip) != 16)
+                                       return null;
+
+                               rv[i] = _arrtoip(ip);
+                               break;
+
+                       case 'ether_addr':
+                               mac = this.parse_mac(values[i]);
+
+                               if (!mac || mac.invert)
+                                       return null;
+
+                               rv[i] = mac.mac;
+                               break;
+
+                       case 'inet_service':
+                               port = this.parse_port(values[i]);
+
+                               if (!port || port.invert || port.min != port.max)
+                                       return null;
+
+                               rv[i] = port.min;
+                               break;
+
+                       default:
+                               rv[i] = values[i];
+                       }
+               }
+
+               return _length(rv) ? rv : null;
+       },
+
+       parse_string: function(val) {
+               return "" + val;
+       },
+
+       parse_opt: function(s, opt, fn, defval, flags) {
+               let val = s[opt];
+
+               if (val == null) {
+                       if (flags & REQUIRED) {
+                               this.warn_section(s, "option '" + opt + "' is mandatory but not set");
+                               return NaN;
+                       }
+
+                       val = defval;
+               }
+
+               if (_type(val) == "array") {
+                       this.warn_section(s, "option '" + opt + "' must not be a list");
+                       return NaN;
+               }
+               else if (val == null) {
+                       return null;
+               }
+
+               let res = this[fn](val);
+
+               if (res === null) {
+                       this.warn_section(s, "option '" + opt + "' specifies invalid value '" + val + "'");
+                       return NaN;
+               }
+
+               return res;
+       },
+
+       parse_list: function(s, opt, fn, defval, flags) {
+               let val = s[opt];
+               let rv = [];
+
+               if (val == null) {
+                       if (flags & REQUIRED) {
+                               this.warn_section(s, "option '" + opt + "' is mandatory but not set");
+                               return NaN;
+                       }
+
+                       val = defval;
+               }
+
+               for (val in to_array(val)) {
+                       let res = this[fn](val);
+
+                       if (res === null) {
+                               this.warn_section(s, "option '" + opt + "' specifies invalid value '" + val + "'");
+                               return NaN;
+                       }
+
+                       if (flags & FLATTEN_LIST)
+                               _push(rv, ...to_array(res));
+                       else
+                               _push(rv, res);
+               }
+
+               return _length(rv) ? rv : null;
+       },
+
+       quote: function(s, force) {
+               if (force === true || !_match(s, /^([0-9A-Fa-f:.\/]+)( \. [0-9A-Fa-f:.\/]+)*$/))
+                       return _sprintf('"%s"', _replace(s + "", /(["\\])/g, '\\$1'));
+
+               return s;
+       },
+
+       cidr: function(a) {
+               if (a.range)
+                       return _sprintf("%s-%s", a.addr, a.addr2);
+
+               if ((a.family == 4 && a.bits == 32) ||
+                   (a.family == 6 && a.bits == 128))
+                   return a.addr;
+
+               return _sprintf("%s/%d", apply_mask(a.addr, a.bits), a.bits);
+       },
+
+       host: function(a) {
+               return a.range
+                       ? _sprintf("%s-%s", a.addr, a.addr2)
+                       : apply_mask(a.addr, a.bits);
+       },
+
+       port: function(p) {
+               if (p.min == p.max)
+                       return _sprintf('%d', p.min);
+
+               return _sprintf('%d-%d', p.min, p.max);
+       },
+
+       set: function(v, force) {
+               v = to_array(v);
+
+               if (force || _length(v) != 1)
+                       return _sprintf('{ %s }', _join(', ', _map(v, this.quote)));
+
+               return this.quote(v[0]);
+       },
+
+       concat: function(v) {
+               return _join(' . ', to_array(v));
+       },
+
+       ipproto: function(family) {
+               switch (family) {
+               case 4:
+                       return "ip";
+
+               case 6:
+                       return "ip6";
+               }
+       },
+
+       nfproto: function(family, human_readable) {
+               switch (family) {
+               case 4:
+                       return human_readable ? "IPv4" : "ipv4";
+
+               case 6:
+                       return human_readable ? "IPv6" : "ipv6";
+
+               default:
+                       return human_readable ? "IPv4/IPv6" : null;
+               }
+       },
+
+       datetime: function(stamp) {
+               return _sprintf('"%04d-%02d-%02d %02d:%02d:%02d"',
+                              stamp.year, stamp.month, stamp.day,
+                              stamp.hour, stamp.min, stamp.sec);
+       },
+
+       date: function(stamp) {
+               return _sprintf('"%04d-%02d-%02d"', stamp.year, stamp.month, stamp.day);
+       },
+
+       time: function(stamp) {
+               return _sprintf('"%02d:%02d:%02d"', stamp.hour, stamp.min, stamp.sec);
+       },
+
+       hex: function(n) {
+               return _sprintf('0x%x', n);
+       },
+
+       is_loopback_dev: function(dev) {
+               let fd = _fs.open(_sprintf("/sys/class/net/%s/flags", dev), "r");
+
+               if (!fd)
+                       return false;
+
+               let flags = +fd.read("line");
+
+               fd.close();
+
+               return !!(flags & 0x8);
+       },
+
+       is_loopback_addr: function(addr) {
+               return (_index(addr, "127.") == 0 || addr == "::1" || addr == "::1/128");
+       },
+
+       filter_loopback_devs: function(devs, invert) {
+               let self = this;
+               return _filter(devs, d => (self.is_loopback_dev(d) == invert));
+       },
+
+       filter_loopback_addrs: function(addrs, invert) {
+               let self = this;
+               return _filter(addrs, a => (self.is_loopback_addr(a) == invert));
+       },
+
+
+       input_policy: function(reject_as_drop) {
+               return (!reject_as_drop || this.state.defaults.input != 'reject') ? this.state.defaults.input : 'drop';
+       },
+
+       output_policy: function(reject_as_drop) {
+               return (!reject_as_drop || this.state.defaults.output != 'reject') ? this.state.defaults.output : 'drop';
+       },
+
+       forward_policy: function(reject_as_drop) {
+               return (!reject_as_drop || this.state.defaults.forward != 'reject') ? this.state.defaults.forward : 'drop';
+       },
+
+       default_option: function(flag) {
+               return this.state.defaults[flag];
+       },
+
+       helpers: function() {
+               return this.state.helpers;
+       },
+
+       zones: function() {
+               return this.state.zones;
+       },
+
+       rules: function(chain) {
+               return _filter(this.state.rules, r => (r.chain == chain));
+       },
+
+       redirects: function(chain) {
+               return _filter(this.state.redirects, r => (r.chain == chain));
+       },
+
+       ipsets: function() {
+               return this.state.ipsets;
+       },
+
+       parse_setfile: function(set, cb) {
+               let fd = _fs.open(set.loadfile, "r");
+
+               if (!fd) {
+                       _warn(_sprintf("Unable to load file '%s' for set '%s': %s\n",
+                                    set.loadfile, set.name, _fs.error()));
+                       return;
+               }
+
+               let line = null, count = 0;
+
+               while ((line = fd.read("line")) !== "") {
+                       line = _trim(line);
+
+                       if (_length(line) == 0 || _ord(line) == 35)
+                               continue;
+
+                       let v = this.parse_ipsetentry(line, set);
+
+                       if (!v) {
+                               this.warn("Skipping invalid entry '%s' in file '%s' for set '%s'",
+                                         line, set.loadfile, set.name);
+                               continue;
+                       }
+
+                       cb(v);
+
+                       count++;
+               }
+
+               fd.close();
+
+               return count;
+       },
+
+       print_setentries: function(set) {
+               let first = true;
+               let printer = (entry) => {
+                       if (first) {
+                               _print("\t\telements = {\n");
+                               first = false;
+                       }
+
+                       _print("\t\t\t", _join(" . ", entry), ",\n");
+               };
+
+               _map(set.entries, printer);
+
+               if (set.loadfile)
+                       this.parse_setfile(set, printer);
+
+               if (!first)
+                       _print("\t\t}\n");
+       },
+
+       parse_helper: function(data) {
+               let helper = this.parse_options(data, {
+                       name: [ "string", null, REQUIRED ],
+                       description: [ "string" ],
+                       module: [ "string" ],
+                       family: [ "family" ],
+                       proto: [ "protocol", null, PARSE_LIST | FLATTEN_LIST | NO_INVERT ],
+                       port: [ "port", null, NO_INVERT ]
+               });
+
+               if (helper === false) {
+                       this.warn("Helper definition '%s' skipped due to invalid options", data.name || data['.name']);
+                       return;
+               }
+               else if (helper.proto.any) {
+                       this.warn("Helper definition '%s' must not specify wildcard protocol", data.name || data['.name']);
+                       return;
+               }
+               else if (_length(helper.proto) > 1) {
+                       this.warn("Helper definition '%s' must not specify multiple protocols", data.name || data['.name']);
+                       return;
+               }
+
+               helper.available = ((_fs.stat("/sys/module/" + helper.module) || {}).type == "directory");
+
+               this.state.helpers = this.state.helpers || [];
+               _push(this.state.helpers, helper);
+       },
+
+       parse_defaults: function(data) {
+               if (this.state.defaults) {
+                       this.warn_section(data, ": ignoring duplicate defaults section");
+                       return;
+               }
+
+               let defs = this.parse_options(data, {
+                       input: [ "policy", "drop" ],
+                       output: [ "policy", "drop" ],
+                       forward: [ "policy", "drop" ],
+
+                       drop_invalid: [ "bool" ],
+                       tcp_reject_code: [ "reject_code", "tcp-reset" ],
+                       any_reject_code: [ "reject_code", "port-unreachable" ],
+
+                       syn_flood: [ "bool" ],
+                       synflood_protect: [ "bool" ],
+                       synflood_rate: [ "limit", "25/second" ],
+                       synflood_burst: [ "int", "50" ],
+
+                       tcp_syncookies: [ "bool", "1" ],
+                       tcp_ecn: [ "int" ],
+                       tcp_window_scaling: [ "bool", "1" ],
+
+                       accept_redirects: [ "bool" ],
+                       accept_source_route: [ "bool" ],
+
+                       auto_helper: [ "bool", "1" ],
+                       custom_chains: [ "bool", null, UNSUPPORTED ],
+                       disable_ipv6: [ "bool", null, UNSUPPORTED ],
+                       flow_offloading: [ "bool", null, UNSUPPORTED ],
+                       flow_offloading_hw: [ "bool", null, UNSUPPORTED ]
+               });
+
+               if (defs === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+
+               if (defs.synflood_protect === null)
+                       defs.synflood_protect = defs.syn_flood;
+
+               _delete(defs, "syn_flood");
+
+               this.state.defaults = defs;
+       },
+
+       parse_zone: function(data) {
+               let zone = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+
+                       name: [ "string", null, REQUIRED ],
+                       family: [ "family" ],
+
+                       network: [ "device", null, PARSE_LIST ],
+                       device: [ "device", null, PARSE_LIST ],
+                       subnet: [ "network", null, PARSE_LIST ],
+
+                       input: [ "policy", this.state.defaults ? this.state.defaults.input : "drop" ],
+                       output: [ "policy", this.state.defaults ? this.state.defaults.output : "drop" ],
+                       forward: [ "policy", this.state.defaults ? this.state.defaults.forward : "drop" ],
+
+                       masq: [ "bool" ],
+                       masq_allow_invalid: [ "bool" ],
+                       masq_src: [ "network", null, PARSE_LIST ],
+                       masq_dest: [ "network", null, PARSE_LIST ],
+
+                       extra: [ "string", null, UNSUPPORTED ],
+                       extra_src: [ "string", null, UNSUPPORTED ],
+                       extra_dest: [ "string", null, UNSUPPORTED ],
+
+                       mtu_fix: [ "bool" ],
+                       custom_chains: [ "bool", null, UNSUPPORTED ],
+
+                       log: [ "int" ],
+                       log_limit: [ "limit", null, UNSUPPORTED ],
+
+                       auto_helper: [ "bool", "1" ],
+                       helper: [ "cthelper", null, PARSE_LIST ],
+
+                       counter: [ "bool", "1" ]
+               });
+
+               if (zone === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!zone.enabled) {
+                       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;
+               }
+
+               if (zone.mtu_fix && this.kernel < 0x040a0000) {
+                       this.warn_section(data, "option 'mtu_fix' requires kernel 4.10 or later");
+                       return;
+               }
+
+               if (this.state.defaults && this.state.defaults.auto_helper === false)
+                       zone.auto_helper = false;
+
+               let match_devices = [];
+               let related_subnets = [];
+               let match_subnets, masq_src_subnets, masq_dest_subnets;
+
+               for (let e in to_array(zone.network)) {
+                       if (_exists(this.state.networks, e.device)) {
+                               let net = this.state.networks[e.device];
+
+                               if (net.device) {
+                                       _push(match_devices, {
+                                               invert: e.invert,
+                                               device: net.device
+                                       });
+                               }
+
+                               _push(related_subnets, ...(net.ipaddrs || []));
+                       }
+               }
+
+               _push(match_devices, ...to_array(zone.device));
+
+               match_subnets = subnets_split_af(zone.subnet);
+               masq_src_subnets = subnets_split_af(zone.masq_src);
+               masq_dest_subnets = subnets_split_af(zone.masq_dest);
+
+               _push(related_subnets, ...(match_subnets[0] || []), ...(match_subnets[1] || []));
+
+               let match_rules = [];
+
+               let add_rule = (family, devices, subnets, zone) => {
+                       let r = {};
+
+                       r.family = family;
+
+                       r.devices_pos = _map(filter_pos(devices), d => d.device);
+                       r.devices_neg = _map(filter_neg(devices), d => d.device);
+
+                       r.subnets_pos = _map(filter_pos(subnets), this.cidr);
+                       r.subnets_neg = _map(filter_neg(subnets), this.cidr);
+
+                       _push(match_rules, r);
+               };
+
+               let family = infer_family(zone.family, [
+                       zone.helper, "ct helper"
+               ]);
+
+               // check if there's no AF specific bits, in this case we can do AF agnostic matching
+               if (!family && _length(match_devices) && !_length(match_subnets[0]) && !_length(match_subnets[1])) {
+                       add_rule(0, match_devices, null, zone);
+               }
+
+               // we need to emit one or two AF specific rules
+               else {
+                       if (family_is_ipv4(zone) && (_length(match_devices) || _length(match_subnets[0])))
+                               add_rule(4, match_devices, match_subnets[0], zone);
+
+                       if (family_is_ipv6(zone) && (_length(match_devices) || _length(match_subnets[1])))
+                               add_rule(6, match_devices, match_subnets[1], zone);
+               }
+
+               zone.match_rules = match_rules;
+
+               if (masq_src_subnets[0]) {
+                       zone.masq4_src_pos = _map(filter_pos(masq_src_subnets[0]), this.cidr);
+                       zone.masq4_src_neg = _map(filter_neg(masq_src_subnets[0]), this.cidr);
+               }
+
+               if (masq_src_subnets[1]) {
+                       zone.masq6_src_pos = _map(filter_pos(masq_src_subnets[1]), this.cidr);
+                       zone.masq6_src_neg = _map(filter_neg(masq_src_subnets[1]), this.cidr);
+               }
+
+               if (masq_dest_subnets[0]) {
+                       zone.masq4_dest_pos = _map(filter_pos(masq_dest_subnets[0]), this.cidr);
+                       zone.masq4_dest_neg = _map(filter_neg(masq_dest_subnets[0]), this.cidr);
+               }
+
+               if (masq_dest_subnets[1]) {
+                       zone.masq6_dest_pos = _map(filter_pos(masq_dest_subnets[1]), this.cidr);
+                       zone.masq6_dest_neg = _map(filter_neg(masq_dest_subnets[1]), this.cidr);
+               }
+
+               zone.sflags = {};
+               zone.sflags[zone.input] = true;
+
+               zone.dflags = {};
+               zone.dflags[zone.output] = true;
+               zone.dflags[zone.forward] = true;
+
+               zone.match_devices = _map(_filter(match_devices, d => !d.invert), d => d.device);
+               zone.match_subnets = _map(_filter(related_subnets, s => !s.invert), this.cidr);
+
+               zone.related_subnets = related_subnets;
+
+               if (zone.masq || zone.masq6)
+                       zone.dflags.snat = true;
+
+               if ((zone.auto_helper && !(zone.masq || zone.masq6)) || _length(zone.helper)) {
+                       zone.dflags.helper = true;
+
+                       for (let helper in (_length(zone.helper) ? zone.helper : this.state.helpers)) {
+                               if (!helper.available)
+                                       continue;
+
+                               for (let proto in helper.proto) {
+                                       this.state.rules = this.state.rules || [];
+                                       _push(this.state.rules, {
+                                               chain: "helper_" + zone.name,
+                                               family: helper.family,
+                                               name: helper.description || helper.name,
+                                               proto: proto,
+                                               src: zone,
+                                               dports_pos: [ this.port(helper.port) ],
+                                               target: "helper",
+                                               set_helper: helper
+                                       });
+                               }
+                       }
+               }
+
+               this.state.zones = this.state.zones || [];
+               _push(this.state.zones, zone);
+       },
+
+       parse_forwarding: function(data) {
+               let fwd = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+
+                       name: [ "string" ],
+                       family: [ "family" ],
+
+                       src: [ "zone_ref", null, REQUIRED ],
+                       dest: [ "zone_ref", null, REQUIRED ]
+               });
+
+               if (fwd === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!fwd.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               let add_rule = (family, fwd) => {
+                       let f = {
+                               ...fwd,
+
+                               family: family,
+                               proto: { any: true }
+                       };
+
+                       f.name = fwd.name || _sprintf("Accept %s to %s forwarding",
+                               fwd.src.any ? "any" : fwd.src.zone.name,
+                               fwd.dest.any ? "any" : fwd.dest.zone.name);
+
+                       f.chain = fwd.src.any ? "forward" : _sprintf("forward_%s", fwd.src.zone.name);
+
+                       if (fwd.dest.any)
+                               f.target = "accept";
+                       else
+                               f.jump_chain = _sprintf("accept_to_%s", fwd.dest.zone.name);
+
+                       this.state.rules = this.state.rules || [];
+                       _push(this.state.rules, f);
+               };
+
+
+               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 != 0 && f2 != 0 && f1 != f2) {
+                               this.warn_section(data,
+                                       _sprintf("references src %s restricted to %s and dest restricted to %s, ignoring forwarding",
+                                               fwd.src.zone.name, this.nfproto(f1, true),
+                                               fwd.dest.zone.name, this.nfproto(f2, true)));
+
+                               return;
+                       }
+                       else if (f1) {
+                               this.warn_section(data,
+                                       _sprintf("inheriting %s restriction from src %s",
+                                               this.nfproto(f1, true), fwd.src.zone.name));
+
+                               family = f1;
+                       }
+                       else if (f2) {
+                               this.warn_section(data,
+                                       _sprintf("inheriting %s restriction from dest %s",
+                                               this.nfproto(f2, true), fwd.dest.zone.name));
+
+                               family = f2;
+                       }
+               }
+
+               add_rule(family, fwd);
+
+               if (fwd.dest.zone)
+                       fwd.dest.zone.dflags.accept = true;
+       },
+
+       parse_rule: function(data) {
+               let rule = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+
+                       name: [ "string", this.section_id(data[".name"]) ],
+                       family: [ "family" ],
+
+                       src: [ "zone_ref" ],
+                       dest: [ "zone_ref" ],
+
+                       device: [ "device" ],
+                       direction: [ "direction" ],
+
+                       ipset: [ "setmatch" ],
+                       helper: [ "cthelper" ],
+                       set_helper: [ "cthelper", null, NO_INVERT ],
+
+                       proto: [ "protocol", "tcpudp", PARSE_LIST | FLATTEN_LIST ],
+
+                       src_ip: [ "network", null, PARSE_LIST ],
+                       src_mac: [ "mac", null, PARSE_LIST ],
+                       src_port: [ "port", null, PARSE_LIST ],
+
+                       dest_ip: [ "network", null, PARSE_LIST ],
+                       dest_port: [ "port", null, PARSE_LIST ],
+
+                       icmp_type: [ "icmptype", null, PARSE_LIST ],
+                       extra: [ "string", null, UNSUPPORTED ],
+
+                       limit: [ "limit" ],
+                       limit_burst: [ "int" ],
+
+                       utc_time: [ "bool" ],
+                       start_date: [ "date" ],
+                       stop_date: [ "date" ],
+                       start_time: [ "time" ],
+                       stop_time: [ "time" ],
+                       weekdays: [ "weekdays" ],
+                       monthdays: [ "monthdays", null, UNSUPPORTED ],
+
+                       mark: [ "mark" ],
+                       set_mark: [ "mark", null, NO_INVERT ],
+                       set_xmark: [ "mark", null, NO_INVERT ],
+
+                       dscp: [ "dscp" ],
+                       set_dscp: [ "dscp", null, NO_INVERT ],
+
+                       counter: [ "bool", "1" ],
+
+                       target: [ "target" ]
+               });
+
+               if (rule === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!rule.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               if (rule.target in ["helper", "notrack"] && (!rule.src || !rule.src.zone)) {
+                       this.warn_section(data, "must specify a source zone for target '" + rule.target + "'");
+                       return;
+               }
+               else if (rule.target in ["dscp", "mark"] && rule.dest) {
+                       this.warn_section(data, "must not specify option 'dest' for target '" + rule.target + "'");
+                       return;
+               }
+               else if (rule.target == "dscp" && !rule.set_dscp) {
+                       this.warn_section(data, "must specify option 'set_dscp' for target 'dscp'");
+                       return;
+               }
+               else if (rule.target == "mark" && !rule.set_mark && !rule.set_xmark) {
+                       this.warn_section(data, "must specify option 'set_mark' or 'set_xmark' for target 'mark'");
+                       return;
+               }
+               else if (rule.target == "helper" && !rule.set_helper) {
+                       this.warn_section(data, "must specify option 'set_helper' for target 'helper'");
+                       return;
+               }
+
+               let ipset;
+
+               if (rule.ipset) {
+                       ipset = _filter(this.state.ipsets, s => (s.name == rule.ipset.name))[0];
+
+                       if (!ipset) {
+                               this.warn_section(data, "references unknown set '" + rule.ipset.name + "'");
+                               return;
+                       }
+
+                       if (('inet_service' in ipset.types) && !ensure_tcpudp(rule.proto)) {
+                               this.warn_section(data, "references named set with port match but no UDP/TCP protocol, ignoring section");
+                               return;
+                       }
+               }
+
+               let need_src_action_chain = (rule) => (rule.src && rule.src.zone && rule.src.zone.log && rule.target != "accept");
+
+               let add_rule = (family, proto, saddrs, daddrs, sports, dports, icmptypes, icmpcodes, ipset, rule) => {
+                       let r = {
+                               ...rule,
+
+                               family: family,
+                               proto: proto,
+                               has_addrs: !!(_length(saddrs) || _length(daddrs)),
+                               has_ports: !!(_length(sports) || _length(dports)),
+                               saddrs_pos: _map(filter_pos(saddrs), this.cidr),
+                               saddrs_neg: _map(filter_neg(saddrs), this.cidr),
+                               daddrs_pos: _map(filter_pos(daddrs), this.cidr),
+                               daddrs_neg: _map(filter_neg(daddrs), this.cidr),
+                               sports_pos: _map(filter_pos(sports), this.port),
+                               sports_neg: _map(filter_neg(sports), this.port),
+                               dports_pos: _map(filter_pos(dports), this.port),
+                               dports_neg: _map(filter_neg(dports), this.port),
+                               smacs_pos: _map(filter_pos(rule.src_mac), m => m.mac),
+                               smacs_neg: _map(filter_neg(rule.src_mac), m => m.mac),
+                               icmp_types: _map(icmptypes, i => (family == 4 ? i.type : i.type6)),
+                               icmp_codes: _map(icmpcodes, ic => _sprintf('%d . %d', (family == 4) ? ic.type : ic.type6, (family == 4) ? ic.code_min : ic.code6_min))
+                       };
+
+                       if (!_length(r.icmp_types))
+                               _delete(r, "icmp_types");
+
+                       if (!_length(r.icmp_codes))
+                               _delete(r, "icmp_codes");
+
+                       if (r.set_mark) {
+                               r.set_xmark = {
+                                       invert: r.set_mark.invert,
+                                       mark:   r.set_mark.mark,
+                                       mask:   r.set_mark.mark | r.set_mark.mask
+                               };
+
+                               _delete(r, "set_mark");
+                       }
+
+                       let set_types = map_setmatch(ipset, rule.ipset, proto.name);
+
+                       if (set_types !== set_types) {
+                               this.warn_section(data, "destination MAC address matching not supported");
+                               return;
+                       } else if (set_types) {
+                               r.ipset = { ...r.ipset, fields: set_types };
+                       }
+
+                       if (r.target == "notrack") {
+                               r.chain = _sprintf("notrack_%s", r.src.zone.name);
+                               r.src.zone.dflags.notrack = true;
+                       }
+                       else if (r.target == "helper") {
+                               r.chain = _sprintf("helper_%s", r.src.zone.name);
+                               r.src.zone.dflags.helper = true;
+                       }
+                       else if (r.target == "mark" || r.target == "dscp") {
+                               if (r.src) {
+                                       r.chain = "mangle_prerouting";
+                                       r.src.zone.dflags[r.target] = true;
+                               }
+                               else {
+                                       r.chain = "mangle_output";
+                               }
+                       }
+                       else {
+                               r.chain = "output";
+
+                               if (r.src) {
+                                       if (!r.src.any)
+                                               r.chain = _sprintf("%s_%s", r.dest ? "forward" : "input", r.src.zone.name);
+                                       else
+                                               r.chain = r.dest ? "forward" : "input";
+                               }
+
+                               if (r.dest && !r.src) {
+                                       if (!r.dest.any)
+                                               r.chain = _sprintf("output_%s", r.dest.zone.name);
+                                       else
+                                               r.chain = "output";
+                               }
+
+                               if (r.dest && !r.dest.any) {
+                                       r.jump_chain = _sprintf("%s_to_%s", r.target, r.dest.zone.name);
+                                       r.dest.zone.dflags[r.target] = true;
+                               }
+                               else if (need_src_action_chain(r)) {
+                                       r.jump_chain = _sprintf("%s_from_%s", r.target, r.src.zone.name);
+                                       r.src.zone.dflags[r.target] = true;
+                               }
+                               else if (r.target == "reject")
+                                       r.jump_chain = "handle_reject";
+                       }
+
+                       this.state.rules = this.state.rules || [];
+                       _push(this.state.rules, r);
+               };
+
+               for (let proto in rule.proto) {
+                       let sip, dip, sports, dports, itypes4, itypes6;
+                       let family = rule.family;
+
+                       switch (proto.name) {
+                       case "icmp":
+                               itypes4 = _filter(rule.icmp_type || [], family_is_ipv4);
+                               itypes6 = _filter(rule.icmp_type || [], family_is_ipv6);
+                               break;
+
+                       case "ipv6-icmp":
+                               family = 6;
+                               itypes6 = _filter(rule.icmp_type || [], family_is_ipv6);
+                               break;
+
+                       case "tcp":
+                       case "udp":
+                               sports = rule.src_port;
+                               dports = rule.dest_port;
+                               break;
+                       }
+
+                       family = infer_family(family, [
+                               ipset, "set match",
+                               rule.src, "source zone",
+                               rule.dest, "destination zone",
+                               rule.helper, "helper match",
+                               rule.set_helper, "helper to set"
+                       ]);
+
+                       if (_type(family) == "string") {
+                               this.warn_section(data, family + ", skipping");
+                               continue;
+                       }
+
+                       sip = subnets_split_af(rule.src_ip);
+                       dip = subnets_split_af(rule.dest_ip);
+
+                       let has_ipv4_specifics = (_length(sip[0]) || _length(dip[0]) || _length(itypes4));
+                       let has_ipv6_specifics = (_length(sip[1]) || _length(dip[1]) || _length(itypes6));
+
+                       /* if no family was configured, infer target family from IP addresses */
+                       if (family === null) {
+                               if (has_ipv4_specifics && !has_ipv6_specifics)
+                                       family = 4;
+                               else if (has_ipv6_specifics && !has_ipv4_specifics)
+                                       family = 6;
+                               else
+                                       family = 0;
+                       }
+
+                       /* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
+                       if (!family && rule.target != "dscp" && !has_ipv4_specifics && !has_ipv6_specifics) {
+                               add_rule(0, proto, null, null, sports, dports, null, null, null, rule);
+                       }
+
+                       /* we need to emit one or two AF specific rules */
+                       else {
+                               if (family == 0 || family == 4) {
+                                       let icmp_types = filter(itypes4, i => (i.code_min == 0 && i.code_max == 0xFF));
+                                       let icmp_codes = _filter(itypes4, i => (i.code_min != 0 || i.code_max != 0xFF));
+
+                                       if (_length(icmp_types) || (!_length(icmp_types) && !_length(icmp_codes)))
+                                               add_rule(4, proto, sip[0], dip[0], sports, dports, icmp_types, null, ipset, rule);
+
+                                       if (_length(icmp_codes))
+                                               add_rule(4, proto, sip[0], dip[0], sports, dports, null, icmp_codes, ipset, rule);
+                               }
+
+                               if (family == 0 || family == 6) {
+                                       let icmp_types = filter(itypes6, i => (i.code_min == 0 && i.code_max == 0xFF));
+                                       let icmp_codes = _filter(itypes6, i => (i.code_min != 0 || i.code_max != 0xFF));
+
+                                       if (_length(icmp_types) || (!_length(icmp_types) && !_length(icmp_codes)))
+                                               add_rule(6, proto, sip[1], dip[1], sports, dports, icmp_types, null, ipset, rule);
+
+                                       if (_length(icmp_codes))
+                                               add_rule(6, proto, sip[1], dip[1], sports, dports, null, icmp_codes, ipset, rule);
+                               }
+                       }
+               }
+       },
+
+       parse_redirect: function(data) {
+               let redir = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+
+                       name: [ "string", this.section_id(data[".name"]) ],
+                       family: [ "family", "4" ],
+
+                       src: [ "zone_ref" ],
+                       dest: [ "zone_ref" ],
+
+                       ipset: [ "setmatch" ],
+                       helper: [ "cthelper", null, NO_INVERT ],
+
+                       proto: [ "protocol", "tcpudp", PARSE_LIST | FLATTEN_LIST ],
+
+                       src_ip: [ "network" ],
+                       src_mac: [ "mac", null, PARSE_LIST ],
+                       src_port: [ "port" ],
+
+                       src_dip: [ "network" ],
+                       src_dport: [ "port" ],
+
+                       dest_ip: [ "network" ],
+                       dest_port: [ "port" ],
+
+                       extra: [ "string", null, UNSUPPORTED ],
+
+                       limit: [ "limit" ],
+                       limit_burst: [ "int" ],
+
+                       utc_time: [ "bool" ],
+                       start_date: [ "date" ],
+                       stop_date: [ "date" ],
+                       start_time: [ "time" ],
+                       stop_time: [ "time" ],
+                       weekdays: [ "weekdays" ],
+                       monthdays: [ "monthdays", null, UNSUPPORTED ],
+
+                       mark: [ "mark" ],
+
+                       reflection: [ "bool", "1" ],
+                       reflection_src: [ "reflection_source", "internal" ],
+
+                       reflection_zone: [ "zone_ref", null, PARSE_LIST ],
+
+                       counter: [ "bool", "1" ],
+
+                       target: [ "target", "dnat" ]
+               });
+
+               if (redir === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!redir.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               if (!(redir.target in ["dnat", "snat"])) {
+                       this.warn_section(data, "has invalid target specified, defaulting to dnat");
+                       redir.target = "dnat";
+               }
+
+               let ipset;
+
+               if (redir.ipset) {
+                       ipset = _filter(this.state.ipsets, s => (s.name == redir.ipset.name))[0];
+
+                       if (!ipset) {
+                               this.warn_section(data, "references unknown set '" + redir.ipset.name + "'");
+                               return;
+                       }
+
+                       if (('inet_service' in ipset.types) && !ensure_tcpudp(redir.proto)) {
+                               this.warn_section(data, "references named set with port match but no UDP/TCP protocol, ignoring section");
+                               return;
+                       }
+               }
+
+               let resolve_dest = (redir) => {
+                       for (let zone in this.state.zones) {
+                               for (let addr in zone.related_subnets) {
+                                       if (redir.dest_ip.family != addr.family)
+                                               continue;
+
+                                       let a = apply_mask(redir.dest_ip.addr, addr.bits);
+                                       let b = apply_mask(addr.addr, addr.bits);
+
+                                       if (a != b)
+                                               continue;
+
+                                       redir.dest = {
+                                               any: false,
+                                               zone: zone
+                                       };
+
+                                       return true;
+                               }
+                       }
+
+                       return false;
+               };
+
+               if (redir.target == "dnat") {
+                       if (!redir.src)
+                               return this.warn_section(r, "has no source specified");
+                       else if (redir.src.any)
+                               return this.warn_section(r, "must not have source '*' for dnat target");
+                       else if (redir.dest_ip && redir.dest_ip.invert)
+                               return this.warn_section(r, "must not specify a negated 'dest_ip' value");
+
+                       if (!redir.dest && redir.dest_ip && resolve_dest(redir))
+                               this.warn_section(r, "does not specify a destination, assuming '" + redir.dest.zone.name + "'");
+
+                       if (!redir.dest_port)
+                               redir.dest_port = redir.src_dport;
+
+                       if (redir.reflection && redir.dest && redir.dest.zone && redir.src.zone.masq) {
+                               redir.dest.zone.dflags.accept = true;
+                               redir.dest.zone.dflags.dnat = true;
+                               redir.dest.zone.dflags.snat = true;
+                       }
+
+                       if (redir.helper)
+                               redir.src.zone.dflags.helper = true;
+
+                       redir.src.zone.dflags[redir.target] = true;
+               }
+               else {
+                       if (!redir.dest)
+                               return this.warn_section(data, "has no destination specified");
+                       else if (redir.dest.any)
+                               return this.warn_section(data, "must not have destination '*' for snat target");
+                       else if (!redir.src_dip)
+                               return this.warn_section(data, "has no 'src_dip' option specified");
+                       else if (redir.src_dip.invert)
+                               return this.warn_section(data, "must not specify a negated 'src_dip' value");
+                       else if (redir.src_mac)
+                               return this.warn_section(data, "must not use 'src_mac' option for snat target");
+                       else if (redir.helper)
+                               return this.warn_section(data, "must not use 'helper' option for snat target");
+
+                       redir.dest.zone.dflags[redir.target] = true;
+               }
+
+
+               let add_rule = (family, proto, saddrs, daddrs, raddrs, sport, dport, rport, ipset, redir) => {
+                       let r = {
+                               ...redir,
+
+                               family: family,
+                               proto: proto,
+                               has_addrs: !!(_length(saddrs) || _length(daddrs)),
+                               has_ports: !!(sport || dport || rport),
+                               saddrs_pos: _map(filter_pos(saddrs), this.cidr),
+                               saddrs_neg: _map(filter_neg(saddrs), this.cidr),
+                               daddrs_pos: _map(filter_pos(daddrs), this.cidr),
+                               daddrs_neg: _map(filter_neg(daddrs), this.cidr),
+                               sports_pos: _map(filter_pos(to_array(sport)), this.port),
+                               sports_neg: _map(filter_neg(to_array(sport)), this.port),
+                               dports_pos: _map(filter_pos(to_array(dport)), this.port),
+                               dports_neg: _map(filter_neg(to_array(dport)), this.port),
+                               smacs_pos: _map(filter_pos(redir.src_mac), m => m.mac),
+                               smacs_neg: _map(filter_neg(redir.src_mac), m => m.mac),
+
+                               raddr: raddrs ? raddrs[0] : null,
+                               rport: rport
+                       };
+
+                       let set_types = map_setmatch(ipset, redir.ipset, proto.name);
+
+                       if (set_types !== set_types) {
+                               this.warn_section(data, "destination MAC address matching not supported");
+                               return;
+                       } else if (set_types) {
+                               r.ipset = { ...r.ipset, fields: set_types };
+                       }
+
+                       switch (r.target) {
+                       case "dnat":
+                               r.chain = _sprintf("dstnat_%s", r.src.zone.name);
+
+                               if (!r.raddr)
+                                       r.target = "redirect";
+
+                               break;
+
+                       case "snat":
+                               r.chain = _sprintf("srcnat_%s", r.dest.zone.name);
+                               break;
+                       }
+
+                       this.state.redirects = this.state.redirects || [];
+                       _push(this.state.redirects, r);
+               };
+
+               let to_hostaddr = (a) => {
+                       let bits = (a.family == 4) ? 32 : 128;
+
+                       return {
+                               family: a.family,
+                               addr: apply_mask(a.addr, bits),
+                               bits: bits
+                       };
+               };
+
+               for (let proto in redir.proto) {
+                       let sip, dip, rip, iip, eip, refip, sport, dport, rport;
+                       let family = redir.family;
+
+                       if (proto.name == "ipv6-icmp")
+                               family = 6;
+
+                       family = infer_family(family, [
+                               ipset, "set match",
+                               redir.src, "source zone",
+                               redir.dest, "destination zone",
+                               redir.helper, "helper match"
+                       ]);
+
+                       if (_type(family) == "string") {
+                               this.warn_section(data, family + ", skipping");
+                               continue;
+                       }
+
+                       switch (redir.target) {
+                       case "dnat":
+                               sip = subnets_split_af(redir.src_ip);
+                               dip = subnets_split_af(redir.src_dip);
+                               rip = subnets_split_af(redir.dest_ip);
+
+                               switch (proto.name) {
+                               case "tcp":
+                               case "udp":
+                                       sport = redir.src_port;
+                                       dport = redir.src_dport;
+                                       rport = redir.dest_port;
+                                       break;
+                               }
+
+                               /* build reflection rules */
+                               if (redir.reflection && (_length(rip[0]) || _length(rip[1])) &&
+                                   redir.src && redir.src.zone && redir.src.zone[family == 4 ? "masq" : "masq6"] &&
+                                   redir.dest && redir.dest.zone) {
+
+                                       let refredir = {
+                                               name: redir.name + " (reflection)",
+
+                                               helper: redir.helper,
+
+                                               // XXX: this likely makes no sense for reflection rules
+                                               //src_mac: redir.src_mac,
+
+                                               limit: redir.limit,
+                                               limit_burst: redir.limit_burst,
+
+                                               start_date: redir.start_date,
+                                               stop_date: redir.stop_date,
+                                               start_time: redir.start_time,
+                                               stop_time: redir.stop_time,
+                                               weekdays: redir.weekdays,
+
+                                               mark: redir.mark
+                                       };
+
+                                       let eaddrs = subnets_split_af(_length(dip) ? dip : { addrs: redir.src.zone.related_subnets });
+                                       let rzones = _length(redir.reflection_zone) ? redir.reflection_zone : [ redir.dest ];
+
+                                       for (let rzone in rzones) {
+                                               if (!is_family(rzone, family)) {
+                                                       this.warn_section(data,
+                                                               _sprintf("is restricted to IPv%d but referenced reflection zone is IPv%d only, skipping",
+                                                                       family, rzone.family));
+                                                       continue;
+                                               }
+
+                                               let iaddrs = subnets_split_af({ addrs: rzone.zone.related_subnets });
+                                               let refaddrs = (redir.reflection_src == "internal") ? iaddrs : eaddrs;
+
+                                               refaddrs = [
+                                                       _map(refaddrs[0], to_hostaddr),
+                                                       _map(refaddrs[1], to_hostaddr)
+                                               ];
+
+                                               eaddrs = [
+                                                       _map(eaddrs[0], to_hostaddr),
+                                                       _map(eaddrs[1], to_hostaddr)
+                                               ];
+
+                                               for (let i = 0; i <= 1; i++) {
+                                                       if (_length(rip[i])) {
+                                                               refredir.src = rzone;
+                                                               refredir.dest = null;
+                                                               refredir.target = "dnat";
+                                                               add_rule(i ? 6 : 4, proto, iaddrs[i], eaddrs[i], rip[i], sport, dport, rport, null, refredir);
+
+                                                               for (let refaddr in refaddrs[i]) {
+                                                                       refredir.src = null;
+                                                                       refredir.dest = rzone;
+                                                                       refredir.target = "snat";
+                                                                       add_rule(i ? 6 : 4, proto, iaddrs[i], rip[i], [ refaddr ], null, rport, null, null, refredir);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+
+
+                               break;
+
+                       case "snat":
+                               sip = subnets_split_af(redir.src_ip);
+                               dip = subnets_split_af(redir.dest_ip);
+                               rip = subnets_split_af(redir.src_dip);
+
+                               switch (proto.name) {
+                               case "tcp":
+                               case "udp":
+                                       sport = redir.src_port;
+                                       dport = redir.dest_port;
+                                       rport = redir.src_dport;
+                                       break;
+                               }
+
+                               break;
+                       }
+
+                       if (_length(rip[0]) > 1 || _length(rip[1]) > 1)
+                               this.warn_section(data, "specifies multiple rewrite addresses, using only first one");
+
+                       /* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
+                       if (!family && !_length(sip[0]) && !_length(sip[1]) && !_length(dip[0]) && !_length(dip[1]) && !_length(rip[0]) && !_length(rip[1])) {
+                               add_rule(0, proto, null, null, null, sport, dport, rport, null, redir);
+                       }
+
+                       /* we need to emit one or two AF specific rules */
+                       else {
+                               if (family == 0 || family == 4)
+                                       add_rule(4, proto, sip[0], dip[0], rip[0], sport, dport, rport, ipset, redir);
+
+                               if (family == 0 || family == 6)
+                                       add_rule(6, proto, sip[1], dip[1], rip[1], sport, dport, rport, ipset, redir);
+                       }
+               }
+       },
+
+       parse_nat: function(data) {
+               let snat = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+
+                       name: [ "string", this.section_id(data[".name"]) ],
+                       family: [ "family", "4" ],
+
+                       src: [ "zone_ref" ],
+                       device: [ "string" ],
+
+                       ipset: [ "setmatch", null, UNSUPPORTED ],
+
+                       proto: [ "protocol", "all", PARSE_LIST | FLATTEN_LIST ],
+
+                       src_ip: [ "network" ],
+                       src_port: [ "port" ],
+
+                       snat_ip: [ "network", null, NO_INVERT ],
+                       snat_port: [ "port", null, NO_INVERT ],
+
+                       dest_ip: [ "network" ],
+                       dest_port: [ "port" ],
+
+                       extra: [ "string", null, UNSUPPORTED ],
+
+                       limit: [ "limit" ],
+                       limit_burst: [ "int" ],
+
+                       connlimit_ports: [ "bool" ],
+
+                       utc_time: [ "bool" ],
+                       start_date: [ "date" ],
+                       stop_date: [ "date" ],
+                       start_time: [ "time" ],
+                       stop_time: [ "time" ],
+                       weekdays: [ "weekdays" ],
+                       monthdays: [ "monthdays", null, UNSUPPORTED ],
+
+                       mark: [ "mark" ],
+
+                       target: [ "target", "masquerade" ]
+               });
+
+               if (snat === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!snat.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               if (!(snat.target in ["accept", "snat", "masquerade"])) {
+                       this.warn_section(data, "has invalid target specified, defaulting to masquerade");
+                       snat.target = "masquerade";
+               }
+
+               if (snat.target == "snat" && !snat.snat_ip && !snat.snat_port) {
+                       this.warn_section(data, "needs either 'snat_ip' or 'snat_port' for target snat, ignoring section");
+                       return;
+               }
+               else if (snat.target != "snat" && snat.snat_ip) {
+                       this.warn_section(data, "must not use 'snat_ip' for non-snat target, ignoring section");
+                       return;
+               }
+               else if (snat.target != "snat" && snat.snat_port) {
+                       this.warn_section(data, "must not use 'snat_port' for non-snat target, ignoring section");
+                       return;
+               }
+
+               if ((snat.snat_port || snat.src_port || snat.dest_port) && !ensure_tcpudp(snat.proto)) {
+                       this.warn_section(data, "specifies ports but no UDP/TCP protocol, ignoring section");
+                       return;
+               }
+
+               if (snat.src && snat.src.zone)
+                       snat.src.zone.dflags.snat = true;
+
+               let add_rule = (family, proto, saddrs, daddrs, raddrs, sport, dport, rport, snat) => {
+                       let n = {
+                               ...snat,
+
+                               family: family,
+                               proto: proto,
+                               has_addrs: !!(_length(saddrs) || _length(daddrs) || _length(raddrs)),
+                               has_ports: !!(sport || dport),
+                               saddrs_pos: _map(filter_pos(saddrs), this.cidr),
+                               saddrs_neg: _map(filter_neg(saddrs), this.cidr),
+                               daddrs_pos: _map(filter_pos(daddrs), this.cidr),
+                               daddrs_neg: _map(filter_neg(daddrs), this.cidr),
+                               sports_pos: _map(filter_pos(to_array(sport)), this.port),
+                               sports_neg: _map(filter_neg(to_array(sport)), this.port),
+                               dports_pos: _map(filter_pos(to_array(dport)), this.port),
+                               dports_neg: _map(filter_neg(to_array(dport)), this.port),
+
+                               raddr: raddrs ? raddrs[0] : null,
+                               rport: rport,
+
+                               chain: (snat.src && snat.src.zone) ? _sprintf("srcnat_%s", snat.src.zone.name) : "srcnat"
+                       };
+
+                       this.state.redirects = this.state.redirects || [];
+                       _push(this.state.redirects, n);
+               };
+
+               for (let proto in snat.proto) {
+                       let sip, dip, rip, sport, dport, rport;
+                       let family = snat.family;
+
+                       sip = subnets_split_af(snat.src_ip);
+                       dip = subnets_split_af(snat.dest_ip);
+                       rip = subnets_split_af(snat.snat_ip);
+
+                       switch (proto.name) {
+                       case "tcp":
+                       case "udp":
+                               sport = snat.src_port;
+                               dport = snat.dest_port;
+                               rport = snat.snat_port;
+                               break;
+                       }
+
+                       if (_length(rip[0]) > 1 || _length(rip[1]) > 1)
+                               this.warn_section(data, "specifies multiple rewrite addresses, using only first one");
+
+                       /* inherit family restrictions from related zones */
+                       if (family === 0 || family === null) {
+                               let f = (rule.src && rule.src.zone) ? rule.src.zone.family : 0;
+
+                               if (f) {
+                                       this.warn_section(r,
+                                               _sprintf("inheriting %s restriction from src %s",
+                                                       this.nfproto(f1, true), rule.src.zone.name));
+
+                                       family = f;
+                               }
+                       }
+
+                       /* if no family was configured, infer target family from IP addresses */
+                       if (family === null) {
+                               if ((_length(sip[0]) || _length(dip[0]) || _length(rip[0])) && !_length(sip[1]) && !_length(dip[1]) && !_length(rip[1]))
+                                       family = 4;
+                               else if ((_length(sip[1]) || _length(dip[1]) || _length(rip[1])) && !_length(sip[0]) && !_length(dip[0]) && !_length(rip[0]))
+                                       family = 6;
+                               else
+                                       family = 0;
+                       }
+
+                       /* check if there's no AF specific bits, in this case we can do an AF agnostic rule */
+                       if (!family && !_length(sip[0]) && !_length(sip[1]) && !_length(dip[0]) && !_length(dip[1]) && !_length(rip[0]) && !_length(rip[1])) {
+                               add_rule(0, proto, null, null, null, sport, dport, rport, snat);
+                       }
+
+                       /* we need to emit one or two AF specific rules */
+                       else {
+                               if (family == 0 || family == 4)
+                                       add_rule(4, proto, sip[0], dip[0], rip[0], sport, dport, rport, snat);
+
+                               if (family == 0 || family == 6)
+                                       add_rule(6, proto, sip[1], dip[1], rip[1], sport, dport, rport, snat);
+                       }
+               }
+       },
+
+       parse_ipset: function(data) {
+               let ipset = this.parse_options(data, {
+                       enabled: [ "bool", "1" ],
+                       reload_set: [ "bool" ],
+                       counters: [ "bool" ],
+                       comment: [ "bool" ],
+
+                       name: [ "string", null, REQUIRED ],
+                       family: [ "family", "4" ],
+
+                       storage: [ "string", null, UNSUPPORTED ],
+                       match: [ "ipsettype", null, PARSE_LIST ],
+
+                       iprange: [ "string", null, UNSUPPORTED ],
+                       portrange: [ "string", null, UNSUPPORTED ],
+
+                       netmask: [ "int", null, UNSUPPORTED ],
+                       maxelem: [ "int" ],
+                       hashsize: [ "int", null, UNSUPPORTED ],
+                       timeout: [ "int", null, UNSUPPORTED ],
+
+                       external: [ "string", null, UNSUPPORTED ],
+
+                       entry: [ "string", null, PARSE_LIST ],
+                       loadfile: [ "string" ]
+               });
+
+               if (ipset === false) {
+                       this.warn_section(data, "skipped due to invalid options");
+                       return;
+               }
+               else if (!ipset.enabled) {
+                       this.warn_section(data, "is disabled, ignoring section");
+                       return;
+               }
+
+               if (ipset.family == 0) {
+                       this.warn_section(data, "must not specify family 'any'");
+                       return;
+               }
+               else if (!_length(ipset.match)) {
+                       this.warn_section(data, "has no datatypes assigned");
+                       return;
+               }
+
+               let dirs = _map(ipset.match, m => m[0]),
+                   types = _map(ipset.match, m => m[1]),
+                   interval = false;
+
+               if ("set" in types) {
+                       this.warn_section(data, "match type 'set' is not supported");
+                       return;
+               }
+
+               if ("net" in types) {
+                       if (this.kernel < 0x05060000) {
+                               this.warn_section(data, "match type 'net' requires kernel 5.6 or later");
+                               return;
+                       }
+
+                       interval = true;
+               }
+
+               let s = {
+                       ...ipset,
+
+                       types: _map(types, (t) => {
+                               switch (t) {
+                               case 'ip':
+                               case 'net':
+                                       return (ipset.family == 4) ? 'ipv4_addr' : 'ipv6_addr';
+
+                               case 'mac':
+                                       return 'ether_addr';
+
+                               case 'port':
+                                       return 'inet_service';
+                               }
+                       }),
+
+                       directions: dirs,
+                       interval: interval
+               };
+
+               let self = this;
+               s.entries = _filter(_map(ipset.entry, (e) => {
+                       let v = self.parse_ipsetentry(e, s);
+
+                       if (!v)
+                               self.warn_section(data, "ignoring invalid ipset entry '" + e + "'");
+
+                       return v;
+               }), (e) => (e != null));
+
+               this.state.ipsets = this.state.ipsets || [];
+               _push(this.state.ipsets, s);
+       }
+};
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755 (executable)
index 0000000..eb4dafa
--- /dev/null
@@ -0,0 +1,173 @@
+#!/usr/bin/env bash
+
+line='........................................'
+uenv='{ "REQUIRE_SEARCH_PATH": [ "/usr/local/lib/ucode/*.so", "/usr/lib/ucode/*.so", "./tests/*.uc", "./root/usr/share/ucode/*.uc" ] }'
+
+extract_sections() {
+       local file=$1
+       local dir=$2
+       local count=0
+       local tag line outfile
+
+       while IFS= read -r line; do
+               case "$line" in
+                       "-- Testcase --")
+                               tag="test"
+                               count=$((count + 1))
+                               outfile=$(printf "%s/%03d.in" "$dir" $count)
+                               printf "" > "$outfile"
+                       ;;
+                       "-- Environment --")
+                               tag="env"
+                               count=$((count + 1))
+                               outfile=$(printf "%s/%03d.env" "$dir" $count)
+                               printf "" > "$outfile"
+                       ;;
+                       "-- Expect stdout --"|"-- Expect stderr --"|"-- Expect exitcode --")
+                               tag="${line#-- Expect }"
+                               tag="${tag% --}"
+                               count=$((count + 1))
+                               outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
+                               printf "" > "$outfile"
+                       ;;
+                       "-- End --")
+                               tag=""
+                               outfile=""
+                       ;;
+                       *)
+                               if [ -n "$tag" ]; then
+                                       printf "%s\\n" "$line" >> "$outfile"
+                               fi
+                       ;;
+               esac
+       done < "$file"
+
+       return $(ls -l "$dir/"*.in 2>/dev/null | wc -l)
+}
+
+run_testcase() {
+       local num=$1
+       local dir=$2
+       local in=$3
+       local env=$4
+       local out=$5
+       local err=$6
+       local code=$7
+       local fail=0
+
+       ucode ${uenv:+-e "$uenv"} ${env:+-e "$(cat "$env")"} -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+
+       printf "%d\n" $? > "$dir/res.code"
+
+       touch "$dir/empty"
+
+       if ! cmp -s "$dir/res.err" "${err:-$dir/empty}"; then
+               [ $fail = 0 ] && printf "!\n"
+               printf "Testcase #%d: Expected stderr did not match:\n" $num
+               diff -u --color=always --label="Expected stderr" --label="Resulting stderr" "${err:-$dir/empty}" "$dir/res.err"
+               printf -- "---\n"
+               fail=1
+       fi
+
+       if ! cmp -s "$dir/res.out" "${out:-$dir/empty}"; then
+               [ $fail = 0 ] && printf "!\n"
+               printf "Testcase #%d: Expected stdout did not match:\n" $num
+               diff -u --color=always --label="Expected stdout" --label="Resulting stdout" "${out:-$dir/empty}" "$dir/res.out"
+               printf -- "---\n"
+               fail=1
+       fi
+
+       if [ -n "$code" ] && ! cmp -s "$dir/res.code" "$code"; then
+               [ $fail = 0 ] && printf "!\n"
+               printf "Testcase #%d: Expected exit code did not match:\n" $num
+               diff -u --color=always --label="Expected code" --label="Resulting code" "$code" "$dir/res.code"
+               printf -- "---\n"
+               fail=1
+       fi
+
+       return $fail
+}
+
+run_test() {
+       local file=$1
+       local name=${file##*/}
+       local res ecode eout eerr ein eenv tests
+       local testcase_first=0 failed=0 count=0
+
+       printf "%s %s " "$name" "${line:${#name}}"
+
+       mkdir "/tmp/test.$$"
+
+       extract_sections "$file" "/tmp/test.$$"
+       tests=$?
+
+       [ -f "/tmp/test.$$/001.in" ] && testcase_first=1
+
+       for res in "/tmp/test.$$/"[0-9]*; do
+               case "$res" in
+                       *.in)
+                               count=$((count + 1))
+
+                               if [ $testcase_first = 1 ]; then
+                                       # Flush previous test
+                                       if [ -n "$ein" ]; then
+                                               run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+                                               eout=""
+                                               eerr=""
+                                               ecode=""
+                                               eenv=""
+                                       fi
+
+                                       ein=$res
+                               else
+                                       run_testcase $count "/tmp/test.$$" "$res" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+
+                                       eout=""
+                                       eerr=""
+                                       ecode=""
+                                       eenv=""
+                               fi
+
+                       ;;
+                       *.env) eenv=$res ;;
+                       *.stdout) eout=$res ;;
+                       *.stderr) eerr=$res ;;
+                       *.exitcode) ecode=$res ;;
+               esac
+       done
+
+       # Flush last test
+       if [ $testcase_first = 1 ] && [ -n "$eout$eerr$ecode" ]; then
+               run_testcase $count "/tmp/test.$$" "$ein" "$eenv" "$eout" "$eerr" "$ecode" || failed=$((failed + 1))
+       fi
+
+       rm -r "/tmp/test.$$"
+
+       if [ $failed = 0 ]; then
+               printf "OK\n"
+       else
+               printf "%s %s FAILED (%d/%d)\n" "$name" "${line:${#name}}" $failed $tests
+       fi
+
+       return $failed
+}
+
+
+n_tests=0
+n_fails=0
+
+for catdir in tests/[0-9][0-9]_*; do
+       [ -d "$catdir" ] || continue
+
+       printf "\n##\n## Running %s tests\n##\n\n" "${catdir##*/[0-9][0-9]_}"
+
+       for testfile in "$catdir/"[0-9][0-9]_*; do
+               [ -f "$testfile" ] || continue
+
+               n_tests=$((n_tests + 1))
+               run_test "$testfile" || n_fails=$((n_fails + 1))
+       done
+done
+
+printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails
diff --git a/tests/01_configuration/01_ruleset b/tests/01_configuration/01_ruleset
new file mode 100644 (file)
index 0000000..5725ebf
--- /dev/null
@@ -0,0 +1,303 @@
+Testing the ruleset rendered from the default firewall configuration.
+
+-- Testcase --
+{%
+       include("./tests/mock.uc", {
+               TESTFILE: "test-wrapper.uc",
+               TRACE_CALLS: "stderr",
+
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+       define lan_devices = { "br-lan" }
+       define lan_subnets = { 192.168.26.0/24, fd63:e2f:f706::/60 }
+
+       define wan_devices = { "wan" }
+       define wan_subnets = { 10.11.12.0/24 }
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy accept;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+
+
+               tcp flags & (fin | syn | rst | ack) == syn jump syn_flood comment "!fw4: Rate limit TCP syn packets"
+
+
+               iifname "br-lan" jump input_lan comment "!fw4: Handle lan IPv4/IPv6 input traffic"
+               iifname "wan" jump input_wan comment "!fw4: Handle wan IPv4/IPv6 input traffic"
+
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+
+
+
+               iifname "br-lan" jump forward_lan comment "!fw4: Handle lan IPv4/IPv6 forward traffic"
+               iifname "wan" jump forward_wan comment "!fw4: Handle wan IPv4/IPv6 forward traffic"
+
+               jump handle_reject
+       }
+
+       chain output {
+               type filter hook output priority filter; policy accept;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+
+
+
+               oifname "br-lan" jump output_lan comment "!fw4: Handle lan IPv4/IPv6 output traffic"
+               oifname "wan" jump output_wan comment "!fw4: Handle wan IPv4/IPv6 output traffic"
+
+       }
+
+       chain handle_reject {
+               meta l4proto tcp reject with tcp reset comment "!fw4: Reject TCP traffic"
+               reject with icmpx type port-unreachable comment "!fw4: Reject any other traffic"
+       }
+
+       chain syn_flood {
+               tcp flags & (fin | syn | rst | ack) == syn limit rate 25/second burst 50 packets return comment "!fw4: Accept SYN packets below rate-limit"
+               drop comment "!fw4: Drop excess packets"
+       }
+
+
+       chain input_lan {
+               jump accept_from_lan
+       }
+
+       chain output_lan {
+               jump accept_to_lan
+       }
+
+       chain forward_lan {
+               jump accept_to_wan comment "!fw4: Accept lan to wan forwarding"
+               jump accept_to_lan
+       }
+
+       chain accept_from_lan {
+               iifname "br-lan" counter accept comment "!fw4: accept lan IPv4/IPv6 traffic"
+       }
+
+       chain accept_to_lan {
+               oifname "br-lan" counter accept comment "!fw4: accept lan IPv4/IPv6 traffic"
+       }
+
+       chain input_wan {
+               meta nfproto ipv4 udp dport 68 counter accept comment "!fw4: Allow-DHCP-Renew"
+               meta nfproto ipv4 meta l4proto icmp counter accept comment "!fw4: Allow-Ping"
+               meta nfproto ipv4 meta l4proto igmp counter accept comment "!fw4: Allow-IGMP"
+               ip6 saddr fc00::/6 ip6 daddr fc00::/6 udp dport 546 counter accept comment "!fw4: Allow-DHCPv6"
+               meta l4proto ipv6-icmp ip6 saddr fe80::/10 counter accept comment "!fw4: Allow-MLD"
+               meta nfproto ipv6 meta l4proto ipv6-icmp limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Input"
+               jump reject_from_wan
+       }
+
+       chain output_wan {
+               jump accept_to_wan
+       }
+
+       chain forward_wan {
+               meta nfproto ipv6 meta l4proto ipv6-icmp limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Forward"
+               meta l4proto esp counter jump accept_to_lan comment "!fw4: Allow-IPSec-ESP"
+               udp dport 500 counter jump accept_to_lan comment "!fw4: Allow-ISAKMP"
+               jump reject_to_wan
+       }
+
+       chain accept_to_wan {
+               oifname "wan" counter accept comment "!fw4: accept wan IPv4/IPv6 traffic"
+       }
+
+       chain reject_from_wan {
+               iifname "wan" counter jump handle_reject comment "!fw4: reject wan IPv4/IPv6 traffic"
+       }
+
+       chain reject_to_wan {
+               oifname "wan" counter jump handle_reject comment "!fw4: reject wan IPv4/IPv6 traffic"
+       }
+
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+
+               oifname "wan" jump srcnat_wan comment "!fw4: Handle wan IPv4/IPv6 srcnat traffic"
+       }
+
+       chain srcnat_wan {
+               meta nfproto ipv4 masquerade comment "!fw4: Masquerade IPv4 wan traffic"
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+
+               iifname "br-lan" jump helper_lan comment "!fw4: lan IPv4/IPv6 CT helper assignment"
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+
+       }
+
+       ct helper amanda {
+               type "amanda" protocol udp;
+       }
+
+       ct helper ftp {
+               type "ftp" protocol tcp;
+       }
+
+       ct helper RAS {
+               type "RAS" protocol udp;
+       }
+
+       ct helper Q.931 {
+               type "Q.931" protocol tcp;
+       }
+
+       ct helper irc {
+               type "irc" protocol tcp;
+       }
+
+       ct helper netbios-ns {
+               type "netbios-ns" protocol udp;
+       }
+
+       ct helper pptp {
+               type "pptp" protocol tcp;
+       }
+
+       ct helper sane {
+               type "sane" protocol tcp;
+       }
+
+       ct helper sip {
+               type "sip" protocol udp;
+       }
+
+       ct helper snmp {
+               type "snmp" protocol udp;
+       }
+
+       ct helper tftp {
+               type "tftp" protocol udp;
+       }
+
+       ct helper rtsp {
+               type "rtsp" protocol tcp;
+       }
+
+
+       chain helper_lan {
+               meta l4proto udp udp dport 10080 ct helper set "amanda" comment "!fw4: Amanda backup and archiving proto"
+               meta l4proto tcp tcp dport 21 ct helper set "ftp" comment "!fw4: FTP passive connection tracking"
+               meta l4proto udp udp dport 1719 ct helper set "RAS" comment "!fw4: RAS proto tracking"
+               meta l4proto tcp tcp dport 1720 ct helper set "Q.931" comment "!fw4: Q.931 proto tracking"
+               meta nfproto ipv4 meta l4proto tcp tcp dport 6667 ct helper set "irc" comment "!fw4: IRC DCC connection tracking"
+               meta nfproto ipv4 meta l4proto udp udp dport 137 ct helper set "netbios-ns" comment "!fw4: NetBIOS name service broadcast tracking"
+               meta nfproto ipv4 meta l4proto tcp tcp dport 1723 ct helper set "pptp" comment "!fw4: PPTP VPN connection tracking"
+               meta l4proto tcp tcp dport 6566 ct helper set "sane" comment "!fw4: SANE scanner connection tracking"
+               meta l4proto udp udp dport 5060 ct helper set "sip" comment "!fw4: SIP VoIP connection tracking"
+               meta nfproto ipv4 meta l4proto udp udp dport 161 ct helper set "snmp" comment "!fw4: SNMP monitoring connection tracking"
+               meta l4proto udp udp dport 69 ct helper set "tftp" comment "!fw4: TFTP connection tracking"
+               meta nfproto ipv4 meta l4proto tcp tcp dport 554 ct helper set "rtsp" comment "!fw4: RTSP connection tracking"
+       }
+
+
+
+       #
+       # Mangle rules
+       #
+
+       chain mangle_prerouting {
+               type filter hook prerouting priority mangle; policy accept;
+
+       }
+
+       chain mangle_output {
+               type filter hook output priority mangle; policy accept;
+
+       }
+
+       chain mangle_forward {
+               type filter hook forward priority mangle; policy accept;
+
+               iifname "wan" tcp flags syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 ingress MTU fixing"
+               oifname "wan" tcp flags syn tcp option maxseg size set rt mtu comment "!fw4: Zone wan IPv4/IPv6 egress MTU fixing"
+       }
+}
+-- End --
+
+-- Expect stderr --
+[call] ctx.call object <network.interface> method <dump> args <null>
+[call] ctx.call object <service> method <get_data> args <{ "type": "firewall" }>
+[call] fs.open path </proc/version> mode <r>
+[call] fs.stat path </sys/module/nf_conntrack_amanda>
+[call] fs.stat path </sys/module/nf_conntrack_ftp>
+[call] fs.stat path </sys/module/nf_conntrack_h323>
+[call] fs.stat path </sys/module/nf_conntrack_h323>
+[call] fs.stat path </sys/module/nf_conntrack_irc>
+[call] fs.stat path </sys/module/nf_conntrack_netbios_ns>
+[call] fs.stat path </sys/module/nf_conntrack_pptp>
+[call] fs.stat path </sys/module/nf_conntrack_sane>
+[call] fs.stat path </sys/module/nf_conntrack_sip>
+[call] fs.stat path </sys/module/nf_conntrack_snmp>
+[call] fs.stat path </sys/module/nf_conntrack_tftp>
+[call] fs.stat path </sys/module/nf_conntrack_rtsp>
+[call] fs.open path </sys/class/net/br-lan/flags> mode <r>
+[call] fs.open path </sys/class/net/br-lan/flags> mode <r>
+-- End --
diff --git a/tests/mock.uc b/tests/mock.uc
new file mode 100644 (file)
index 0000000..7a5da39
--- /dev/null
@@ -0,0 +1,479 @@
+{%
+       let _fs = require("fs");
+
+       let _log = (level, fmt, ...args) => {
+               let color, prefix;
+
+               switch (level) {
+               case 'info':
+                       color = 34;
+                       prefix = '!';
+                       break;
+
+               case 'warn':
+                       color = 33;
+                       prefix = 'W';
+                       break;
+
+               case 'error':
+                       color = 31;
+                       prefix = 'E';
+                       break;
+
+               default:
+                       color = 0;
+                       prefix = 'I';
+               }
+
+               let f = sprintf("\u001b[%d;1m[%s] %s\u001b[0m", color, prefix, fmt);
+               warn(replace(sprintf(f, ...args), "\n", "\n    "), "\n");
+       };
+
+       let I = (...args) => _log('info', ...args);
+       let N = (...args) => _log('notice', ...args);
+       let W = (...args) => _log('warn', ...args);
+       let E = (...args) => _log('error', ...args);
+
+       let read_json_file = (path) => {
+               let fd = _fs.open(path, "r");
+               if (fd) {
+                       let data = fd.read("all");
+                       fd.close();
+
+                       try {
+                               return json(data);
+                       }
+                       catch (e) {
+                               E("Unable to parse JSON data in %s: %s", path, e);
+
+                               return NaN;
+                       }
+               }
+
+               return null;
+       };
+
+       let format_json = (data) => {
+               let rv;
+
+               let format_value = (value) => {
+                       switch (type(value)) {
+                       case "object":
+                               return sprintf("{ /* %d keys */ }", length(value));
+
+                       case "array":
+                               return sprintf("[ /* %d items */ ]", length(value));
+
+                       case "string":
+                               if (length(value) > 64)
+                                       value = substr(value, 0, 64) + "...";
+
+                               /* fall through */
+                               return sprintf("%J", value);
+
+                       default:
+                               return sprintf("%J", value);
+                       }
+               };
+
+               switch (type(data)) {
+               case "object":
+                       rv = "{";
+
+                       let k = sort(keys(data));
+
+                       for (let i, n in k)
+                               rv += sprintf("%s %J: %s", i ? "," : "", n, format_value(data[n]));
+
+                       rv += " }";
+                       break;
+
+               case "array":
+                       rv = "[";
+
+                       for (let i, v in data)
+                               rv += (i ? "," : "") + " " + format_value(v);
+
+                       rv += " ]";
+                       break;
+
+               default:
+                       rv = format_value(data);
+               }
+
+               return rv;
+       };
+
+       let trace_call = (ns, func, args) => {
+               let msg = "[call] " +
+                       (ns ? ns + "." : "") +
+                       func;
+
+               for (let k, v in args) {
+                       msg += ' ' + k + ' <';
+
+                       switch (type(v)) {
+                       case "array":
+                       case "object":
+                               msg += format_json(v);
+                               break;
+
+                       default:
+                               msg += v;
+                       }
+
+                       msg += '>';
+               }
+
+               switch (TRACE_CALLS) {
+               case '1':
+               case 'stdout':
+                       print(msg + "\n");
+                       break;
+
+               case 'stderr':
+                       warn(msg + "\n");
+                       break;
+               }
+       };
+
+
+       /* Setup mock environment */
+       let mocks = {
+
+               /* Mock ubus module */
+               ubus: {
+                       connect: function() {
+                               let self = this;
+
+                               return {
+                                       call: (object, method, args) => {
+                                               let signature = [ object + "~" + method ];
+
+                                               if (type(args) == "object") {
+                                                       for (let i, k in sort(keys(args))) {
+                                                               switch (type(args[k])) {
+                                                               case "string":
+                                                               case "double":
+                                                               case "bool":
+                                                               case "int":
+                                                                       push(signature, k + "-" + replace(args[k], /[^A-Za-z0-9_-]+/g, "_"));
+                                                                       break;
+
+                                                               default:
+                                                                       push(signature, type(args[k]));
+                                                               }
+                                                       }
+                                               }
+
+                                               let candidates = [];
+
+                                               for (let i = length(signature); i > 0; i--) {
+                                                       let path = sprintf("./tests/mocks/ubus/%s.json", join("~", signature)),
+                                                           mock = read_json_file(path);
+
+                                                       if (mock != mock) {
+                                                               self._error = "Invalid argument";
+
+                                                               return null;
+                                                       }
+                                                       else if (mock) {
+                                                               trace_call("ctx", "call", { object, method, args });
+
+                                                               return mock;
+                                                       }
+
+                                                       push(candidates, path);
+                                                       pop(signature);
+                                               }
+
+                                               I("No response fixture defined for ubus call %s/%s with arguments %s.", object, method, args);
+                                               I("Provide a mock response through one of the following JSON files:\n%s\n", join("\n", candidates));
+
+                                               self._error = "Method not found";
+
+                                               return null;
+                                       },
+
+                                       disconnect: () => null,
+
+                                       error: () => self.error()
+                               };
+                       },
+
+                       error: function() {
+                               let e = this._error;
+                               delete(this._error);
+
+                               return e;
+                       }
+               },
+
+
+               /* Mock uci module */
+               uci: {
+                       cursor: () => ({
+                               _configs: {},
+
+                               load: function(file) {
+                                       let basename = replace(file, /^.+\//, ''),
+                                           path = sprintf("./tests/mocks/uci/%s.json", basename),
+                                           mock = read_json_file(path);
+
+                                       if (!mock || mock != mock) {
+                                               I("No configuration fixture defined for uci package %s.", file);
+                                               I("Provide a mock configuration through the following JSON file:\n%s\n", path);
+
+                                               return null;
+                                       }
+
+                                       this._configs[basename] = mock;
+                               },
+
+                               _get_section: function(config, section) {
+                                       if (!exists(this._configs, config)) {
+                                               this.load(config);
+
+                                               if (!exists(this._configs, config))
+                                                       return null;
+                                       }
+
+                                       let extended = match(section, "^@([A-Za-z0-9_-]+)\[(-?[0-9]+)\]$");
+
+                                       if (extended) {
+                                               let stype = extended[1],
+                                                   sindex = +extended[2],
+                                                   sections = [];
+
+                                               for (let sid, sobj in this._configs[config])
+                                                       if (sobj[".type"] == stype)
+                                                               push(sections, sobj);
+
+                                               sort(sections, (a, b) => (a[".index"] || 999) - (b[".index"] || 999));
+
+                                               if (sindex < 0)
+                                                       sindex = sections.length + sindex;
+
+                                               return sections[sindex];
+                                       }
+
+                                       return this._configs[config][section];
+                               },
+
+                               get: function(config, section, option) {
+                                       let sobj = this._get_section(config, section);
+
+                                       if (option && index(option, ".") == 0)
+                                               return null;
+                                       else if (sobj && option)
+                                               return sobj[option];
+                                       else if (sobj)
+                                               return sobj[".type"];
+                               },
+
+                               get_all: function(config, section) {
+                                       return section ? this._get_section(config, section) : this._configs[config];
+                               },
+
+                               foreach: function(config, stype, cb) {
+                                       let rv = false;
+
+                                       if (exists(this._configs, config)) {
+                                               let i = 0;
+
+                                               for (let sid, sobj in this._configs[config]) {
+                                                       i++;
+
+                                                       if (stype == null || sobj[".type"] == stype) {
+                                                               cb({ ".index": i - 1, ".type": stype, ".name": sid, ...sobj });
+                                                               rv = true;
+                                                       }
+                                               }
+                                       }
+
+                                       return rv;
+                               }
+                       })
+               },
+
+
+               /* Mock fs module */
+               fs: {
+                       readlink: function(path) {
+                               trace_call("fs", "readlink", { path });
+
+                               return path + "-link";
+                       },
+
+                       stat: function(path) {
+                               let file = sprintf("./tests/mocks/fs/stat~%s.json", replace(path, /[^A-Za-z0-9_-]+/g, '_')),
+                                   mock = read_json_file(file);
+
+                               if (!mock || mock != mock) {
+                                       I("No stat result fixture defined for fs.stat() call on %s.", path);
+                                       I("Provide a mock result through the following JSON file:\n%s\n", file);
+
+                                       if (match(path, /\/$/))
+                                               mock = { type: "directory" };
+                                       else
+                                               mock = { type: "file" };
+                               }
+
+                               trace_call("fs", "stat", { path });
+
+                               return mock;
+                       },
+
+                       unlink: function(path) {
+                               trace_call("fs", "unlink", { path });
+
+                               return true;
+                       },
+
+                       popen: (cmdline, mode) => {
+                               let read = (!mode || index(mode, "r") != -1),
+                                   path = sprintf("./tests/mocks/fs/popen~%s.txt", replace(cmdline, /[^A-Za-z0-9_-]+/g, '_')),
+                                   fd = read ? _fs.open(path, "r") : null,
+                                   mock = null;
+
+                               if (fd) {
+                                   mock = fd.read("all");
+                                   fd.close();
+                               }
+
+                               if (read && !mock) {
+                                       I("No stdout fixture defined for fs.popen() command %s.", cmdline);
+                                       I("Provide a mock output through the following text file:\n%s\n", path);
+
+                                       return null;
+                               }
+
+                               trace_call("fs", "popen", { cmdline, mode });
+
+                               return {
+                                       read: function(amount) {
+                                               let rv;
+
+                                               switch (amount) {
+                                               case "all":
+                                                       rv = mock;
+                                                       mock = "";
+                                                       break;
+
+                                               case "line":
+                                                       let i = index(mock, "\n");
+                                                       i = (i > -1) ? i + 1 : mock.length;
+                                                       rv = substr(mock, 0, i);
+                                                       mock = substr(mock, i);
+                                                       break;
+
+                                               default:
+                                                       let n = +amount;
+                                                       n = (n > 0) ? n : 0;
+                                                       rv = substr(mock, 0, n);
+                                                       mock = substr(mock, n);
+                                                       break;
+                                               }
+
+                                               return rv;
+                                       },
+
+                                       write: function() {},
+                                       close: function() {},
+
+                                       error: function() {
+                                               return null;
+                                       }
+                               };
+                       },
+
+                       open: (fpath, mode) => {
+                               let read = (!mode || index(mode, "r") != -1 || index(mode, "+") != -1),
+                                   path = sprintf("./tests/mocks/fs/open~%s.txt", replace(fpath, /[^A-Za-z0-9_-]+/g, '_')),
+                                   fd = read ? _fs.open(path, "r") : null,
+                                   mock = null;
+
+                               if (fd) {
+                                   mock = fd.read("all");
+                                   fd.close();
+                               }
+
+                               if (read && !mock) {
+                                       I("No stdout fixture defined for fs.open() path %s.", fpath);
+                                       I("Provide a mock output through the following text file:\n%s\n", path);
+
+                                       return null;
+                               }
+
+                               trace_call("fs", "open", { path: fpath, mode });
+
+                               return {
+                                       read: function(amount) {
+                                               let rv;
+
+                                               switch (amount) {
+                                               case "all":
+                                                       rv = mock;
+                                                       mock = "";
+                                                       break;
+
+                                               case "line":
+                                                       let i = index(mock, "\n");
+                                                       i = (i > -1) ? i + 1 : mock.length;
+                                                       rv = substr(mock, 0, i);
+                                                       mock = substr(mock, i);
+                                                       break;
+
+                                               default:
+                                                       let n = +amount;
+                                                       n = (n > 0) ? n : 0;
+                                                       rv = substr(mock, 0, n);
+                                                       mock = substr(mock, n);
+                                                       break;
+                                               }
+
+                                               return rv;
+                                       },
+
+                                       write: function() {},
+                                       close: function() {},
+
+                                       error: function() {
+                                               return null;
+                                       }
+                               };
+                       },
+
+                       error: () => "Unspecified error"
+               },
+
+
+               /* Mock stdlib functions */
+
+               system: function(argv, timeout) {
+                       trace_call(null, "system", { command: argv, timeout });
+
+                       return 0;
+               },
+
+               time: function() {
+                       printf("time()\n");
+
+                       return 1615382640;
+               },
+
+               print: function(...args) {
+                       if (length(args) == 1 && type(args[0]) in ["array", "object"])
+                               printf("%s\n", format_json(args[0]));
+                       else
+                               global.print(...args);
+               }
+       };
+
+
+       /* Execute test file */
+
+       if (!TESTFILE)
+               E("The TESTFILE variable is not defined.");
+
+       include(TESTFILE, mocks);
diff --git a/tests/mocks/fs/open~_proc_version.txt b/tests/mocks/fs/open~_proc_version.txt
new file mode 100644 (file)
index 0000000..fb8d3e6
--- /dev/null
@@ -0,0 +1 @@
+Linux version 5.4.101 (jow@j7) (gcc version 8.4.0 (OpenWrt GCC 8.4.0 r12978-7c2e0fa586)) #0 SMP Tue Mar 2 14:41:54 2021
diff --git a/tests/mocks/fs/open~_sys_class_net_br-lan_flags.txt b/tests/mocks/fs/open~_sys_class_net_br-lan_flags.txt
new file mode 100644 (file)
index 0000000..8198c04
--- /dev/null
@@ -0,0 +1 @@
+0x1003
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_amanda.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_amanda.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_ftp.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_ftp.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_h323.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_h323.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_irc.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_irc.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_netbios_ns.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_netbios_ns.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_pptp.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_pptp.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_rtsp.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_rtsp.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_sane.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_sane.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_sip.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_sip.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_snmp.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_snmp.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/fs/stat~_sys_module_nf_conntrack_tftp.json b/tests/mocks/fs/stat~_sys_module_nf_conntrack_tftp.json
new file mode 100644 (file)
index 0000000..d06d8cb
--- /dev/null
@@ -0,0 +1,32 @@
+{
+   "atime" : 1616175834,
+   "blksize" : 4096,
+   "blocks" : 0,
+   "ctime" : 1616175834,
+   "dev" : {
+      "major" : 0,
+      "minor" : 11
+   },
+   "gid" : 0,
+   "inode" : 6586,
+   "mode" : 493,
+   "mtime" : 1616175834,
+   "nlink" : 6,
+   "perm" : {
+      "group_exec" : true,
+      "group_read" : true,
+      "group_write" : false,
+      "other_exec" : true,
+      "other_read" : true,
+      "other_write" : false,
+      "setgid" : false,
+      "setuid" : false,
+      "sticky" : false,
+      "user_exec" : true,
+      "user_read" : true,
+      "user_write" : true
+   },
+   "size" : 0,
+   "type" : "directory",
+   "uid" : 0
+}
diff --git a/tests/mocks/ubus/network.interface~dump.json b/tests/mocks/ubus/network.interface~dump.json
new file mode 100644 (file)
index 0000000..25d3415
--- /dev/null
@@ -0,0 +1,231 @@
+{
+       "interface": [
+               {
+                       "interface": "lan",
+                       "up": true,
+                       "pending": false,
+                       "available": true,
+                       "autostart": true,
+                       "dynamic": false,
+                       "uptime": 89940,
+                       "l3_device": "br-lan",
+                       "proto": "static",
+                       "device": "br-lan",
+                       "updated": [
+                               "addresses"
+                       ],
+                       "metric": 0,
+                       "dns_metric": 0,
+                       "delegation": true,
+                       "ipv4-address": [
+                               {
+                                       "address": "192.168.26.1",
+                                       "mask": 24
+                               }
+                       ],
+                       "ipv6-address": [
+
+                       ],
+                       "ipv6-prefix": [
+
+                       ],
+                       "ipv6-prefix-assignment": [
+                               {
+                                       "address": "fd63:e2f:f706::",
+                                       "mask": 60,
+                                       "local-address": {
+                                               "address": "fd63:e2f:f706::1",
+                                               "mask": 60
+                                       }
+                               }
+                       ],
+                       "route": [
+
+                       ],
+                       "dns-server": [
+
+                       ],
+                       "dns-search": [
+
+                       ],
+                       "neighbors": [
+
+                       ],
+                       "inactive": {
+                               "ipv4-address": [
+
+                               ],
+                               "ipv6-address": [
+
+                               ],
+                               "route": [
+
+                               ],
+                               "dns-server": [
+
+                               ],
+                               "dns-search": [
+
+                               ],
+                               "neighbors": [
+
+                               ]
+                       },
+                       "data": {
+
+                       }
+               },
+               {
+                       "interface": "loopback",
+                       "up": true,
+                       "pending": false,
+                       "available": true,
+                       "autostart": true,
+                       "dynamic": false,
+                       "uptime": 89939,
+                       "l3_device": "lo",
+                       "proto": "static",
+                       "device": "lo",
+                       "updated": [
+                               "addresses"
+                       ],
+                       "metric": 0,
+                       "dns_metric": 0,
+                       "delegation": true,
+                       "ipv4-address": [
+                               {
+                                       "address": "127.0.0.1",
+                                       "mask": 8
+                               }
+                       ],
+                       "ipv6-address": [
+
+                       ],
+                       "ipv6-prefix": [
+
+                       ],
+                       "ipv6-prefix-assignment": [
+
+                       ],
+                       "route": [
+
+                       ],
+                       "dns-server": [
+
+                       ],
+                       "dns-search": [
+
+                       ],
+                       "neighbors": [
+
+                       ],
+                       "inactive": {
+                               "ipv4-address": [
+
+                               ],
+                               "ipv6-address": [
+
+                               ],
+                               "route": [
+
+                               ],
+                               "dns-server": [
+
+                               ],
+                               "dns-search": [
+
+                               ],
+                               "neighbors": [
+
+                               ]
+                       },
+                       "data": {
+
+                       }
+               },
+               {
+                       "interface": "wan6",
+                       "up": false,
+                       "pending": true,
+                       "available": true,
+                       "autostart": true,
+                       "dynamic": false,
+                       "proto": "dhcpv6",
+                       "device": "wan",
+                       "data": {
+
+                       }
+               },
+               {
+                       "interface": "wan",
+                       "up": true,
+                       "pending": false,
+                       "available": true,
+                       "autostart": true,
+                       "dynamic": false,
+                       "uptime": 35968,
+                       "l3_device": "wan",
+                       "proto": "dhcp",
+                       "device": "wan",
+                       "metric": 0,
+                       "dns_metric": 0,
+                       "delegation": true,
+                       "ipv4-address": [
+                               {
+                                       "address": "10.11.12.194",
+                                       "mask": 24
+                               }
+                       ],
+                       "ipv6-address": [
+
+                       ],
+                       "ipv6-prefix": [
+
+                       ],
+                       "ipv6-prefix-assignment": [
+
+                       ],
+                       "route": [
+                               {
+                                       "target": "0.0.0.0",
+                                       "mask": 0,
+                                       "nexthop": "10.11.12.13",
+                                       "source": "10.11.12.194/32"
+                               }
+                       ],
+                       "dns-server": [
+                               "10.11.12.13"
+                       ],
+                       "dns-search": [
+                               "lan"
+                       ],
+                       "neighbors": [
+
+                       ],
+                       "inactive": {
+                               "ipv4-address": [
+
+                               ],
+                               "ipv6-address": [
+
+                               ],
+                               "route": [
+
+                               ],
+                               "dns-server": [
+
+                               ],
+                               "dns-search": [
+
+                               ],
+                               "neighbors": [
+
+                               ]
+                       },
+                       "data": {
+                               "hostname": "OpenWrt",
+                               "leasetime": 43200
+                       }
+               }
+       ]
+}
diff --git a/tests/mocks/ubus/service~get_data~type-firewall.json b/tests/mocks/ubus/service~get_data~type-firewall.json
new file mode 100644 (file)
index 0000000..36a3713
--- /dev/null
@@ -0,0 +1,3 @@
+{
+       
+}
diff --git a/tests/mocks/uci/firewall.json b/tests/mocks/uci/firewall.json
new file mode 100644 (file)
index 0000000..a7e7720
--- /dev/null
@@ -0,0 +1,186 @@
+{
+   "cfg01e63d" : {
+      ".anonymous" : true,
+      ".index" : 0,
+      ".name" : "cfg01e63d",
+      ".type" : "defaults",
+      "forward" : "REJECT",
+      "input" : "ACCEPT",
+      "output" : "ACCEPT",
+      "syn_flood" : "1"
+   },
+   "cfg02dc81" : {
+      ".anonymous" : true,
+      ".index" : 1,
+      ".name" : "cfg02dc81",
+      ".type" : "zone",
+      "forward" : "ACCEPT",
+      "input" : "ACCEPT",
+      "name" : "lan",
+      "network" : [
+         "lan"
+      ],
+      "output" : "ACCEPT"
+   },
+   "cfg03dc81" : {
+      ".anonymous" : true,
+      ".index" : 2,
+      ".name" : "cfg03dc81",
+      ".type" : "zone",
+      "forward" : "REJECT",
+      "input" : "REJECT",
+      "masq" : "1",
+      "mtu_fix" : "1",
+      "name" : "wan",
+      "network" : [
+         "wan",
+         "wan6"
+      ],
+      "output" : "ACCEPT"
+   },
+   "cfg04ad58" : {
+      ".anonymous" : true,
+      ".index" : 3,
+      ".name" : "cfg04ad58",
+      ".type" : "forwarding",
+      "dest" : "wan",
+      "src" : "lan"
+   },
+   "cfg0592bd" : {
+      ".anonymous" : true,
+      ".index" : 4,
+      ".name" : "cfg0592bd",
+      ".type" : "rule",
+      "dest_port" : "68",
+      "family" : "ipv4",
+      "name" : "Allow-DHCP-Renew",
+      "proto" : "udp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0692bd" : {
+      ".anonymous" : true,
+      ".index" : 5,
+      ".name" : "cfg0692bd",
+      ".type" : "rule",
+      "family" : "ipv4",
+      "icmp_type" : "echo-request",
+      "name" : "Allow-Ping",
+      "proto" : "icmp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0792bd" : {
+      ".anonymous" : true,
+      ".index" : 6,
+      ".name" : "cfg0792bd",
+      ".type" : "rule",
+      "family" : "ipv4",
+      "name" : "Allow-IGMP",
+      "proto" : "igmp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0892bd" : {
+      ".anonymous" : true,
+      ".index" : 7,
+      ".name" : "cfg0892bd",
+      ".type" : "rule",
+      "dest_ip" : "fc00::/6",
+      "dest_port" : "546",
+      "family" : "ipv6",
+      "name" : "Allow-DHCPv6",
+      "proto" : "udp",
+      "src" : "wan",
+      "src_ip" : "fc00::/6",
+      "target" : "ACCEPT"
+   },
+   "cfg0992bd" : {
+      ".anonymous" : true,
+      ".index" : 8,
+      ".name" : "cfg0992bd",
+      ".type" : "rule",
+      "family" : "ipv6",
+      "icmp_type" : [
+         "130/0",
+         "131/0",
+         "132/0",
+         "143/0"
+      ],
+      "name" : "Allow-MLD",
+      "proto" : "icmp",
+      "src" : "wan",
+      "src_ip" : "fe80::/10",
+      "target" : "ACCEPT"
+   },
+   "cfg0a92bd" : {
+      ".anonymous" : true,
+      ".index" : 9,
+      ".name" : "cfg0a92bd",
+      ".type" : "rule",
+      "family" : "ipv6",
+      "icmp_type" : [
+         "echo-request",
+         "echo-reply",
+         "destination-unreachable",
+         "packet-too-big",
+         "time-exceeded",
+         "bad-header",
+         "unknown-header-type",
+         "router-solicitation",
+         "neighbour-solicitation",
+         "router-advertisement",
+         "neighbour-advertisement"
+      ],
+      "limit" : "1000/sec",
+      "name" : "Allow-ICMPv6-Input",
+      "proto" : "icmp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0b92bd" : {
+      ".anonymous" : true,
+      ".index" : 10,
+      ".name" : "cfg0b92bd",
+      ".type" : "rule",
+      "dest" : "*",
+      "family" : "ipv6",
+      "icmp_type" : [
+         "echo-request",
+         "echo-reply",
+         "destination-unreachable",
+         "packet-too-big",
+         "time-exceeded",
+         "bad-header",
+         "unknown-header-type"
+      ],
+      "limit" : "1000/sec",
+      "name" : "Allow-ICMPv6-Forward",
+      "proto" : "icmp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0c92bd" : {
+      ".anonymous" : true,
+      ".index" : 11,
+      ".name" : "cfg0c92bd",
+      ".type" : "rule",
+      "dest" : "lan",
+      "name" : "Allow-IPSec-ESP",
+      "proto" : "esp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   },
+   "cfg0d92bd" : {
+      ".anonymous" : true,
+      ".index" : 12,
+      ".name" : "cfg0d92bd",
+      ".type" : "rule",
+      "dest" : "lan",
+      "dest_port" : "500",
+      "name" : "Allow-ISAKMP",
+      "proto" : "udp",
+      "src" : "wan",
+      "target" : "ACCEPT"
+   }
+}
diff --git a/tests/mocks/uci/helpers.json b/tests/mocks/uci/helpers.json
new file mode 100644 (file)
index 0000000..453901d
--- /dev/null
@@ -0,0 +1,146 @@
+{
+   "cfg0153e5" : {
+      ".anonymous" : true,
+      ".index" : 0,
+      ".name" : "cfg0153e5",
+      ".type" : "helper",
+      "description" : "Amanda backup and archiving proto",
+      "family" : "any",
+      "module" : "nf_conntrack_amanda",
+      "name" : "amanda",
+      "port" : "10080",
+      "proto" : "udp"
+   },
+   "cfg0253e5" : {
+      ".anonymous" : true,
+      ".index" : 1,
+      ".name" : "cfg0253e5",
+      ".type" : "helper",
+      "description" : "FTP passive connection tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_ftp",
+      "name" : "ftp",
+      "port" : "21",
+      "proto" : "tcp"
+   },
+   "cfg0353e5" : {
+      ".anonymous" : true,
+      ".index" : 2,
+      ".name" : "cfg0353e5",
+      ".type" : "helper",
+      "description" : "RAS proto tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_h323",
+      "name" : "RAS",
+      "port" : "1719",
+      "proto" : "udp"
+   },
+   "cfg0453e5" : {
+      ".anonymous" : true,
+      ".index" : 3,
+      ".name" : "cfg0453e5",
+      ".type" : "helper",
+      "description" : "Q.931 proto tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_h323",
+      "name" : "Q.931",
+      "port" : "1720",
+      "proto" : "tcp"
+   },
+   "cfg0553e5" : {
+      ".anonymous" : true,
+      ".index" : 4,
+      ".name" : "cfg0553e5",
+      ".type" : "helper",
+      "description" : "IRC DCC connection tracking",
+      "family" : "ipv4",
+      "module" : "nf_conntrack_irc",
+      "name" : "irc",
+      "port" : "6667",
+      "proto" : "tcp"
+   },
+   "cfg0653e5" : {
+      ".anonymous" : true,
+      ".index" : 5,
+      ".name" : "cfg0653e5",
+      ".type" : "helper",
+      "description" : "NetBIOS name service broadcast tracking",
+      "family" : "ipv4",
+      "module" : "nf_conntrack_netbios_ns",
+      "name" : "netbios-ns",
+      "port" : "137",
+      "proto" : "udp"
+   },
+   "cfg0753e5" : {
+      ".anonymous" : true,
+      ".index" : 6,
+      ".name" : "cfg0753e5",
+      ".type" : "helper",
+      "description" : "PPTP VPN connection tracking",
+      "family" : "ipv4",
+      "module" : "nf_conntrack_pptp",
+      "name" : "pptp",
+      "port" : "1723",
+      "proto" : "tcp"
+   },
+   "cfg0853e5" : {
+      ".anonymous" : true,
+      ".index" : 7,
+      ".name" : "cfg0853e5",
+      ".type" : "helper",
+      "description" : "SANE scanner connection tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_sane",
+      "name" : "sane",
+      "port" : "6566",
+      "proto" : "tcp"
+   },
+   "cfg0953e5" : {
+      ".anonymous" : true,
+      ".index" : 8,
+      ".name" : "cfg0953e5",
+      ".type" : "helper",
+      "description" : "SIP VoIP connection tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_sip",
+      "name" : "sip",
+      "port" : "5060",
+      "proto" : "udp"
+   },
+   "cfg0a53e5" : {
+      ".anonymous" : true,
+      ".index" : 9,
+      ".name" : "cfg0a53e5",
+      ".type" : "helper",
+      "description" : "SNMP monitoring connection tracking",
+      "family" : "ipv4",
+      "module" : "nf_conntrack_snmp",
+      "name" : "snmp",
+      "port" : "161",
+      "proto" : "udp"
+   },
+   "cfg0b53e5" : {
+      ".anonymous" : true,
+      ".index" : 10,
+      ".name" : "cfg0b53e5",
+      ".type" : "helper",
+      "description" : "TFTP connection tracking",
+      "family" : "any",
+      "module" : "nf_conntrack_tftp",
+      "name" : "tftp",
+      "port" : "69",
+      "proto" : "udp"
+   },
+   "cfg0c53e5" : {
+      ".anonymous" : true,
+      ".index" : 11,
+      ".name" : "cfg0c53e5",
+      ".type" : "helper",
+      "description" : "RTSP connection tracking",
+      "family" : "ipv4",
+      "module" : "nf_conntrack_rtsp",
+      "name" : "rtsp",
+      "port" : "554",
+      "proto" : "tcp"
+   }
+}
diff --git a/tests/test-wrapper.uc b/tests/test-wrapper.uc
new file mode 100644 (file)
index 0000000..a5412ae
--- /dev/null
@@ -0,0 +1,5 @@
+{%
+       fw4 = require("fw4");
+
+       include("../root/usr/share/firewall4/main.uc");
+%}