From 59dbb982b7fefa480196dec03ba51c4f8c4dd7ae Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Fri, 19 Mar 2021 19:26:04 +0100 Subject: [PATCH 1/1] Initial commit Signed-off-by: Jo-Philipp Wich --- .editorconfig | 9 + root/etc/config/firewall | 191 ++ root/etc/hotplug.d/iface/20-firewall | 11 + root/etc/init.d/firewall | 32 + .../nftables.d/10-custom-filter-chains.nft | 39 + root/etc/nftables.d/README | 4 + root/usr/sbin/fw3 | 1 + root/usr/sbin/fw4 | 164 + root/usr/share/firewall4/helpers | 95 + root/usr/share/firewall4/main.uc | 177 ++ .../usr/share/firewall4/templates/redirect.uc | 67 + root/usr/share/firewall4/templates/rule.uc | 90 + root/usr/share/firewall4/templates/ruleset.uc | 386 +++ .../share/firewall4/templates/zone-match.uc | 20 + .../share/firewall4/templates/zone-mssfix.uc | 15 + .../share/firewall4/templates/zone-notrack.uc | 19 + .../share/firewall4/templates/zone-verdict.uc | 20 + root/usr/share/ucode/fw4.uc | 2831 +++++++++++++++++ run_tests.sh | 173 + tests/01_configuration/01_ruleset | 303 ++ tests/mock.uc | 479 +++ tests/mocks/fs/open~_proc_version.txt | 1 + .../fs/open~_sys_class_net_br-lan_flags.txt | 1 + .../stat~_sys_module_nf_conntrack_amanda.json | 32 + .../fs/stat~_sys_module_nf_conntrack_ftp.json | 32 + .../stat~_sys_module_nf_conntrack_h323.json | 32 + .../fs/stat~_sys_module_nf_conntrack_irc.json | 32 + ...t~_sys_module_nf_conntrack_netbios_ns.json | 32 + .../stat~_sys_module_nf_conntrack_pptp.json | 32 + .../stat~_sys_module_nf_conntrack_rtsp.json | 32 + .../stat~_sys_module_nf_conntrack_sane.json | 32 + .../fs/stat~_sys_module_nf_conntrack_sip.json | 32 + .../stat~_sys_module_nf_conntrack_snmp.json | 32 + .../stat~_sys_module_nf_conntrack_tftp.json | 32 + tests/mocks/ubus/network.interface~dump.json | 231 ++ .../ubus/service~get_data~type-firewall.json | 3 + tests/mocks/uci/firewall.json | 186 ++ tests/mocks/uci/helpers.json | 146 + tests/test-wrapper.uc | 5 + 39 files changed, 6051 insertions(+) create mode 100644 .editorconfig create mode 100644 root/etc/config/firewall create mode 100644 root/etc/hotplug.d/iface/20-firewall create mode 100755 root/etc/init.d/firewall create mode 100644 root/etc/nftables.d/10-custom-filter-chains.nft create mode 100644 root/etc/nftables.d/README create mode 120000 root/usr/sbin/fw3 create mode 100755 root/usr/sbin/fw4 create mode 100644 root/usr/share/firewall4/helpers create mode 100644 root/usr/share/firewall4/main.uc create mode 100644 root/usr/share/firewall4/templates/redirect.uc create mode 100644 root/usr/share/firewall4/templates/rule.uc create mode 100644 root/usr/share/firewall4/templates/ruleset.uc create mode 100644 root/usr/share/firewall4/templates/zone-match.uc create mode 100644 root/usr/share/firewall4/templates/zone-mssfix.uc create mode 100644 root/usr/share/firewall4/templates/zone-notrack.uc create mode 100644 root/usr/share/firewall4/templates/zone-verdict.uc create mode 100644 root/usr/share/ucode/fw4.uc create mode 100755 run_tests.sh create mode 100644 tests/01_configuration/01_ruleset create mode 100644 tests/mock.uc create mode 100644 tests/mocks/fs/open~_proc_version.txt create mode 100644 tests/mocks/fs/open~_sys_class_net_br-lan_flags.txt create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_amanda.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_ftp.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_h323.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_irc.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_netbios_ns.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_pptp.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_rtsp.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_sane.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_sip.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_snmp.json create mode 100644 tests/mocks/fs/stat~_sys_module_nf_conntrack_tftp.json create mode 100644 tests/mocks/ubus/network.interface~dump.json create mode 100644 tests/mocks/ubus/service~get_data~type-firewall.json create mode 100644 tests/mocks/uci/firewall.json create mode 100644 tests/mocks/uci/helpers.json create mode 100644 tests/test-wrapper.uc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c881546 --- /dev/null +++ b/.editorconfig @@ -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 index 0000000..f4a3322 --- /dev/null +++ b/root/etc/config/firewall @@ -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 index 0000000..c2ed89a --- /dev/null +++ b/root/etc/hotplug.d/iface/20-firewall @@ -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 index 0000000..f4bdd99 --- /dev/null +++ b/root/etc/init.d/firewall @@ -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 index 0000000..4cb4213 --- /dev/null +++ b/root/etc/nftables.d/10-custom-filter-chains.nft @@ -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 index 0000000..70f4779 --- /dev/null +++ b/root/etc/nftables.d/README @@ -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 index 0000000..8fbcf2c --- /dev/null +++ b/root/usr/sbin/fw3 @@ -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 index 0000000..ac95473 --- /dev/null +++ b/root/usr/sbin/fw4 @@ -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 < { + 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 index 0000000..b710a04 --- /dev/null +++ b/root/usr/share/firewall4/templates/redirect.uc @@ -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 index 0000000..1f01d5f --- /dev/null +++ b/root/usr/share/firewall4/templates/rule.uc @@ -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 index 0000000..b2a996d --- /dev/null +++ b/root/usr/share/firewall4/templates/ruleset.uc @@ -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 index 0000000..656c568 --- /dev/null +++ b/root/usr/share/firewall4/templates/zone-match.uc @@ -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 index 0000000..dd5766b --- /dev/null +++ b/root/usr/share/firewall4/templates/zone-mssfix.uc @@ -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 index 0000000..fe31eb2 --- /dev/null +++ b/root/usr/share/firewall4/templates/zone-notrack.uc @@ -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 index 0000000..c8f5667 --- /dev/null +++ b/root/usr/share/firewall4/templates/zone-verdict.uc @@ -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 index 0000000..46dff9a --- /dev/null +++ b/root/usr/share/ucode/fw4.uc @@ -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 index 0000000..eb4dafa --- /dev/null +++ b/run_tests.sh @@ -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 index 0000000..5725ebf --- /dev/null +++ b/tests/01_configuration/01_ruleset @@ -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 method args +[call] ctx.call object method args <{ "type": "firewall" }> +[call] fs.open path mode +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.stat path +[call] fs.open path mode +[call] fs.open path mode +-- End -- diff --git a/tests/mock.uc b/tests/mock.uc new file mode 100644 index 0000000..7a5da39 --- /dev/null +++ b/tests/mock.uc @@ -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 index 0000000..fb8d3e6 --- /dev/null +++ b/tests/mocks/fs/open~_proc_version.txt @@ -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 index 0000000..8198c04 --- /dev/null +++ b/tests/mocks/fs/open~_sys_class_net_br-lan_flags.txt @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_amanda.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_ftp.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_h323.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_irc.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_netbios_ns.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_pptp.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_rtsp.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_sane.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_sip.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_snmp.json @@ -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 index 0000000..d06d8cb --- /dev/null +++ b/tests/mocks/fs/stat~_sys_module_nf_conntrack_tftp.json @@ -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 index 0000000..25d3415 --- /dev/null +++ b/tests/mocks/ubus/network.interface~dump.json @@ -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 index 0000000..36a3713 --- /dev/null +++ b/tests/mocks/ubus/service~get_data~type-firewall.json @@ -0,0 +1,3 @@ +{ + +} diff --git a/tests/mocks/uci/firewall.json b/tests/mocks/uci/firewall.json new file mode 100644 index 0000000..a7e7720 --- /dev/null +++ b/tests/mocks/uci/firewall.json @@ -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 index 0000000..453901d --- /dev/null +++ b/tests/mocks/uci/helpers.json @@ -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 index 0000000..a5412ae --- /dev/null +++ b/tests/test-wrapper.uc @@ -0,0 +1,5 @@ +{% + fw4 = require("fw4"); + + include("../root/usr/share/firewall4/main.uc"); +%} -- 2.30.2