tests: expand testing
authorJo-Philipp Wich <jo@mein.io>
Wed, 31 Mar 2021 20:09:01 +0000 (22:09 +0200)
committerJo-Philipp Wich <jo@mein.io>
Wed, 31 Mar 2021 20:10:04 +0000 (22:10 +0200)
 - Rewrite test framework
 - Add initial rule test coverage

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
14 files changed:
run_tests.sh
tests/01_configuration/01_ruleset
tests/03_rules/01_direction [new file with mode: 0644]
tests/03_rules/02_enabled [new file with mode: 0644]
tests/03_rules/03_constraints [new file with mode: 0644]
tests/03_rules/04_icmp [new file with mode: 0644]
tests/lib/mocklib.uc [new file with mode: 0644]
tests/lib/mocklib/fs.uc [new file with mode: 0644]
tests/lib/mocklib/ubus.uc [new file with mode: 0644]
tests/lib/mocklib/uci.uc [new file with mode: 0644]
tests/mock.uc [deleted file]
tests/mocks/uci/firewall.json
tests/mocks/uci/helpers.json
tests/test-wrapper.uc [deleted file]

index eb4dafa003e6977eb333391f7aba1833173df7e7..8f0c2938d6b376d76df638d15c8d0167e08f114f 100755 (executable)
@@ -1,7 +1,14 @@
 #!/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" ] }'
+uenv='{
+       "REQUIRE_SEARCH_PATH": [
+               "./tests/lib/*.uc",
+               "./root/usr/share/ucode/*.uc",
+               "/usr/local/lib/ucode/*.so",
+               "/usr/lib/ucode/*.so"
+       ]
+}'
 
 extract_sections() {
        local file=$1
@@ -30,6 +37,14 @@ extract_sections() {
                                outfile=$(printf "%s/%03d.%s" "$dir" $count "$tag")
                                printf "" > "$outfile"
                        ;;
+                       "-- File "*" --")
+                               tag="file"
+                               outfile="${line#-- File }"
+                               outfile="$(echo "${outfile% --}" | xargs)"
+                               outfile="$dir/files$(readlink -m "/${outfile:-file}")"
+                               mkdir -p "$(dirname "$outfile")"
+                               printf "" > "$outfile"
+                       ;;
                        "-- End --")
                                tag=""
                                outfile=""
@@ -55,7 +70,12 @@ run_testcase() {
        local code=$7
        local fail=0
 
-       ucode ${uenv:+-e "$uenv"} ${env:+-e "$(cat "$env")"} -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
+       ucode -m mocklib -m fw4 ${uenv:+-e "$uenv"} -e '{
+               "MOCK_SEARCH_PATH": [
+                       "'"$dir"'/files",
+                       "./tests/mocks"
+               ]
+       }' ${env:+-e "$(cat "$env")"} -i - <"$in" >"$dir/res.out" 2>"$dir/res.err"
 
        printf "%d\n" $? > "$dir/res.code"
 
@@ -157,13 +177,31 @@ run_test() {
 n_tests=0
 n_fails=0
 
+select_tests="$@"
+
+use_test() {
+       local input="$(readlink -f "$1")"
+       local test
+
+       [ -f "$input" ] || return 1
+       [ -n "$select_tests" ] || return 0
+
+       for test in "$select_tests"; do
+               test="$(readlink -f "$test")"
+
+               [ "$test" != "$input" ] || return 0
+       done
+
+       return 1
+}
+
 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
+               use_test "$testfile" || continue
 
                n_tests=$((n_tests + 1))
                run_test "$testfile" || n_fails=$((n_fails + 1))
@@ -171,3 +209,4 @@ for catdir in tests/[0-9][0-9]_*; do
 done
 
 printf "\nRan %d tests, %d okay, %d failures\n" $n_tests $((n_tests - n_fails)) $n_fails
+exit $n_fails
index 5725ebfabc43a85577a854c4f991212d697947a6..313b46a782c060c48ab8c7f8ba6314528295b976 100644 (file)
@@ -2,8 +2,7 @@ Testing the ruleset rendered from the default firewall configuration.
 
 -- Testcase --
 {%
-       include("./tests/mock.uc", {
-               TESTFILE: "test-wrapper.uc",
+       include("./root/usr/share/firewall4/main.uc", {
                TRACE_CALLS: "stderr",
 
                getenv: function(varname) {
@@ -32,11 +31,9 @@ table inet fw4 {
 
        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
        #
@@ -54,26 +51,17 @@ table inet fw4 {
                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
        }
 
@@ -83,12 +71,8 @@ table inet fw4 {
                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 {
@@ -101,7 +85,6 @@ table inet fw4 {
                drop comment "!fw4: Drop excess packets"
        }
 
-
        chain input_lan {
                jump accept_from_lan
        }
@@ -125,11 +108,12 @@ table inet fw4 {
 
        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 icmp type 8 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"
+               ip6 saddr fe80::/10 icmpv6 type . icmpv6 code { 130 . 0, 131 . 0, 132 . 0, 143 . 0 } counter accept comment "!fw4: Allow-MLD"
+               meta nfproto ipv6 icmpv6 type { 128, 129, 1, 3, 133, 134 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Input"
+               meta nfproto ipv6 icmpv6 type . icmpv6 code { 2 . 0, 4 . 0, 4 . 1, 135 . 0, 136 . 0 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Input"
                jump reject_from_wan
        }
 
@@ -138,7 +122,8 @@ table inet fw4 {
        }
 
        chain forward_wan {
-               meta nfproto ipv6 meta l4proto ipv6-icmp limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Forward"
+               meta nfproto ipv6 icmpv6 type { 128, 129, 1, 3 } limit rate 1000/second counter accept comment "!fw4: Allow-ICMPv6-Forward"
+               meta nfproto ipv6 icmpv6 type . icmpv6 code { 2 . 0, 4 . 0, 4 . 1 } 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
@@ -157,19 +142,16 @@ table inet fw4 {
        }
 
 
-
        #
        # 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"
        }
 
@@ -184,13 +166,11 @@ table inet fw4 {
 
        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 {
@@ -241,7 +221,6 @@ table inet fw4 {
                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"
@@ -258,24 +237,20 @@ table inet fw4 {
        }
 
 
-
        #
        # 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"
        }
diff --git a/tests/03_rules/01_direction b/tests/03_rules/01_direction
new file mode 100644 (file)
index 0000000..b2079c7
--- /dev/null
@@ -0,0 +1,148 @@
+Testing that rule declarations are mapped to the proper chains depending
+on src and dest options.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "rule": [
+               {
+                       ".description": "Neither source, nor dest => should result in an output rule",
+                       "proto": "any"
+               },
+               {
+                       ".description": "Source any, no dest => should result in an input rule",
+                       "proto": "any",
+                       "src": "*"
+               },
+               {
+                       ".description": "Dest any, no source => should result in an output rule",
+                       "proto": "any",
+                       "dest": "*"
+               },
+               {
+                       ".description": "Source any, dest any => should result in a forward rule",
+                       "proto": "any",
+                       "src": "*",
+                       "dest": "*"
+               }
+       ]
+}
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+               counter comment "!fw4: @rule[1]"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+               counter comment "!fw4: @rule[3]"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               counter comment "!fw4: @rule[0]"
+               counter comment "!fw4: @rule[2]"
+       }
+
+       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"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+
+       #
+       # 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;
+       }
+}
+-- End --
diff --git a/tests/03_rules/02_enabled b/tests/03_rules/02_enabled
new file mode 100644 (file)
index 0000000..247f236
--- /dev/null
@@ -0,0 +1,143 @@
+Testing that not enabled rules are ignored.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "rule": [
+               {
+                       "proto": "any",
+                       "name": "Implicitly enabled"
+               },
+               {
+                       "proto": "any",
+                       "name": "Explicitly enabled",
+                       "enabled": "1"
+               },
+               {
+                       "proto": "any",
+                       "name": "Explicitly disabled",
+                       "enabled": "0"
+               }
+       ]
+}
+-- End --
+
+-- Expect stderr --
+[!] Section @rule[2] (Explicitly disabled) is disabled, ignoring section
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               counter comment "!fw4: Implicitly enabled"
+               counter comment "!fw4: Explicitly enabled"
+       }
+
+       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"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+
+       #
+       # 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;
+       }
+}
+-- End --
diff --git a/tests/03_rules/03_constraints b/tests/03_rules/03_constraints
new file mode 100644 (file)
index 0000000..8b1e04b
--- /dev/null
@@ -0,0 +1,207 @@
+Testing various option constraints.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "zone": [
+               {
+                       "name": "lan"
+               }
+       ],
+       "rule": [
+               {
+                       ".description": "Helper rules require an explicit source zone",
+                       "proto": "any",
+                       "name": "Helper rule #1",
+                       "target": "helper"
+               },
+               {
+                       ".description": "Helper rules require a set_helper option",
+                       "proto": "any",
+                       "name": "Helper rule #2",
+                       "src": "lan",
+                       "target": "helper"
+               },
+
+               {
+                       ".description": "Notrack rules require an explicit source zone",
+                       "proto": "any",
+                       "name": "Notrack rule",
+                       "target": "notrack"
+               },
+
+               {
+                       ".description": "DSCP rules must not specify a destination",
+                       "proto": "any",
+                       "name": "DSCP rule #1",
+                       "dest": "*",
+                       "target": "dscp"
+               },
+               {
+                       ".description": "DSCP rules require a set_dscp option",
+                       "proto": "any",
+                       "name": "DSCP rule #2",
+                       "target": "dscp"
+               },
+
+               {
+                       ".description": "Mark rules must not specify a destination",
+                       "proto": "any",
+                       "name": "Mark rule #1",
+                       "dest": "*",
+                       "target": "mark"
+               },
+               {
+                       ".description": "Mark rules require a set_xmark or set_mark option",
+                       "proto": "any",
+                       "name": "Mark rule #2",
+                       "target": "mark"
+               },
+       ]
+}
+-- End --
+
+-- Expect stderr --
+[!] Section @rule[0] (Helper rule #1) must specify a source zone for target 'helper'
+[!] Section @rule[1] (Helper rule #2) must specify option 'set_helper' for target 'helper'
+[!] Section @rule[2] (Notrack rule) must specify a source zone for target 'notrack'
+[!] Section @rule[3] (DSCP rule #1) must not specify option 'dest' for target 'dscp'
+[!] Section @rule[4] (DSCP rule #2) must specify option 'set_dscp' for target 'dscp'
+[!] Section @rule[5] (Mark rule #1) must not specify option 'dest' for target 'mark'
+[!] Section @rule[6] (Mark rule #2) must specify option 'set_mark' or 'set_xmark' for target 'mark'
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+       }
+
+       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 input_lan {
+               jump drop_from_lan
+       }
+
+       chain output_lan {
+               jump drop_to_lan
+       }
+
+       chain forward_lan {
+               jump drop_to_lan
+       }
+
+       chain drop_from_lan {
+       }
+
+       chain drop_to_lan {
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+       chain helper_lan {
+       }
+
+
+       #
+       # 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;
+       }
+}
+-- End --
diff --git a/tests/03_rules/04_icmp b/tests/03_rules/04_icmp
new file mode 100644 (file)
index 0000000..53cee11
--- /dev/null
@@ -0,0 +1,155 @@
+Testing handling of ICMP related options.
+
+-- Testcase --
+{%
+       include("./root/usr/share/firewall4/main.uc", {
+               getenv: function(varname) {
+                       switch (varname) {
+                       case 'ACTION':
+                               return 'print';
+                       }
+               }
+       })
+%}
+-- End --
+
+-- File uci/helpers.json --
+{}
+-- End --
+
+-- File uci/firewall.json --
+{
+       "rule": [
+               {
+                       ".description": "Proto 'icmp' maps to IPv4 and IPv6 rules",
+                       "proto": "icmp",
+                       "name": "ICMP rule #1"
+               },
+               {
+                       ".description": "Proto 'icmpv6' maps to IPv6 rule only",
+                       "proto": "icmpv6",
+                       "name": "ICMP rule #2",
+               },
+               {
+                       ".description": "Proto 'ipv6-icmp' is an alias for 'icmpv6'",
+                       "proto": "ipv6-icmp",
+                       "name": "ICMP rule #3",
+               },
+               {
+                       ".description": "Proto 'icmp' with IPv4 specific types inhibits IPv6 rule",
+                       "proto": "icmp",
+                       "name": "ICMP rule #4",
+                       "icmp_type": [ "ip-header-bad" ]
+               },
+               {
+                       ".description": "Proto 'icmp' with IPv6 specific types inhibits IPv4 rule",
+                       "proto": "icmp",
+                       "name": "ICMP rule #5",
+                       "icmp_type": [ "neighbour-advertisement" ]
+               }
+       ]
+}
+-- End --
+
+-- Expect stdout --
+table inet fw4
+flush table inet fw4
+
+table inet fw4 {
+       #
+       # Set definitions
+       #
+
+
+       #
+       # Defines
+       #
+
+
+       #
+       # User includes
+       #
+
+       include "/etc/nftables.d/*.nft"
+
+
+       #
+       # Filter rules
+       #
+
+       chain input {
+               type filter hook input priority filter; policy drop;
+
+               iifname "lo" accept comment "!fw4: Accept traffic from loopback"
+
+               ct state established,related accept comment "!fw4: Allow inbound established and related flows"
+       }
+
+       chain forward {
+               type filter hook forward priority filter; policy drop;
+
+               ct state established,related accept comment "!fw4: Allow forwarded established and related flows"
+       }
+
+       chain output {
+               type filter hook output priority filter; policy drop;
+
+               oifname "lo" accept comment "!fw4: Accept traffic towards loopback"
+
+               ct state established,related accept comment "!fw4: Allow outbound established and related flows"
+               meta l4proto icmp counter comment "!fw4: ICMP rule #1"
+               meta nfproto ipv6 meta l4proto ipv6-icmp counter comment "!fw4: ICMP rule #2"
+               meta nfproto ipv6 meta l4proto ipv6-icmp counter comment "!fw4: ICMP rule #3"
+               meta nfproto ipv4 icmp type . icmp code 12 . 0 counter comment "!fw4: ICMP rule #4"
+               meta nfproto ipv6 icmpv6 type . icmpv6 code 136 . 0 counter comment "!fw4: ICMP rule #5"
+       }
+
+       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"
+       }
+
+
+       #
+       # NAT rules
+       #
+
+       chain dstnat {
+               type nat hook prerouting priority dstnat; policy accept;
+       }
+
+       chain srcnat {
+               type nat hook postrouting priority srcnat; policy accept;
+       }
+
+
+       #
+       # Raw rules (notrack & helper)
+       #
+
+       chain raw_prerouting {
+               type filter hook prerouting priority raw; policy accept;
+       }
+
+       chain raw_output {
+               type filter hook output priority raw; policy accept;
+       }
+
+
+       #
+       # 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;
+       }
+}
+-- End --
diff --git a/tests/lib/mocklib.uc b/tests/lib/mocklib.uc
new file mode 100644 (file)
index 0000000..db38022
--- /dev/null
@@ -0,0 +1,224 @@
+{%
+       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 read_data_file = (path) => {
+               for (let dir in MOCK_SEARCH_PATH) {
+                       let fd = _fs.open(dir + '/' + path, "r");
+
+                       if (fd) {
+                               let data = fd.read("all");
+                               fd.close();
+
+                               return data;
+                       }
+               }
+
+               return null;
+       };
+
+       let read_json_file = (path) => {
+               let data = read_data_file(path);
+
+               if (data != null)  {
+                       try {
+                               return json(data);
+                       }
+                       catch (e) {
+                               _log('error', "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':
+                       _fs.stdout.write(msg + "\n");
+                       break;
+
+               case 'stderr':
+                       _fs.stderr.write(msg + "\n");
+                       break;
+               }
+       };
+
+       /* Prepend mocklib to REQUIRE_SEARCH_PATH */
+       for (let pattern in REQUIRE_SEARCH_PATH) {
+               /* Only consider ucode includes */
+               if (!match(pattern, /\*\.uc$/))
+                       continue;
+
+               let path = replace(pattern, /\*/, 'mocklib'),
+                   stat = _fs.stat(path);
+
+               if (!stat || stat.type != 'file')
+                       continue;
+
+               if (type(MOCK_SEARCH_PATH) != 'array' || length(MOCK_SEARCH_PATH) == 0)
+                       MOCK_SEARCH_PATH = [ replace(path, /mocklib\.uc$/, '../mocks') ];
+
+               unshift(REQUIRE_SEARCH_PATH, replace(path, /mocklib\.uc$/, 'mocklib/*.uc'));
+               break;
+       }
+
+       if (type(MOCK_SEARCH_PATH) != 'array' || length(MOCK_SEARCH_PATH) == 0)
+               MOCK_SEARCH_PATH = [ './mocks' ];
+
+       /* Register global mocklib namespace */
+       global.mocklib = {
+               require: function(module) {
+                       let path, res, ex;
+
+                       if (type(REQUIRE_SEARCH_PATH) == "array" && index(REQUIRE_SEARCH_PATH[0], 'mocklib/*.uc') != -1)
+                               path = shift(REQUIRE_SEARCH_PATH);
+
+                       try {
+                               res = require(module);
+                       }
+                       catch (e) {
+                               ex = e;
+                       }
+
+                       if (path)
+                               unshift(REQUIRE_SEARCH_PATH, path);
+
+                       if (ex)
+                               die(ex);
+
+                       return res;
+               },
+
+               I: (...args) => _log('info', ...args),
+               N: (...args) => _log('notice', ...args),
+               W: (...args) => _log('warn', ...args),
+               E: (...args) => _log('error', ...args),
+
+               format_json,
+               read_data_file,
+               read_json_file,
+               trace_call
+       };
+
+       /* Override stdlib functions */
+       global.system = function(argv, timeout) {
+               trace_call(null, "system", { command: argv, timeout });
+
+               return 0;
+       };
+
+       global.time = function() {
+               trace_call(null, "time");
+
+               return 1615382640;
+       };
+
+       global.print = function(...args) {
+               if (length(args) == 1 && type(args[0]) in ["array", "object"])
+                       printf("%s\n", format_json(args[0]));
+               else
+                       global.print(...args);
+       };
+
diff --git a/tests/lib/mocklib/fs.uc b/tests/lib/mocklib/fs.uc
new file mode 100644 (file)
index 0000000..1bde07e
--- /dev/null
@@ -0,0 +1,140 @@
+{%
+       let mocklib = global.mocklib,
+           fs = mocklib.require("fs");
+
+       return {
+               readlink: function(path) {
+                       mocklib.trace_call("fs", "readlink", { path });
+
+                       return path + "-link";
+               },
+
+               stat: function(path) {
+                       let file = sprintf("fs/stat~%s.json", replace(path, /[^A-Za-z0-9_-]+/g, '_')),
+                           mock = mocklib.read_json_file(file);
+
+                       if (!mock || mock != mock) {
+                               mocklib.I("No stat result fixture defined for fs.stat() call on %s.", path);
+                               mocklib.I("Provide a mock result through the following JSON file:\n%s\n", file);
+
+                               if (match(path, /\/$/))
+                                       mock = { type: "directory" };
+                               else
+                                       mock = { type: "file" };
+                       }
+
+                       mocklib.trace_call("fs", "stat", { path });
+
+                       return mock;
+               },
+
+               unlink: function(path) {
+                       printf("fs.unlink() path <%s>\n", path);
+
+                       return true;
+               },
+
+               popen: (cmdline, mode) => {
+                       let read = (!mode || index(mode, "r") != -1),
+                           path = sprintf("fs/popen~%s.txt", replace(cmdline, /[^A-Za-z0-9_-]+/g, '_')),
+                           mock = mocklib.read_data_file(path);
+
+                       if (read && !mock) {
+                               mocklib.I("No stdout fixture defined for fs.popen() command %s.", cmdline);
+                               mocklib.I("Provide a mock output through the following text file:\n%s\n", path);
+
+                               return null;
+                       }
+
+                       printf("fs.popen() cmdline <%s> mode <%s>\n", 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("fs/open~%s.txt", replace(fpath, /[^A-Za-z0-9_-]+/g, '_')),
+                           mock = read ? mocklib.read_data_file(path) : null;
+
+                       if (read && !mock) {
+                               mocklib.I("No stdout fixture defined for fs.open() path %s.", fpath);
+                               mocklib.I("Provide a mock output through the following text file:\n%s\n", path);
+
+                               return null;
+                       }
+
+                       mocklib.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"
+       };
diff --git a/tests/lib/mocklib/ubus.uc b/tests/lib/mocklib/ubus.uc
new file mode 100644 (file)
index 0000000..b56f727
--- /dev/null
@@ -0,0 +1,69 @@
+{%
+       let mocklib = global.mocklib;
+
+       return {
+               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("ubus/%s.json", join("~", signature)),
+                                                   mock = mocklib.read_json_file(path);
+
+                                               if (mock != mock) {
+                                                       self._error = "Invalid argument";
+
+                                                       return null;
+                                               }
+                                               else if (mock) {
+                                                       mocklib.trace_call("ctx", "call", { object, method, args });
+
+                                                       return mock;
+                                               }
+
+                                               push(candidates, path);
+                                               pop(signature);
+                                       }
+
+                                       mocklib.I("No response fixture defined for ubus call %s/%s with arguments %s.", object, method, args);
+                                       mocklib.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;
+               }
+       };
diff --git a/tests/lib/mocklib/uci.uc b/tests/lib/mocklib/uci.uc
new file mode 100644 (file)
index 0000000..542eeb8
--- /dev/null
@@ -0,0 +1,148 @@
+{%
+       let mocklib = global.mocklib;
+
+       let hash = (s) => {
+               let h = 7;
+
+               for (let i = 0; i < length(s); i++)
+                       h = h * 31 + ord(s, i)[0];
+
+               return h;
+       };
+
+       let id = (config, t, n) => {
+               while (true) {
+                       let id = sprintf('cfg%08x', hash(t + n));
+
+                       if (!exists(config, id))
+                               return id;
+
+                       n++;
+               }
+       };
+
+       let fixup_config = (config) => {
+               let rv = {};
+
+               for (let stype in config) {
+                       switch (type(config[stype])) {
+                       case 'object':
+                               config[stype] = [ config[stype] ];
+                               /* fall through */
+
+                       case 'array':
+                               for (let idx, sobj in config[stype]) {
+                                       let sid, anon;
+
+                                       if (exists(sobj, '.name') && !exists(rv, sobj['.name'])) {
+                                               sid = sobj['.name'];
+                                               anon = false;
+                                       }
+                                       else {
+                                               sid = id(rv, stype, idx);
+                                               anon = true;
+                                       }
+
+                                       rv[sid] = {
+                                               '.index': n_section++,
+                                               ...sobj,
+                                               '.name': sid,
+                                               '.type': stype,
+                                               '.anonymous': anon
+                                       };
+                               }
+
+                               break;
+                       }
+               }
+
+               for (let n, sid in sort(keys(rv), (a, b) => rv[a]['.index'] - rv[b]['.index']))
+                       rv[sid]['.index'] = n;
+
+               return rv;
+       };
+
+       return {
+               cursor: () => ({
+                       _configs: {},
+
+                       load: function(file) {
+                               let basename = replace(file, /^.+\//, ''),
+                                   path = sprintf("uci/%s.json", basename),
+                                   mock = mocklib.read_json_file(path);
+
+                               if (!mock || mock != mock) {
+                                       mocklib.I("No configuration fixture defined for uci package %s.", file);
+                                       mocklib.I("Provide a mock configuration through the following JSON file:\n%s\n", path);
+
+                                       return null;
+                               }
+
+                               this._configs[basename] = fixup_config(mock);
+                       },
+
+                       _get_section: function(config, section) {
+                               if (!exists(this._configs, config)) {
+                                       this.load(config);
+
+                                       if (!exists(this._configs, config))
+                                               return null;
+                               }
+
+                               let cfg = this._configs[config],
+                                   extended = match(section, "^@([A-Za-z0-9_-]+)\[(-?[0-9]+)\]$");
+
+                               if (extended) {
+                                       let stype = extended[1],
+                                           sindex = +extended[2];
+
+                                       let sids = sort(
+                                               filter(keys(cfg), sid => cfg[sid]['.type'] == stype),
+                                               (a, b) => cfg[a]['.index'] - cfg[b]['.index']
+                                       );
+
+                                       if (sindex < 0)
+                                               sindex = sids.length + sindex;
+
+                                       return cfg[sids[sindex]];
+                               }
+
+                               return cfg[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 cfg = this._configs[config],
+                                           sids = sort(keys(cfg), (a, b) => cfg[a]['.index'] - cfg[b]['.index']);
+
+                                       for (let i, sid in sids) {
+                                               if (stype == null || cfg[sid]['.type'] == stype) {
+                                                       if (cb({ ...(cfg[sid]) }) === false)
+                                                               break;
+
+                                                       rv = true;
+                                               }
+                                       }
+                               }
+
+                               return rv;
+                       }
+               })
+       };
diff --git a/tests/mock.uc b/tests/mock.uc
deleted file mode 100644 (file)
index 7a5da39..0000000
+++ /dev/null
@@ -1,479 +0,0 @@
-{%
-       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);
index a7e7720a4deca784bf012adef8b1c25583926b33..3a203b604356b9f2744a2a6e8dbf459bec8c79a8 100644 (file)
 {
-   "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"
-   }
+       "defaults": {
+               "forward": "REJECT",
+               "input": "ACCEPT",
+               "output": "ACCEPT",
+               "syn_flood": "1"
+       },
+       "zone": [
+               {
+                       "name": "lan",
+                       "input": "ACCEPT",
+                       "output": "ACCEPT",
+                       "forward": "ACCEPT",
+                       "network": [ "lan" ]
+               },
+               {
+                       "input": "REJECT",
+                       "output": "ACCEPT",
+                       "forward": "REJECT",
+                       "masq": "1",
+                       "mtu_fix": "1",
+                       "name": "wan",
+                       "network": [ "wan", "wan6" ]
+               }
+       ],
+       "forwarding": {
+               "dest": "wan",
+               "src": "lan"
+       },
+       "rule": [
+               {
+                       "name": "Allow-DHCP-Renew",
+                       "family": "ipv4",
+                       "proto": "udp",
+                       "src": "wan",
+                       "dest_port": "68",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-Ping",
+                       "family": "ipv4",
+                       "proto": "icmp",
+                       "src": "wan",
+                       "icmp_type": "echo-request",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-IGMP",
+                       "family": "ipv4",
+                       "proto": "igmp",
+                       "src": "wan",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-DHCPv6",
+                       "family": "ipv6",
+                       "proto": "udp",
+                       "src": "wan",
+                       "src_ip": "fc00::/6",
+                       "dest_ip": "fc00::/6",
+                       "dest_port": "546",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-MLD",
+                       "family": "ipv6",
+                       "proto": "icmp",
+                       "src": "wan",
+                       "src_ip": "fe80::/10",
+                       "icmp_type": [ "130/0", "131/0", "132/0", "143/0" ],
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-ICMPv6-Input",
+                       "family": "ipv6",
+                       "proto": "icmp",
+                       "src": "wan",
+                       "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",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-ICMPv6-Forward",
+                       "family": "ipv6",
+                       "proto": "icmp",
+                       "src": "wan",
+                       "dest": "*",
+                       "icmp_type": [
+                               "echo-request", "echo-reply", "destination-unreachable",
+                               "packet-too-big", "time-exceeded", "bad-header", "unknown-header-type"
+                       ],
+                       "limit": "1000/sec",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-IPSec-ESP",
+                       "proto": "esp",
+                       "src": "wan",
+                       "dest": "lan",
+                       "target": "ACCEPT"
+               },
+               {
+                       "name": "Allow-ISAKMP",
+                       "proto": "udp",
+                       "src": "wan",
+                       "dest": "lan",
+                       "dest_port": "500",
+                       "target": "ACCEPT"
+               }
+       ]
 }
index 453901d153541607409c9c51757cf2078ac02db9..296941cb1278a56c6f8ecd6546bf9cdafb65887a 100644 (file)
 {
-   "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"
-   }
+       "helper": [
+               {
+                       "name": "amanda",
+                       "description": "Amanda backup and archiving proto",
+                       "family": "any",
+                       "module": "nf_conntrack_amanda",
+                       "port": "10080",
+                       "proto": "udp"
+               },
+               {
+                       "name": "ftp",
+                       "description": "FTP passive connection tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_ftp",
+                       "port": "21",
+                       "proto": "tcp"
+               },
+               {
+                       "name": "RAS",
+                       "description": "RAS proto tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_h323",
+                       "port": "1719",
+                       "proto": "udp"
+               },
+               {
+                       "name": "Q.931",
+                       "description": "Q.931 proto tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_h323",
+                       "port": "1720",
+                       "proto": "tcp"
+               },
+               {
+                       "name": "irc",
+                       "description": "IRC DCC connection tracking",
+                       "family": "ipv4",
+                       "module": "nf_conntrack_irc",
+                       "port": "6667",
+                       "proto": "tcp"
+               },
+               {
+                       "name": "netbios-ns",
+                       "description": "NetBIOS name service broadcast tracking",
+                       "family": "ipv4",
+                       "module": "nf_conntrack_netbios_ns",
+                       "port": "137",
+                       "proto": "udp"
+               },
+               {
+                       "name": "pptp",
+                       "description": "PPTP VPN connection tracking",
+                       "family": "ipv4",
+                       "module": "nf_conntrack_pptp",
+                       "port": "1723",
+                       "proto": "tcp"
+               },
+               {
+                       "name": "sane",
+                       "description": "SANE scanner connection tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_sane",
+                       "port": "6566",
+                       "proto": "tcp"
+               },
+               {
+                       "name": "sip",
+                       "description": "SIP VoIP connection tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_sip",
+                       "port": "5060",
+                       "proto": "udp"
+               },
+               {
+                       "name": "snmp",
+                       "description": "SNMP monitoring connection tracking",
+                       "family": "ipv4",
+                       "module": "nf_conntrack_snmp",
+                       "port": "161",
+                       "proto": "udp"
+               },
+               {
+                       "name": "tftp",
+                       "description": "TFTP connection tracking",
+                       "family": "any",
+                       "module": "nf_conntrack_tftp",
+                       "port": "69",
+                       "proto": "udp"
+               },
+               {
+                       "name": "rtsp",
+                       "description": "RTSP connection tracking",
+                       "family": "ipv4",
+                       "module": "nf_conntrack_rtsp",
+                       "port": "554",
+                       "proto": "tcp"
+               }
+       ]
 }
diff --git a/tests/test-wrapper.uc b/tests/test-wrapper.uc
deleted file mode 100644 (file)
index a5412ae..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-{%
-       fw4 = require("fw4");
-
-       include("../root/usr/share/firewall4/main.uc");
-%}