From 7d0db39fc88c047e98ff9fd8bc746e0e68b100f2 Mon Sep 17 00:00:00 2001 From: Stan Grishin Date: Mon, 2 Mar 2026 20:21:48 +0000 Subject: [PATCH] https-dns-proxy: update to 2025.12.29-4 * add explicit LICENSE file to the repository * pretty up Makefile * minor shell script styling improvements * better parsing if individual dnsmasq instances are used in config * functional test Signed-off-by: Stan Grishin --- net/https-dns-proxy/LICENSE | 21 + net/https-dns-proxy/Makefile | 16 +- .../files/etc/init.d/https-dns-proxy | 29 +- net/https-dns-proxy/tests/run_tests.sh | 639 ++++++++++++++++++ 4 files changed, 686 insertions(+), 19 deletions(-) create mode 100644 net/https-dns-proxy/LICENSE create mode 100644 net/https-dns-proxy/tests/run_tests.sh diff --git a/net/https-dns-proxy/LICENSE b/net/https-dns-proxy/LICENSE new file mode 100644 index 0000000000..d5f962817d --- /dev/null +++ b/net/https-dns-proxy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MOSSDeF + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/net/https-dns-proxy/Makefile b/net/https-dns-proxy/Makefile index ff74afe117..7103324acc 100644 --- a/net/https-dns-proxy/Makefile +++ b/net/https-dns-proxy/Makefile @@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=https-dns-proxy PKG_VERSION:=2025.12.29 -PKG_RELEASE:=1 +PKG_RELEASE:=4 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/aarond10/https_dns_proxy/ @@ -25,10 +25,16 @@ define Package/https-dns-proxy SECTION:=net CATEGORY:=Network TITLE:=DNS Over HTTPS Proxy - URL:=https://github.com/stangri/https-dns-proxy/ - DEPENDS:=+libcares +libcurl +libev +ca-bundle +jsonfilter +resolveip - DEPENDS+=+!BUSYBOX_DEFAULT_GREP:grep - DEPENDS+=+!BUSYBOX_DEFAULT_SED:sed + URL:=https://github.com/mossdef-org/https-dns-proxy/ + DEPENDS:= \ + +libcares \ + +libcurl \ + +libev \ + +ca-bundle \ + +jsonfilter \ + +resolveip \ + +!BUSYBOX_DEFAULT_GREP:grep \ + +!BUSYBOX_DEFAULT_SED:sed CONFLICTS:=https_dns_proxy endef diff --git a/net/https-dns-proxy/files/etc/init.d/https-dns-proxy b/net/https-dns-proxy/files/etc/init.d/https-dns-proxy index a46a321eba..c1b0c61fe0 100755 --- a/net/https-dns-proxy/files/etc/init.d/https-dns-proxy +++ b/net/https-dns-proxy/files/etc/init.d/https-dns-proxy @@ -1,5 +1,5 @@ #!/bin/sh /etc/rc.common -# Copyright 2019-2023 Stan Grishin (stangri@melmac.ca) +# Copyright 2019-2026 Stan Grishin (stangri@melmac.ca) # shellcheck disable=SC1091,SC3043,SC3060 # shellcheck disable=SC2034 @@ -67,7 +67,6 @@ global_log_limit= dnsmasq_restart() { /etc/init.d/dnsmasq restart >/dev/null 2>&1; } is_alnum() { case "$1" in (*[![:alnum:]_\ @]*|"") return 1;; esac; } -is_fw4_restart_needed() { [ "$(uci_get "$packageName" 'config' 'force_dns' '1')" = '1' ]; } is_mac_address() { expr "$1" : '[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]$' >/dev/null; } is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; [ "$1" -ge 1 ] && [ "$1" -le 65535 ] || return 1; return 0; } is_ipv4() { expr "$1" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' >/dev/null; } @@ -112,7 +111,7 @@ output_ok() { output "$_OK_"; } output_okn() { output "${_OK_}\\n"; } output_fail() { output "$_FAIL_"; } output_failn() { output "${_FAIL_}\\n"; } -str_contains() { [ "${1//$2}" != "$1" ]; } +str_contains() { [ "${1//${2}}" != "$1" ]; } str_contains_word() { echo "$1" | grep -qw "$2"; } uci_add_list_if_new() { local PACKAGE="$1" @@ -131,7 +130,7 @@ uci_changes() { local CONFIG="$2" local OPTION="$3" [ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \ - [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}")" ] + [ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c ${UCI_CONFIG_DIR}} changes "$PACKAGE${CONFIG:+.${CONFIG}}${OPTION:+.${OPTION}}")" ] } version() { echo "$PKG_VERSION"; } @@ -224,8 +223,8 @@ load_package_config() { config_get global_statistic_interval 'config' 'statistic_interval' '0' config_get global_log_limit 'config' 'log_limit' '0' config_get global_source_addr 'config' 'source_addr' - [ "$canary_domains_icloud" = '1' ] && canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}" - [ "$canary_domains_mozilla" = '1' ] && canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}" + [ "$canary_domains_icloud" = '1' ] && canaryDomains="${canaryDomains:+${canaryDomains} }${canaryDomainsiCloud}" + [ "$canary_domains_mozilla" = '1' ] && canaryDomains="${canaryDomains:+${canaryDomains} }${canaryDomainsMozilla}" [ "$force_dns" = '1' ] || unset force_dns [ "$procd_trigger_wan6" = '1' ] || unset procd_trigger_wan6 } @@ -267,6 +266,7 @@ start_instance() { config_foreach dnsmasq_doh_server 'dnsmasq' 'add' "${listen_addr}" "${listen_port}" config_foreach dnsmasq_instance_append_force_dns_port 'dnsmasq' elif is_alnum "$dnsmasq_config_update"; then + config_load 'dhcp' for i in $dnsmasq_config_update; do dnsmasq_doh_server "@dnsmasq[$i]" 'add' "${listen_addr}" "${listen_port}" || \ dnsmasq_doh_server "${i}" 'add' "${listen_addr}" "${listen_port}" @@ -335,7 +335,7 @@ start_service() { [ "$param" = 'on_boot' ] && hdp_boot_flag='true' && return 0 - output "Starting $serviceName instances ${param:+$param }" + output "Starting $serviceName instances ${param:+${param} }" load_package_config "$param" dhcp_backup 'create' config_load "$packageName" @@ -352,7 +352,7 @@ start_service() { fi case "$param" in on_boot|on_config_update|on_hotplug) - output "Restarting dnsmasq ${param:+$param }" + output "Restarting dnsmasq ${param:+${param} }" if dnsmasq_restart; then output_okn else @@ -369,7 +369,7 @@ stop_service() { local param="$1" local canaryDomains local _error= - output "Stopping $serviceName ${param:+$param }" + output "Stopping $serviceName ${param:+${param} }" load_package_config "$param" dhcp_backup 'restore' if uci_changes 'dhcp'; then @@ -395,7 +395,7 @@ service_triggers() { network_find_wan6 wan6 wan6="${wan6:-wan6}" fi - output "Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }" + output "Setting trigger${wan6:+s} for $wan ${wan6:+${wan6} }" for i in $wan $wan6; do procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" reload 'on_interface_trigger' && output_ok || output_fail done @@ -410,13 +410,14 @@ restart() { reload "$@"; } dnsmasq_instance_append_force_dns_port() { local cfg="$1" instance_port - [ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1 + [ "$(uci_get 'dhcp' "$cfg")" = "dnsmasq" ] || return 1 config_get instance_port "$cfg" 'port' '53' - str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}" + str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+${force_dns_port} }${instance_port}" } dnsmasq_doh_server() { local cfg="$1" param="$2" address="${3:-127.0.0.1}" port="$4" i + [ "$(uci_get 'dhcp' "$cfg")" = "dnsmasq" ] || return 1 case "$param" in add) if [ -n "$force_dns" ]; then @@ -447,7 +448,7 @@ dnsmasq_doh_server() { dhcp_backup() { _dnsmasq_create_server_backup() { local cfg="$1" i - [ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1 + [ "$(uci_get 'dhcp' "$cfg")" = "dnsmasq" ] || return 1 # uci_remove 'dhcp' "$cfg" 'doh_server' # this removes outdated doh_server entries, but causes unnecessary dnsmasq restarts if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then if [ -z "$(uci_get 'dhcp' "$cfg" 'noresolv')" ]; then @@ -473,7 +474,7 @@ dhcp_backup() { # shellcheck disable=SC2317 _dnsmasq_restore_server_backup() { local cfg="$1" i - [ -n "$(uci_get 'dhcp' "$cfg")" ] || return 0 + [ "$(uci_get 'dhcp' "$cfg")" = "dnsmasq" ] || return 0 if [ -n "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then if [ "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" = "-1" ]; then uci_remove 'dhcp' "$cfg" 'noresolv' diff --git a/net/https-dns-proxy/tests/run_tests.sh b/net/https-dns-proxy/tests/run_tests.sh new file mode 100644 index 0000000000..bb188069e0 --- /dev/null +++ b/net/https-dns-proxy/tests/run_tests.sh @@ -0,0 +1,639 @@ +#!/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.openwrt.melmac.ca/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" + +# ── Source the init script (skip the shebang line) ────────────────── + +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 + +# Source all functions. The #!/bin/sh /etc/rc.common line is harmless +# when we've already defined the framework stubs above. +# shellcheck disable=SC1090 +. "$INIT_SCRIPT" + +############################################################################### +# 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" "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## 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 '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" + + 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 "force_ipv6_resolvers" "$MIGRATE_CONF" + assert_rc "migration: use_ipv6_resolvers_only → force_ipv6_resolvers" 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_ipv6_resolvers" "0" + +# 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" + +# 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_ipv6_resolvers" "0" + +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" + +############################################################################### +# 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