mirror of
https://github.com/openwrt/packages.git
synced 2026-10-12 14:37:29 +04:00
Maintainer: me
Compile tested: x86_64, Dell EMC Edge620, OpenWrt 25.12.4
Run tested: x86_64, Dell EMC Edge620, OpenWrt 25.12.4
Description:
Update to 2026.05.06 and add IP family option
- Update PKG_VERSION to 2026.05.06
- Update PKG_RELEASE to 1
- Add PKG_CPE_ID for CVE tracking
- Update PKG_MIRROR_HASH and PKG_SOURCE_VERSION
files/etc/config/https-dns-proxy:
- Add comments for default options
- Add new 'force_ip_family' option to control IP family for resolvers
- Update default 'bootstrap_dns' to include IPv6 addresses
files/etc/init.d/https-dns-proxy:
- Add DEFAULT_BOOTSTRAP4 and DEFAULT_BOOTSTRAP6 for family fallbacks
- Change 'global_force_ipv6' to 'global_force_ip_family' for new option
- Modify append_boot to filter bootstrap DNS by 'force_ip_family'
- Implement fallback bootstrap DNS for forced IP families with no
compatible configured servers
- Change 'start_instance' to use 'force_ip_family' instead of
'force_ipv6'
- Ensure 'service_started' and 'service_stopped' always return 0 to
prevent incorrect failure reports for `start`/`reload`/`restart`
commands
files/etc/uci-defaults/50-https-dns-proxy-migrate-options.sh:
- Migrate 'force_ipv6_resolvers' option to 'force_ip_family' option
- Remove old 'force_ipv6_resolvers' option after migration
Signed-off-by: Stan Grishin <stangri@melmac.ca>
833 lines
29 KiB
Bash
833 lines
29 KiB
Bash
#!/usr/bin/env bash
|
|
# Functional test runner for https-dns-proxy init script.
|
|
#
|
|
# Tests helper functions, validation logic, dnsmasq integration,
|
|
# and UCI migration by mocking OpenWrt's rc.common framework.
|
|
#
|
|
# Usage: cd source.mossdef.org/https-dns-proxy && bash tests/run_tests.sh
|
|
|
|
set -o pipefail
|
|
|
|
line='........................................'
|
|
n_tests=0
|
|
n_fails=0
|
|
|
|
pass() {
|
|
printf " PASS: %s\n" "$1"
|
|
}
|
|
fail() {
|
|
printf " FAIL: %s (expected: '%s', got: '%s')\n" "$1" "$2" "$3"
|
|
n_fails=$((n_fails + 1))
|
|
}
|
|
assert_rc() {
|
|
local desc="$1" expect="$2" actual="$3"
|
|
n_tests=$((n_tests + 1))
|
|
if [ "$expect" -eq "$actual" ]; then
|
|
pass "$desc"
|
|
else
|
|
fail "$desc" "$expect" "$actual"
|
|
fi
|
|
}
|
|
assert_eq() {
|
|
local desc="$1" expect="$2" actual="$3"
|
|
n_tests=$((n_tests + 1))
|
|
if [ "$expect" = "$actual" ]; then
|
|
pass "$desc"
|
|
else
|
|
fail "$desc" "$expect" "$actual"
|
|
fi
|
|
}
|
|
|
|
# ── Mock OpenWrt rc.common framework ─────────────────────────────────
|
|
|
|
TESTDIR="/tmp/hdp_test.$$"
|
|
mkdir -p "$TESTDIR/config" "$TESTDIR/proc"
|
|
trap "rm -rf '$TESTDIR'" EXIT
|
|
|
|
# Provide empty stubs for procd/rc.common functions that the script
|
|
# calls at source time or that we don't need during unit tests
|
|
extra_command() { :; }
|
|
rc_procd() { :; }
|
|
service_started() { :; }
|
|
service_stopped() { :; }
|
|
procd_open_instance() { :; }
|
|
procd_set_param() { :; }
|
|
procd_close_instance() { :; }
|
|
procd_open_data() { :; }
|
|
procd_close_data() { :; }
|
|
procd_add_mdns_service() { :; }
|
|
procd_add_interface_trigger() { :; }
|
|
procd_add_raw_trigger() { :; }
|
|
procd_add_config_trigger() { :; }
|
|
procd_set_config_changed() { :; }
|
|
json_add_object() { :; }
|
|
json_add_string() { :; }
|
|
json_add_int() { :; }
|
|
json_add_boolean() { :; }
|
|
json_add_array() { :; }
|
|
json_close_object() { :; }
|
|
json_close_array() { :; }
|
|
|
|
# ── Mock UCI backend ────────────────────────────────────────────────
|
|
# Stores config in flat files under $TESTDIR/config/
|
|
|
|
__uci_store="$TESTDIR/config"
|
|
|
|
_uci_file() { echo "$__uci_store/${1}__${2}__${3}"; }
|
|
|
|
uci_get() {
|
|
local pkg="$1" sec="$2" opt="$3" def="$4"
|
|
local f
|
|
if [ -z "$opt" ]; then
|
|
# No option → return section type (OpenWrt convention)
|
|
f="$(_uci_file "$pkg" "$sec" ".type")"
|
|
else
|
|
f="$(_uci_file "$pkg" "$sec" "$opt")"
|
|
fi
|
|
if [ -f "$f" ]; then
|
|
cat "$f"
|
|
else
|
|
[ -n "$def" ] && echo "$def"
|
|
fi
|
|
}
|
|
|
|
uci_set() {
|
|
local pkg="$1" sec="$2" opt="$3" val="$4"
|
|
local f
|
|
if [ -n "$opt" ]; then
|
|
f="$(_uci_file "$pkg" "$sec" "$opt")"
|
|
else
|
|
f="$(_uci_file "$pkg" "$sec" ".type")"
|
|
val="$opt"
|
|
fi
|
|
printf '%s' "$val" > "$f"
|
|
}
|
|
|
|
uci_add_list() {
|
|
local pkg="$1" sec="$2" opt="$3" val="$4"
|
|
local f="$(_uci_file "$pkg" "$sec" "$opt")"
|
|
if [ -s "$f" ]; then
|
|
printf '%s' "$(cat "$f") $val" > "$f"
|
|
else
|
|
printf '%s' "$val" > "$f"
|
|
fi
|
|
}
|
|
|
|
uci_remove_list() {
|
|
local pkg="$1" sec="$2" opt="$3" val="$4"
|
|
local f="$(_uci_file "$pkg" "$sec" "$opt")"
|
|
[ -f "$f" ] || return 0
|
|
local cur new=""
|
|
cur="$(cat "$f")"
|
|
for i in $cur; do
|
|
[ "$i" = "$val" ] && continue
|
|
new="${new:+$new }$i"
|
|
done
|
|
printf '%s' "$new" > "$f"
|
|
}
|
|
|
|
uci_remove() {
|
|
local pkg="$1" sec="$2" opt="$3"
|
|
if [ -n "$opt" ]; then
|
|
rm -f "$(_uci_file "$pkg" "$sec" "$opt")"
|
|
else
|
|
rm -f "$__uci_store/${pkg}__${sec}__"*
|
|
fi
|
|
}
|
|
|
|
uci_commit() { return 0; }
|
|
|
|
# config_load / config_get / config_get_bool / config_foreach
|
|
# Simplified mocks that delegate to uci_get
|
|
|
|
config_load() { __cfg_package="$1"; }
|
|
|
|
config_get() {
|
|
local var="$1" sec="$2" opt="$3" def="$4"
|
|
local val
|
|
val="$(uci_get "$__cfg_package" "$sec" "$opt" "$def")"
|
|
eval "$var=\"\$val\""
|
|
}
|
|
|
|
config_get_bool() {
|
|
local var="$1" sec="$2" opt="$3" def="$4"
|
|
local val
|
|
val="$(uci_get "$__cfg_package" "$sec" "$opt" "$def")"
|
|
eval "$var=\"\$val\""
|
|
}
|
|
|
|
# config_foreach: iterate named sections of a given type
|
|
# We track sections via .type marker files
|
|
__cfg_sections=""
|
|
config_foreach() {
|
|
local callback="$1" type="$2"
|
|
shift 2
|
|
local sec
|
|
for f in "$__uci_store/${__cfg_package}__"*__".type"; do
|
|
[ -f "$f" ] || continue
|
|
if [ "$(cat "$f")" = "$type" ]; then
|
|
sec="${f#$__uci_store/${__cfg_package}__}"
|
|
sec="${sec%%__*}"
|
|
"$callback" "$sec" "$@"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ── Mock network/system commands ─────────────────────────────────────
|
|
|
|
logger() { :; }
|
|
|
|
# Override ubus to return nothing (init script defines its own wrapper)
|
|
__UBUS_BIN="true"
|
|
|
|
# Mock `nft`: track invocations and return success by default. Tests can
|
|
# override __nft_rc to simulate failure.
|
|
__nft_calls_file="$TESTDIR/nft_calls"
|
|
__nft_rc=0
|
|
: > "$__nft_calls_file"
|
|
nft() {
|
|
printf '%s\n' "$*" >> "$__nft_calls_file"
|
|
return "$__nft_rc"
|
|
}
|
|
|
|
# ── Source the init script (skip the shebang line) ──────────────────
|
|
#
|
|
# Patch the readonly NOTRACK_NFT_FILE path so tests can write under
|
|
# $TESTDIR instead of /usr/share/nftables.d/ruleset-post/.
|
|
|
|
INIT_SCRIPT="./files/etc/init.d/https-dns-proxy"
|
|
if [ ! -f "$INIT_SCRIPT" ]; then
|
|
echo "ERROR: Cannot find $INIT_SCRIPT. Run from the https-dns-proxy package root."
|
|
exit 1
|
|
fi
|
|
|
|
PATCHED_INIT="$TESTDIR/https-dns-proxy.patched"
|
|
NOTRACK_TEST_FILE="$TESTDIR/usr/share/nftables.d/ruleset-post/20-https-dns-proxy-notrack.nft"
|
|
sed "s|^readonly NOTRACK_NFT_FILE=.*|readonly NOTRACK_NFT_FILE='$NOTRACK_TEST_FILE'|" \
|
|
"$INIT_SCRIPT" > "$PATCHED_INIT"
|
|
|
|
# Source all functions. The #!/bin/sh /etc/rc.common line is harmless
|
|
# when we've already defined the framework stubs above.
|
|
# shellcheck disable=SC1090
|
|
. "$PATCHED_INIT"
|
|
|
|
###############################################################################
|
|
# TEST CATEGORIES #
|
|
###############################################################################
|
|
|
|
printf "\n##\n## 01: Validation helper functions\n##\n\n"
|
|
|
|
# ── is_ipv4 ──
|
|
|
|
is_ipv4 "1.2.3.4"; assert_rc "is_ipv4 '1.2.3.4' → 0" 0 $?
|
|
is_ipv4 "192.168.1.1"; assert_rc "is_ipv4 '192.168.1.1' → 0" 0 $?
|
|
is_ipv4 "255.255.255.255"; assert_rc "is_ipv4 '255.255.255.255' → 0" 0 $?
|
|
is_ipv4 "0.0.0.0"; assert_rc "is_ipv4 '0.0.0.0' → 0" 0 $?
|
|
is_ipv4 "1.2.3"; assert_rc "is_ipv4 '1.2.3' (incomplete) → 1" 1 $?
|
|
is_ipv4 "abc.def.ghi.jkl"; assert_rc "is_ipv4 'abc.def.ghi.jkl' → 1" 1 $?
|
|
is_ipv4 "::1"; assert_rc "is_ipv4 '::1' (IPv6) → 1" 1 $?
|
|
is_ipv4 ""; assert_rc "is_ipv4 '' (empty) → 1" 1 $?
|
|
is_ipv4 "1.2.3.4.5"; assert_rc "is_ipv4 '1.2.3.4.5' (too many octets) → 1" 1 $?
|
|
|
|
# ── is_ipv6 ──
|
|
|
|
is_ipv6 "2606:4700:4700::1111"; assert_rc "is_ipv6 '2606:4700:4700::1111' → 0" 0 $?
|
|
is_ipv6 "::1"; assert_rc "is_ipv6 '::1' → 0" 0 $?
|
|
is_ipv6 "fe80::1"; assert_rc "is_ipv6 'fe80::1' → 0" 0 $?
|
|
is_ipv6 "1.2.3.4"; assert_rc "is_ipv6 '1.2.3.4' (IPv4) → 1" 1 $?
|
|
is_ipv6 "hello"; assert_rc "is_ipv6 'hello' (no colon) → 1" 1 $?
|
|
is_ipv6 ""; assert_rc "is_ipv6 '' (empty) → 1" 1 $?
|
|
# MAC addresses also contain colons — is_ipv6 must reject them
|
|
is_ipv6 "AA:BB:CC:DD:EE:FF"; assert_rc "is_ipv6 'AA:BB:CC:DD:EE:FF' (MAC) → 1" 1 $?
|
|
|
|
# ── is_mac_address ──
|
|
|
|
is_mac_address "AA:BB:CC:DD:EE:FF"; assert_rc "is_mac_address 'AA:BB:CC:DD:EE:FF' → 0" 0 $?
|
|
is_mac_address "00:11:22:33:44:55"; assert_rc "is_mac_address '00:11:22:33:44:55' → 0" 0 $?
|
|
is_mac_address "aa:bb:cc:dd:ee:ff"; assert_rc "is_mac_address lowercase → 1" 1 $?
|
|
is_mac_address "1.2.3.4"; assert_rc "is_mac_address '1.2.3.4' (IPv4) → 1" 1 $?
|
|
is_mac_address "AABBCCDDEEFF"; assert_rc "is_mac_address no separators → 1" 1 $?
|
|
is_mac_address ""; assert_rc "is_mac_address '' (empty) → 1" 1 $?
|
|
|
|
# ── is_integer ──
|
|
|
|
is_integer "1"; assert_rc "is_integer '1' → 0" 0 $?
|
|
is_integer "53"; assert_rc "is_integer '53' → 0" 0 $?
|
|
is_integer "5053"; assert_rc "is_integer '5053' → 0" 0 $?
|
|
is_integer "65535"; assert_rc "is_integer '65535' → 0" 0 $?
|
|
is_integer "0"; assert_rc "is_integer '0' (below range) → 1" 1 $?
|
|
is_integer "65536"; assert_rc "is_integer '65536' (above range) → 1" 1 $?
|
|
is_integer "abc"; assert_rc "is_integer 'abc' → 1" 1 $?
|
|
is_integer ""; assert_rc "is_integer '' (empty) → 1" 1 $?
|
|
is_integer "12abc"; assert_rc "is_integer '12abc' (mixed) → 1" 1 $?
|
|
is_integer "-1"; assert_rc "is_integer '-1' (negative) → 1" 1 $?
|
|
|
|
# ── is_alnum ──
|
|
|
|
is_alnum "hello"; assert_rc "is_alnum 'hello' → 0" 0 $?
|
|
is_alnum "test_123"; assert_rc "is_alnum 'test_123' → 0" 0 $?
|
|
is_alnum "with space"; assert_rc "is_alnum 'with space' → 0" 0 $?
|
|
is_alnum "with@at"; assert_rc "is_alnum 'with@at' → 0" 0 $?
|
|
is_alnum ""; assert_rc "is_alnum '' (empty) → 1" 1 $?
|
|
is_alnum "no/slash"; assert_rc "is_alnum 'no/slash' → 1" 1 $?
|
|
is_alnum "no;semi"; assert_rc "is_alnum 'no;semi' → 1" 1 $?
|
|
|
|
# ── str_contains ──
|
|
|
|
str_contains "hello world" "world"; assert_rc "str_contains 'hello world' 'world' → 0" 0 $?
|
|
str_contains "hello world" "xyz"; assert_rc "str_contains 'hello world' 'xyz' → 1" 1 $?
|
|
str_contains "abc:def" ":"; assert_rc "str_contains 'abc:def' ':' → 0" 0 $?
|
|
|
|
# ── str_contains_word ──
|
|
|
|
str_contains_word "53 853 5353" "53"; assert_rc "str_contains_word finds exact word '53' → 0" 0 $?
|
|
str_contains_word "53 853 5353" "853"; assert_rc "str_contains_word finds exact word '853' → 0" 0 $?
|
|
str_contains_word "53 853 5353" "35"; assert_rc "str_contains_word rejects non-word '35' → 1" 1 $?
|
|
|
|
# ── version ──
|
|
|
|
actual_ver="$(version)"
|
|
assert_eq "version returns PKG_VERSION" "dev-test" "$actual_ver"
|
|
|
|
printf "\n##\n## 02: UCI helper functions\n##\n\n"
|
|
|
|
# ── uci_add_list_if_new ──
|
|
|
|
# Reset state
|
|
rm -f "$__uci_store"/*
|
|
|
|
uci_add_list_if_new "dhcp" "cfg01" "server" "127.0.0.1#5053"
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
assert_eq "uci_add_list_if_new adds first value" "127.0.0.1#5053" "$val"
|
|
|
|
uci_add_list_if_new "dhcp" "cfg01" "server" "127.0.0.1#5054"
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
assert_eq "uci_add_list_if_new adds second value" "127.0.0.1#5053 127.0.0.1#5054" "$val"
|
|
|
|
uci_add_list_if_new "dhcp" "cfg01" "server" "127.0.0.1#5053"
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
assert_eq "uci_add_list_if_new skips duplicate" "127.0.0.1#5053 127.0.0.1#5054" "$val"
|
|
|
|
# ── uci_add_list_if_new with missing params ──
|
|
|
|
uci_add_list_if_new "" "cfg01" "server" "val"
|
|
assert_rc "uci_add_list_if_new rejects empty PACKAGE" 1 $?
|
|
|
|
uci_add_list_if_new "pkg" "" "server" "val"
|
|
assert_rc "uci_add_list_if_new rejects empty CONFIG" 1 $?
|
|
|
|
uci_add_list_if_new "pkg" "cfg" "" "val"
|
|
assert_rc "uci_add_list_if_new rejects empty OPTION" 1 $?
|
|
|
|
uci_add_list_if_new "pkg" "cfg" "opt" ""
|
|
assert_rc "uci_add_list_if_new rejects empty VALUE" 1 $?
|
|
|
|
printf "\n##\n## 03: dnsmasq_doh_server function\n##\n\n"
|
|
|
|
# Reset state
|
|
rm -f "$__uci_store"/*
|
|
__cfg_package="dhcp"
|
|
|
|
# Set up a dnsmasq section
|
|
uci_set "dhcp" "cfg01" ".type" "dnsmasq"
|
|
|
|
# ── add mode: basic DoH server entry ──
|
|
|
|
canaryDomains=""
|
|
force_dns=""
|
|
dnsmasq_doh_server "cfg01" "add" "127.0.0.1" "5053"
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
assert_eq "doh_server add: server list contains 127.0.0.1#5053" "127.0.0.1#5053" "$val"
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "doh_server")"
|
|
assert_eq "doh_server add: doh_server list contains 127.0.0.1#5053" "127.0.0.1#5053" "$val"
|
|
|
|
# ── add mode: second instance ──
|
|
|
|
dnsmasq_doh_server "cfg01" "add" "127.0.0.1" "5054"
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
assert_eq "doh_server add: server list has both" "127.0.0.1#5053 127.0.0.1#5054" "$val"
|
|
|
|
# ── add mode: with canary domains ──
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg02" ".type" "dnsmasq"
|
|
force_dns="1"
|
|
canaryDomains="mask.icloud.com mask-h2.icloud.com use-application-dns.net"
|
|
dnsmasq_doh_server "cfg02" "add" "127.0.0.1" "5053"
|
|
|
|
val="$(uci_get "dhcp" "cfg02" "server")"
|
|
echo "$val" | grep -q "/mask.icloud.com/"
|
|
assert_rc "doh_server add with canary: iCloud canary in server list" 0 $?
|
|
echo "$val" | grep -q "/use-application-dns.net/"
|
|
assert_rc "doh_server add with canary: Mozilla canary in server list" 0 $?
|
|
echo "$val" | grep -q "127.0.0.1#5053"
|
|
assert_rc "doh_server add with canary: DoH server in server list" 0 $?
|
|
|
|
# ── add mode: address normalization ──
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg03" ".type" "dnsmasq"
|
|
force_dns=""
|
|
canaryDomains=""
|
|
dnsmasq_doh_server "cfg03" "add" "0.0.0.0" "5053"
|
|
|
|
val="$(uci_get "dhcp" "cfg03" "server")"
|
|
assert_eq "doh_server add: 0.0.0.0 normalized to 127.0.0.1" "127.0.0.1#5053" "$val"
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg04" ".type" "dnsmasq"
|
|
dnsmasq_doh_server "cfg04" "add" "::" "5053"
|
|
|
|
val="$(uci_get "dhcp" "cfg04" "server")"
|
|
assert_eq "doh_server add: :: normalized to ::1" "::1#5053" "$val"
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg05" ".type" "dnsmasq"
|
|
dnsmasq_doh_server "cfg05" "add" "::ffff:0.0.0.0" "5053"
|
|
|
|
val="$(uci_get "dhcp" "cfg05" "server")"
|
|
assert_eq "doh_server add: ::ffff:0.0.0.0 normalized to 127.0.0.1" "127.0.0.1#5053" "$val"
|
|
|
|
# ── remove mode ──
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg06" ".type" "dnsmasq"
|
|
canaryDomains="mask.icloud.com use-application-dns.net"
|
|
force_dns="1"
|
|
dnsmasq_doh_server "cfg06" "add" "127.0.0.1" "5053"
|
|
dnsmasq_doh_server "cfg06" "add" "127.0.0.1" "5054"
|
|
|
|
# Now remove
|
|
dnsmasq_doh_server "cfg06" "remove"
|
|
|
|
val="$(uci_get "dhcp" "cfg06" "server")"
|
|
echo "$val" | grep -q "127.0.0.1#5053"
|
|
assert_rc "doh_server remove: 127.0.0.1#5053 removed from server" 1 $?
|
|
echo "$val" | grep -q "127.0.0.1#5054"
|
|
assert_rc "doh_server remove: 127.0.0.1#5054 removed from server" 1 $?
|
|
|
|
# ── non-dnsmasq section rejected ──
|
|
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "badcfg" ".type" "other"
|
|
dnsmasq_doh_server "badcfg" "add" "127.0.0.1" "5053"
|
|
assert_rc "doh_server rejects non-dnsmasq section" 1 $?
|
|
|
|
printf "\n##\n## 04: dhcp_backup create/restore\n##\n\n"
|
|
|
|
# Reset state
|
|
rm -f "$__uci_store"/*
|
|
__cfg_package="dhcp"
|
|
|
|
# Set up initial dnsmasq state with existing servers
|
|
uci_set "dhcp" "cfg01" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg01" "server" "8.8.8.8 8.8.4.4"
|
|
uci_set "dhcp" "cfg01" "port" "53"
|
|
|
|
# Set package config
|
|
dnsmasq_config_update="*"
|
|
canaryDomains=""
|
|
force_dns=""
|
|
|
|
# Create backup
|
|
dhcp_backup 'create'
|
|
|
|
# Verify backup was created
|
|
val="$(uci_get "dhcp" "cfg01" "doh_backup_server")"
|
|
assert_eq "dhcp_backup create: backup contains original servers" "8.8.8.8 8.8.4.4" "$val"
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "noresolv")"
|
|
assert_eq "dhcp_backup create: noresolv set to 1" "1" "$val"
|
|
|
|
# Original plain servers should be removed (only canary/DoH servers remain)
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
echo "$val" | grep -q "8.8.8.8"
|
|
assert_rc "dhcp_backup create: original plain server 8.8.8.8 removed" 1 $?
|
|
|
|
# Restore backup
|
|
dhcp_backup 'restore'
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "server")"
|
|
echo "$val" | grep -q "8.8.8.8"
|
|
assert_rc "dhcp_backup restore: server 8.8.8.8 restored" 0 $?
|
|
|
|
# Backup markers should be cleaned up
|
|
val="$(uci_get "dhcp" "cfg01" "doh_backup_server")"
|
|
assert_eq "dhcp_backup restore: backup marker removed" "" "$val"
|
|
|
|
printf "\n##\n## 05: dhcp_backup with noresolv states\n##\n\n"
|
|
|
|
# Test: noresolv was not set originally → backup stores -1
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg01" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg01" "port" "53"
|
|
dnsmasq_config_update="*"
|
|
|
|
dhcp_backup 'create'
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "doh_backup_noresolv")"
|
|
assert_eq "dhcp_backup: noresolv not set → backup is -1" "-1" "$val"
|
|
|
|
dhcp_backup 'restore'
|
|
|
|
# noresolv should be removed (was not originally set)
|
|
val="$(uci_get "dhcp" "cfg01" "noresolv")"
|
|
assert_eq "dhcp_backup restore: noresolv removed when backup was -1" "" "$val"
|
|
|
|
# Test: noresolv was already set to 1
|
|
rm -f "$__uci_store"/*
|
|
uci_set "dhcp" "cfg01" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg01" "noresolv" "1"
|
|
uci_set "dhcp" "cfg01" "port" "53"
|
|
|
|
dhcp_backup 'create'
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "doh_backup_noresolv")"
|
|
assert_eq "dhcp_backup: noresolv=1 → backup is 1" "1" "$val"
|
|
|
|
dhcp_backup 'restore'
|
|
|
|
val="$(uci_get "dhcp" "cfg01" "noresolv")"
|
|
assert_eq "dhcp_backup restore: noresolv=1 preserved" "1" "$val"
|
|
|
|
printf "\n##\n## 06: dnsmasq_instance_append_force_dns_port\n##\n\n"
|
|
|
|
rm -f "$__uci_store"/*
|
|
__cfg_package="dhcp"
|
|
|
|
uci_set "dhcp" "cfg01" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg01" "port" "53"
|
|
force_dns_port="53 853"
|
|
|
|
dnsmasq_instance_append_force_dns_port "cfg01"
|
|
assert_eq "append_force_dns_port: already present port 53 not duplicated" "53 853" "$force_dns_port"
|
|
|
|
uci_set "dhcp" "cfg03" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg03" "port" "0"
|
|
dnsmasq_instance_append_force_dns_port "cfg03"
|
|
assert_eq "append_force_dns_port: disabled dnsmasq port 0 ignored" "53 853" "$force_dns_port"
|
|
|
|
uci_set "dhcp" "cfg02" ".type" "dnsmasq"
|
|
uci_set "dhcp" "cfg02" "port" "5353"
|
|
dnsmasq_instance_append_force_dns_port "cfg02"
|
|
assert_eq "append_force_dns_port: new port 5353 appended" "53 853 5353" "$force_dns_port"
|
|
|
|
# Non-dnsmasq type should fail
|
|
uci_set "dhcp" "badcfg" ".type" "other"
|
|
dnsmasq_instance_append_force_dns_port "badcfg"
|
|
assert_rc "append_force_dns_port: rejects non-dnsmasq section" 1 $?
|
|
|
|
printf "\n##\n## 07: append_parm / append_bool / xappend\n##\n\n"
|
|
|
|
# Test xappend
|
|
PROG_param=""
|
|
xappend "-r https://dns.google/dns-query"
|
|
assert_eq "xappend adds parameter" " -r https://dns.google/dns-query" "$PROG_param"
|
|
xappend "-p 5053"
|
|
assert_eq "xappend appends parameter" " -r https://dns.google/dns-query -p 5053" "$PROG_param"
|
|
|
|
printf "\n##\n## 07b: append_boot (force_ip_family)\n##\n\n"
|
|
|
|
rm -f "$__uci_store"/*
|
|
__cfg_package="https-dns-proxy"
|
|
uci_set "https-dns-proxy" "inst" "bootstrap_dns" "1.1.1.1,2606:4700:4700::1111"
|
|
|
|
# auto: keep both families, no -4
|
|
PROG_param=""; force_ip_family="auto"
|
|
append_boot "inst" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
|
|
assert_eq "append_boot auto keeps both families, no -4" " -b 1.1.1.1,2606:4700:4700::1111" "$PROG_param"
|
|
|
|
# ipv4: v4 only + -4
|
|
PROG_param=""; force_ip_family="ipv4"
|
|
append_boot "inst" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
|
|
assert_eq "append_boot ipv4 keeps v4 and adds -4" " -b 1.1.1.1 -4" "$PROG_param"
|
|
|
|
# ipv6: v6 only, no -4
|
|
PROG_param=""; force_ip_family="ipv6"
|
|
append_boot "inst" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
|
|
assert_eq "append_boot ipv6 keeps v6, no -4" " -b 2606:4700:4700::1111" "$PROG_param"
|
|
|
|
# forced ipv6 but config bootstrap has only v4 → fall back to CF/Google v6
|
|
uci_set "https-dns-proxy" "v4only" "bootstrap_dns" "1.1.1.1,8.8.8.8"
|
|
PROG_param=""; force_ip_family="ipv6"
|
|
append_boot "v4only" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
|
|
assert_eq "append_boot ipv6 falls back to CF/Google v6" \
|
|
" -b 2606:4700:4700::1111,2606:4700:4700::1001,2001:4860:4860::8888,2001:4860:4860::8844" "$PROG_param"
|
|
|
|
# forced ipv4 but config bootstrap has only v6 → fall back to CF/Google v4 + -4
|
|
uci_set "https-dns-proxy" "v6only" "bootstrap_dns" "2606:4700:4700::1111"
|
|
PROG_param=""; force_ip_family="ipv4"
|
|
append_boot "v6only" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP"
|
|
assert_eq "append_boot ipv4 falls back to CF/Google v4 and adds -4" \
|
|
" -b 1.1.1.1,1.0.0.1,8.8.8.8,8.8.4.4 -4" "$PROG_param"
|
|
|
|
unset force_ip_family
|
|
rm -f "$__uci_store"/*
|
|
|
|
printf "\n##\n## 08: UCI migration script\n##\n\n"
|
|
|
|
MIGRATION_SCRIPT="./files/etc/uci-defaults/50-https-dns-proxy-migrate-options.sh"
|
|
if [ -f "$MIGRATION_SCRIPT" ]; then
|
|
# Create a test config with old option names
|
|
MIGRATE_CONF="$TESTDIR/migrate_config"
|
|
cat > "$MIGRATE_CONF" << 'CONF'
|
|
config main 'config'
|
|
option update_dnsmasq_config '*'
|
|
option wan6_trigger '0'
|
|
option procd_fw_src_interfaces 'lan'
|
|
option use_http1 '0'
|
|
option use_ipv6_resolvers_only '1'
|
|
|
|
config https-dns-proxy 'disabled_family'
|
|
option force_ipv6_resolvers '0'
|
|
CONF
|
|
|
|
# Run the migration sed commands against our test file
|
|
sed -i "s|update_dnsmasq_config|dnsmasq_config_update|" "$MIGRATE_CONF"
|
|
sed -i "s|wan6_trigger|procd_trigger_wan6|" "$MIGRATE_CONF"
|
|
sed -i "s|procd_fw_src_interfaces|force_dns_src_interface|" "$MIGRATE_CONF"
|
|
sed -i "s|use_http1|force_http1|" "$MIGRATE_CONF"
|
|
sed -i "s|use_ipv6_resolvers_only|force_ipv6_resolvers|" "$MIGRATE_CONF"
|
|
sed -i "s|option force_ipv6_resolvers '1'|option force_ip_family 'ipv6'|" "$MIGRATE_CONF"
|
|
sed -i "/option force_ipv6_resolvers/d" "$MIGRATE_CONF"
|
|
|
|
grep -q "dnsmasq_config_update" "$MIGRATE_CONF"
|
|
assert_rc "migration: update_dnsmasq_config → dnsmasq_config_update" 0 $?
|
|
|
|
grep -q "procd_trigger_wan6" "$MIGRATE_CONF"
|
|
assert_rc "migration: wan6_trigger → procd_trigger_wan6" 0 $?
|
|
|
|
grep -q "force_dns_src_interface" "$MIGRATE_CONF"
|
|
assert_rc "migration: procd_fw_src_interfaces → force_dns_src_interface" 0 $?
|
|
|
|
grep -q "force_http1" "$MIGRATE_CONF"
|
|
assert_rc "migration: use_http1 → force_http1" 0 $?
|
|
|
|
grep -q "option force_ip_family 'ipv6'" "$MIGRATE_CONF"
|
|
assert_rc "migration: enabled force_ipv6_resolvers → force_ip_family=ipv6" 0 $?
|
|
|
|
grep -q "force_ipv6_resolvers" "$MIGRATE_CONF"
|
|
assert_rc "migration: force_ipv6_resolvers option retired" 1 $?
|
|
|
|
grep -qw "use_ipv6_resolvers_only" "$MIGRATE_CONF"
|
|
assert_rc "migration: old name use_ipv6_resolvers_only removed" 1 $?
|
|
|
|
[ "$(grep -c 'option force_ip_family' "$MIGRATE_CONF")" = "1" ]
|
|
assert_rc "migration: disabled force_ipv6_resolvers dropped (defaults to auto)" 0 $?
|
|
|
|
# Verify old names are gone
|
|
grep -q "update_dnsmasq_config" "$MIGRATE_CONF"
|
|
assert_rc "migration: old name update_dnsmasq_config removed" 1 $?
|
|
|
|
grep -q "wan6_trigger" "$MIGRATE_CONF"
|
|
# procd_trigger_wan6 contains wan6_trigger, so need exact match
|
|
grep -qw "wan6_trigger" "$MIGRATE_CONF"
|
|
assert_rc "migration: old name wan6_trigger removed (word match)" 1 $?
|
|
|
|
grep -q "use_http1" "$MIGRATE_CONF"
|
|
# force_http1 contains the chars but not the old prefix
|
|
grep -qw "use_http1" "$MIGRATE_CONF"
|
|
assert_rc "migration: old name use_http1 removed (word match)" 1 $?
|
|
else
|
|
echo " SKIP: migration script not found at $MIGRATION_SCRIPT"
|
|
fi
|
|
|
|
printf "\n##\n## 09: load_package_config defaults\n##\n\n"
|
|
|
|
rm -f "$__uci_store"/*
|
|
__cfg_package="https-dns-proxy"
|
|
|
|
# Set up minimal config with defaults
|
|
uci_set "https-dns-proxy" "config" "canary_domains_icloud" "1"
|
|
uci_set "https-dns-proxy" "config" "canary_domains_mozilla" "1"
|
|
uci_set "https-dns-proxy" "config" "force_dns" "1"
|
|
uci_set "https-dns-proxy" "config" "procd_trigger_wan6" "0"
|
|
uci_set "https-dns-proxy" "config" "force_http1" "0"
|
|
uci_set "https-dns-proxy" "config" "force_http3" "0"
|
|
uci_set "https-dns-proxy" "config" "force_ip_family" "auto"
|
|
|
|
# Reset globals before load
|
|
canary_domains_icloud=""
|
|
canary_domains_mozilla=""
|
|
force_dns=""
|
|
procd_trigger_wan6=""
|
|
|
|
load_package_config
|
|
|
|
assert_eq "load_package_config: canary_domains_icloud=1" "1" "$canary_domains_icloud"
|
|
assert_eq "load_package_config: canary_domains_mozilla=1" "1" "$canary_domains_mozilla"
|
|
assert_eq "load_package_config: force_dns=1" "1" "$force_dns"
|
|
assert_eq "load_package_config: global_user defaults to nobody" "nobody" "$global_user"
|
|
assert_eq "load_package_config: global_group defaults to nogroup" "nogroup" "$global_group"
|
|
assert_eq "load_package_config: global_listen_addr defaults to 127.0.0.1" "127.0.0.1" "$global_listen_addr"
|
|
assert_eq "load_package_config: global_force_ip_family=auto" "auto" "$global_force_ip_family"
|
|
|
|
# Canary domains should be populated
|
|
echo "$canaryDomains" | grep -q "mask.icloud.com"
|
|
assert_rc "load_package_config: iCloud canary domains added" 0 $?
|
|
echo "$canaryDomains" | grep -q "use-application-dns.net"
|
|
assert_rc "load_package_config: Mozilla canary domains added" 0 $?
|
|
|
|
# ── Test with canary domains disabled ──
|
|
rm -f "$__uci_store"/*
|
|
uci_set "https-dns-proxy" "config" "canary_domains_icloud" "0"
|
|
uci_set "https-dns-proxy" "config" "canary_domains_mozilla" "0"
|
|
uci_set "https-dns-proxy" "config" "force_dns" "0"
|
|
uci_set "https-dns-proxy" "config" "procd_trigger_wan6" "0"
|
|
uci_set "https-dns-proxy" "config" "force_http1" "0"
|
|
uci_set "https-dns-proxy" "config" "force_http3" "0"
|
|
uci_set "https-dns-proxy" "config" "force_ip_family" "auto"
|
|
|
|
canaryDomains=""
|
|
load_package_config
|
|
|
|
assert_eq "load_package_config: canary disabled → canaryDomains empty" "" "$canaryDomains"
|
|
assert_eq "load_package_config: force_dns=0 → unset" "" "$force_dns"
|
|
|
|
printf "\n##\n## 9b: service_started/service_stopped exit code (issue #11)\n##\n\n"
|
|
|
|
# These run only for the procd_set_config_changed side effect. When force_dns and
|
|
# notrack_dns are both empty the guard test is false; without an explicit
|
|
# 'return 0' the function's exit status (1) becomes the exit code of
|
|
# start/reload/restart, so a successful restart wrongly reports failure.
|
|
force_dns='' notrack_dns=''
|
|
service_started; assert_rc "service_started returns 0 when force_dns/notrack_dns empty" 0 $?
|
|
service_stopped; assert_rc "service_stopped returns 0 when force_dns/notrack_dns empty" 0 $?
|
|
force_dns='1' notrack_dns=''
|
|
service_started; assert_rc "service_started returns 0 when force_dns set" 0 $?
|
|
|
|
printf "\n##\n## 10: notrack_nft (regression: missing nftables.d/ruleset-post dir)\n##\n\n"
|
|
|
|
# Reset state — ensure parent dir does NOT exist (this is the apk-install
|
|
# bug: post-install runs `start` before fw4 has created the directory).
|
|
rm -rf "$TESTDIR/usr/share"
|
|
__nft_rc=0
|
|
: > "$__nft_calls_file"
|
|
|
|
assert_eq "notrack_nft: NOTRACK_NFT_FILE patched to test path" "$NOTRACK_TEST_FILE" "$NOTRACK_NFT_FILE"
|
|
|
|
# Pre-condition: parent dir genuinely missing
|
|
[ ! -d "$(dirname "$NOTRACK_TEST_FILE")" ]
|
|
assert_rc "notrack_nft: parent dir absent before update" 0 $?
|
|
|
|
# THE REGRESSION: previously this failed with
|
|
# "can't create .../20-https-dns-proxy-notrack.nft: nonexistent directory"
|
|
notrack_nft update "53 5053"
|
|
assert_rc "notrack_nft update creates parent dir on first call" 0 $?
|
|
|
|
[ -f "$NOTRACK_TEST_FILE" ]
|
|
assert_rc "notrack_nft update wrote nft snippet file" 0 $?
|
|
|
|
[ -d "$(dirname "$NOTRACK_TEST_FILE")" ]
|
|
assert_rc "notrack_nft update created parent dir" 0 $?
|
|
|
|
# Content should reference the table, hook, and ports we passed
|
|
grep -q "table inet https_dns_proxy_notrack" "$NOTRACK_TEST_FILE"
|
|
assert_rc "notrack_nft snippet declares the wrapper table" 0 $?
|
|
|
|
grep -q "type filter hook output priority raw" "$NOTRACK_TEST_FILE"
|
|
assert_rc "notrack_nft snippet declares raw output hook" 0 $?
|
|
|
|
grep -q "53 5053" "$NOTRACK_TEST_FILE"
|
|
assert_rc "notrack_nft snippet contains supplied ports" 0 $?
|
|
|
|
# Syntax check should have been invoked
|
|
grep -q -- "-c -f $NOTRACK_TEST_FILE" "$__nft_calls_file"
|
|
assert_rc "notrack_nft update invokes 'nft -c -f' on the snippet" 0 $?
|
|
|
|
# ── Idempotence: same content → no extra write churn ──
|
|
# We can't easily detect a no-op write, but we can confirm the function
|
|
# still succeeds when the file already exists with matching content.
|
|
notrack_nft update "53 5053"
|
|
assert_rc "notrack_nft update idempotent on identical content" 0 $?
|
|
|
|
# ── Empty port_set → routes to remove ──
|
|
notrack_nft update ""
|
|
[ ! -f "$NOTRACK_TEST_FILE" ]
|
|
assert_rc "notrack_nft update '' removes the snippet file" 0 $?
|
|
|
|
# ── Explicit remove ──
|
|
mkdir -p "$(dirname "$NOTRACK_TEST_FILE")"
|
|
echo "stale" > "$NOTRACK_TEST_FILE"
|
|
: > "$__nft_calls_file"
|
|
notrack_nft remove
|
|
[ ! -f "$NOTRACK_TEST_FILE" ]
|
|
assert_rc "notrack_nft remove deletes the snippet file" 0 $?
|
|
|
|
grep -q "delete table inet https_dns_proxy_notrack" "$__nft_calls_file"
|
|
assert_rc "notrack_nft remove invokes 'nft delete table'" 0 $?
|
|
|
|
# ── remove is a no-op when file already absent and table already gone ──
|
|
# Mock `nft` to return non-zero so `nft list table` reports "no such table"
|
|
# (the real-world post-delete state); the new remove logic returns 0 only
|
|
# when both the file and the live table are absent.
|
|
__nft_rc=1
|
|
notrack_nft remove
|
|
assert_rc "notrack_nft remove succeeds when file and table both absent" 0 $?
|
|
__nft_rc=0
|
|
|
|
# ── nft binary absent: notrack_nft is a no-op ──
|
|
# Without firewall4/nftables installed, the package should not error;
|
|
# `command -v nft` returns non-zero and notrack_nft returns 0 immediately.
|
|
rm -rf "$TESTDIR/usr/share"
|
|
__saved_nft_def="$(typeset -f nft 2>/dev/null || declare -f nft)"
|
|
unset -f nft
|
|
mkdir -p "$TESTDIR/empty-path"
|
|
__saved_path="$PATH"
|
|
PATH="$TESTDIR/empty-path"
|
|
|
|
notrack_nft update "53"
|
|
assert_rc "notrack_nft update is a no-op when nft binary is absent" 0 $?
|
|
|
|
[ ! -f "$NOTRACK_TEST_FILE" ]
|
|
assert_rc "notrack_nft did not write snippet when nft is absent" 0 $?
|
|
|
|
PATH="$__saved_path"
|
|
eval "$__saved_nft_def"
|
|
|
|
# ── mkdir failure path returns non-zero ──
|
|
# Place a regular file at the would-be parent dir so mkdir -p must fail.
|
|
# Defensive logic should return 1 instead of falling through to a broken
|
|
# redirection.
|
|
rm -rf "$TESTDIR/usr/share"
|
|
mkdir -p "$(dirname "$(dirname "$NOTRACK_TEST_FILE")")"
|
|
: > "$(dirname "$NOTRACK_TEST_FILE")"
|
|
|
|
notrack_nft update "53" 2>/dev/null
|
|
assert_rc "notrack_nft update returns 1 when parent dir cannot be created" 1 $?
|
|
|
|
[ ! -f "$NOTRACK_TEST_FILE" ]
|
|
assert_rc "notrack_nft did not write snippet on mkdir failure" 0 $?
|
|
|
|
rm -f "$(dirname "$NOTRACK_TEST_FILE")"
|
|
|
|
###############################################################################
|
|
# SHELL SCRIPT SYNTAX #
|
|
###############################################################################
|
|
|
|
printf "\n--- Shell script syntax ---\n"
|
|
for shellscript in \
|
|
files/etc/init.d/* \
|
|
files/etc/uci-defaults/*; do
|
|
[ -f "$shellscript" ] || continue
|
|
head -1 "$shellscript" | grep -q '^#!/bin/sh' || continue
|
|
name="${shellscript#files/}"
|
|
n_tests=$((n_tests + 1))
|
|
if sh -n "$shellscript" 2>/dev/null; then
|
|
pass "sh -n $name"
|
|
else
|
|
fail "sh -n $name" "syntax ok" "syntax error"
|
|
sh -n "$shellscript"
|
|
fi
|
|
done
|
|
|
|
###############################################################################
|
|
# SUMMARY #
|
|
###############################################################################
|
|
|
|
printf "\nRan %d tests, %d passed, %d failed\n" $n_tests $((n_tests - n_fails)) $n_fails
|
|
exit $n_fails
|