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 <stangri@melmac.ca>
This commit is contained in:
Stan Grishin
2026-03-02 20:21:48 +00:00
parent 38313ec4b4
commit 7d0db39fc8
4 changed files with 686 additions and 19 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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'

View File

@@ -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