#!/bin/sh /etc/rc.common # Copyright 2019-2026 Stan Grishin (stangri@melmac.ca) # shellcheck disable=SC1091,SC3043,SC3060 # shellcheck disable=SC2034 START=20 # shellcheck disable=SC2034 STOP=15 # shellcheck disable=SC2034 USE_PROCD=1 if type extra_command 1>/dev/null 2>&1; then extra_command 'version' 'Show version information' else # shellcheck disable=SC2034 EXTRA_COMMANDS='version' fi readonly PKG_VERSION='dev-test' readonly packageName='https-dns-proxy' readonly serviceName="$packageName $PKG_VERSION" readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m' readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m' readonly PROG=/usr/sbin/https-dns-proxy readonly BOOTSTRAP_CF='1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001' readonly BOOTSTRAP_GOOGLE='8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844' readonly DEFAULT_BOOTSTRAP="${BOOTSTRAP_CF},${BOOTSTRAP_GOOGLE}" readonly canaryDomainsMozilla='use-application-dns.net' readonly canaryDomainsiCloud='mask.icloud.com mask-h2.icloud.com' readonly NOTRACK_NFT_FILE='/usr/share/nftables.d/ruleset-post/20-https-dns-proxy-notrack.nft' # Silence "Command failed: Not found" for redundant procd service delete calls __UBUS_BIN="$(command -v ubus || echo /bin/ubus)" ubus() { if [ "$1" = "call" ] && [ "$2" = "service" ] && [ "$3" = "delete" ]; then "$__UBUS_BIN" "$@" >/dev/null 2>&1 || true else "$__UBUS_BIN" "$@" fi } hdp_boot_flag= # package global config variables canary_domains_icloud= canary_domains_mozilla= dnsmasq_config_update= force_dns= force_dns_port= notrack_dns= notrack_ports= force_dns_src_interface= procd_trigger_wan6= global_listen_addr= global_tcp_client_limit= global_polling_interval= global_proxy_server= global_force_http1= global_force_http3= global_force_ipv6= global_max_idle_time= global_conn_loss_time= global_ca_certs_file= global_user= global_group= global_verbosity= global_logfile= global_statistic_interval= global_log_limit= dnsmasq_restart() { /etc/init.d/dnsmasq restart >/dev/null 2>&1; } is_alnum() { case "$1" in (*[![:alnum:]_\ @]*|"") return 1;; esac; } 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; } is_ipv6() { ! is_mac_address "$1" && str_contains "$1" ":"; } is_port_listening() { local hex is_integer "$1" || return 1 hex="$(printf '%04X' "$1")" # TCP: state 0A == LISTEN if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h && $4=="0A") {found=1}} END{exit found?0:1}' /proc/net/tcp /proc/net/tcp6 2>/dev/null; then return 0 fi # UDP: presence indicates a bound socket if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h) {found=1}} END{exit found?0:1}' /proc/net/udp /proc/net/udp6 2>/dev/null; then return 0 fi return 1 } is_resolver_working() { local heartbeat_domain heartbeat_sleep_timeout heartbeat_wait_timeout config_load "$packageName" config_get heartbeat_domain 'config' 'heartbeat_domain' 'heartbeat.melmac.ca' config_get heartbeat_sleep_timeout 'config' 'heartbeat_sleep_timeout' '10' config_get heartbeat_wait_timeout 'config' 'heartbeat_wait_timeout' '30' [ "$heartbeat_domain" = '-' ] && return 0 is_integer "$heartbeat_sleep_timeout" && sleep "$heartbeat_sleep_timeout" resolveip -t "$heartbeat_wait_timeout" "$heartbeat_domain" >/dev/null 2>&1 } output() { [ -z "$verbosity" ] && verbosity="$(uci_get "$packageName" 'config' 'verbosity' '1')" [ "$#" -ne '1' ] && { case "$1" in [0-9]) [ $((verbosity & $1)) -gt 0 ] && shift || return 0;; esac } local msg="$*" queue="/dev/shm/$packageName-output" [ -t 1 ] && printf "%b" "$msg" [ "$msg" != "${msg//\\n}" ] && { [ -s "$queue" ] && msg="$(cat "$queue")${msg}" && rm -f "$queue" msg="$(printf "%b" "$msg" | sed 's/\x1b\[[0-9;]*m//g')" logger -t "$packageName [$$]" "$(printf "%b" "$msg")" } || printf "%b" "$msg" >> "$queue" } 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_word() { echo "$1" | grep -qw "$2"; } uci_add_list_if_new() { local PACKAGE="$1" local CONFIG="$2" local OPTION="$3" local VALUE="$4" local i [ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1 for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do [ "$i" = "$VALUE" ] && return 0 done uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE" } uci_changes() { local PACKAGE="$1" 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}}")" ] } notrack_nft() { case "$1" in update) local port_set="$2" local new_content existing_content if [ -z "$port_set" ]; then notrack_nft remove return fi new_content="$(cat <<-EOF add table inet https_dns_proxy_notrack flush table inet https_dns_proxy_notrack add chain inet https_dns_proxy_notrack raw_output { type filter hook output priority raw; policy accept; } add rule inet https_dns_proxy_notrack raw_output meta l4proto { tcp, udp } th dport { ${port_set} } ip daddr 127.0.0.0/8 notrack add rule inet https_dns_proxy_notrack raw_output meta l4proto { tcp, udp } th sport { ${port_set} } ip saddr 127.0.0.0/8 notrack EOF )" existing_content="$(cat "$NOTRACK_NFT_FILE" 2>/dev/null)" if [ "$new_content" != "$existing_content" ]; then mkdir -p "${NOTRACK_NFT_FILE%/*}" echo "$new_content" > "$NOTRACK_NFT_FILE" fi [ -s "$NOTRACK_NFT_FILE" ] && nft -c -f "$NOTRACK_NFT_FILE" ;; remove) rm -f "$NOTRACK_NFT_FILE" nft delete table inet https_dns_proxy_notrack >/dev/null 2>&1 ! nft list table inet https_dns_proxy_notrack >/dev/null 2>&1 && [ ! -s "$NOTRACK_NFT_FILE" ] ;; esac } version() { echo "$PKG_VERSION"; } xappend() { PROG_param="$PROG_param $1"; } append_bool() { local section="$1" local option="$2" local value="$3" local default="${4:-0}" local _loctmp config_get_bool _loctmp "$section" "$option" "$default" [ "$_loctmp" -ne 0 ] && xappend "$value" } append_parm() { local section="$1" local option="$2" local switch="$3" local default="$4" local skip_value="$5" local _loctmp config_get _loctmp "$section" "$option" "$default" [ -n "$_loctmp" ] && [ "$_loctmp" != "$skip_value" ] && xappend "$switch $_loctmp" } append_cnt() { local section="$1" local option="$2" local switch="$3" local default="${4:-0}" local _loctmp i config_get _loctmp "$section" "$option" "$default" # shellcheck disable=SC2086,SC2154 for i in $(seq 1 $_loctmp); do xappend '-v' done } append_boot() { local section="$1" local option="$2" local switch="$3" local default="$4" local _old_ifs="$IFS" local _loctmp _newtmp i config_get _loctmp "$section" "$option" "$default" [ -z "$_loctmp" ] && return 0 IFS=" ," for i in $_loctmp; do if { [ -z "$force_ipv6" ] && is_ipv4 "$i"; } || \ { [ -n "$force_ipv6" ] && is_ipv6 "$i"; }; then [ -z "$_newtmp" ] && _newtmp="$i" || _newtmp="${_newtmp},${i}" fi done IFS="$_old_ifs" [ -n "$_newtmp" ] && xappend "$switch $_newtmp" [ -z "$force_ipv6" ] && xappend '-4' } boot() { rc_procd start_service 'on_boot' && service_started 'on_boot' } load_package_config() { local param="$1" config_load "$packageName" config_load "$packageName" config_get_bool canary_domains_icloud 'config' 'canary_domains_icloud' '1' config_get_bool canary_domains_mozilla 'config' 'canary_domains_mozilla' '1' config_get_bool force_dns 'config' 'force_dns' '1' config_get_bool notrack_dns 'config' 'notrack_dns' '1' config_get_bool procd_trigger_wan6 'config' 'procd_trigger_wan6' '0' config_get_bool global_force_http1 'config' 'force_http1' '0' config_get_bool global_force_http3 'config' 'force_http3' '0' config_get_bool global_force_ipv6 'config' 'force_ipv6_resolvers' '0' config_get dnsmasq_config_update 'config' 'dnsmasq_config_update' '*' config_get force_dns_port 'config' 'force_dns_port' '53 853' config_get force_dns_src_interface 'config' 'force_dns_src_interface' 'lan' config_get global_listen_addr 'config' 'listen_addr' '127.0.0.1' config_get global_tcp_client_limit 'config' 'tcp_client_limit' '20' config_get global_polling_interval 'config' 'polling_interval' '120' config_get global_proxy_server 'config' 'proxy_server' config_get global_max_idle_time 'config' 'max_idle_time' '118' config_get global_conn_loss_time 'config' 'conn_loss_time' '15' config_get global_ca_certs_file 'config' 'ca_certs_file' config_get global_user 'config' 'user' 'nobody' config_get global_group 'config' 'group' 'nogroup' config_get global_verbosity 'config' 'verbosity' '0' config_get global_logfile 'config' 'logfile' 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}" [ "$force_dns" = '1' ] || unset force_dns [ "$notrack_dns" = '1' ] || unset notrack_dns [ "$procd_trigger_wan6" = '1' ] || unset procd_trigger_wan6 } start_instance() { local cfg="$1" param="$2" local PROG_param local listen_addr listen_port force_ipv6 p url iface config_get url "$cfg" 'resolver_url' config_get listen_addr "$cfg" 'listen_addr' "$global_listen_addr" config_get listen_port "$cfg" 'listen_port' "$port" config_get_bool force_ipv6 "$cfg" 'force_ipv6_resolvers' "$global_force_ipv6" [ "$force_ipv6" = '1' ] || unset force_ipv6 append_parm "$cfg" 'resolver_url' '-r' append_parm "$cfg" 'listen_addr' '-a' "$global_listen_addr" '127.0.0.1' append_parm "$cfg" 'listen_port' '-p' "$port" append_boot "$cfg" 'bootstrap_dns' '-b' "$DEFAULT_BOOTSTRAP" append_parm "$cfg" 'dscp_codepoint' '-c' append_parm "$cfg" 'tcp_client_limit' '-T' "$global_tcp_client_limit" '20' append_parm "$cfg" 'polling_interval' '-i' "$global_polling_interval" '120' append_parm "$cfg" 'proxy_server' '-t' "$global_proxy_server" append_bool "$cfg" 'force_http1' '-x' "$global_force_http1" append_bool "$cfg" 'force_http3' '-q' "$global_force_http3" append_parm "$cfg" 'max_idle_time' '-m' "$global_max_idle_time" '118' append_parm "$cfg" 'conn_loss_time' '-L' "$global_conn_loss_time" '15' append_parm "$cfg" 'ca_certs_file' '-C' "$global_ca_certs_file" append_parm "$cfg" 'user' '-u' "$global_user" append_parm "$cfg" 'group' '-g' "$global_group" append_parm "$cfg" 'logfile' '-l' "$global_logfile" append_parm "$cfg" 'statistic_interval' '-s' "$global_statistic_interval" '0' append_parm "$cfg" 'log_limit' '-F' "$global_log_limit" '0' append_cnt "$cfg" 'verbosity' '-v' "$global_verbosity" append_parm "$cfg" 'source_addr' '-S' "$global_source_addr" if [ "$dnsmasq_config_update" = '*' ]; then config_load 'dhcp' 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}" dnsmasq_instance_append_force_dns_port "@dnsmasq[$i]" || \ dnsmasq_instance_append_force_dns_port "${i}" done fi procd_open_instance # shellcheck disable=SC2086 procd_set_param command $PROG $PROG_param procd_set_param stderr 1 procd_set_param stdout 1 procd_set_param respawn procd_open_data json_add_object mdns procd_add_mdns_service "$packageName" 'udp' "$listen_port" "DNS over HTTPS proxy" json_close_object if [ -n "$force_dns" ]; then json_add_array firewall for iface in ${force_dns_src_interface//,/ }; do for p in ${force_dns_port//,/ }; do if is_port_listening "$p"; then json_add_object '' json_add_string type 'redirect' json_add_string target 'DNAT' json_add_string src "$iface" json_add_string proto 'tcp udp' json_add_string src_dport '53' json_add_string dest_port "$p" json_add_string family 'any' json_add_boolean reflection '0' json_close_object else json_add_object '' json_add_string type 'rule' json_add_string src "$iface" json_add_string dest '*' json_add_string proto 'tcp udp' json_add_string dest_port "$p" json_add_string target 'REJECT' json_close_object fi done done json_close_array unset force_dns fi procd_close_data procd_close_instance # shellcheck disable=SC2181 if [ "$?" -eq 0 ]; then output_ok notrack_ports="${notrack_ports:+${notrack_ports}, }${listen_port}" port="$((port+1))" else output_fail fi } start_service() { local param="$1" local canaryDomains local force_dns="$force_dns" local port=5053 [ "$param" = 'on_boot' ] && hdp_boot_flag='true' && return 0 output "Starting $serviceName instances ${param:+${param} }" load_package_config "$param" dhcp_backup 'create' config_load "$packageName" config_foreach start_instance "$packageName" "$param" output "\\n" if uci_changes 'dhcp'; then output "Updating dnsmasq config " if uci_commit 'dhcp'; then output_okn param='on_config_update' else output_failn fi fi case "$param" in on_boot|on_config_update|on_hotplug) output "Restarting dnsmasq ${param:+${param} }" if dnsmasq_restart; then output_okn else output_failn fi ;; esac if [ -n "$notrack_dns" ] && [ -n "$notrack_ports" ]; then output "Updating notrack rules " if notrack_nft update "$notrack_ports"; then output_okn else output_failn fi else notrack_nft remove fi # if ! is_resolver_working; then # rc_procd stop_service 'on_failed_health_check' && service_stopped 'on_failed_health_check' # fi } stop_service() { local param="$1" local canaryDomains local _error= output "Stopping $serviceName ${param:+${param} }" load_package_config "$param" dhcp_backup 'restore' if uci_changes 'dhcp'; then uci_commit 'dhcp' dnsmasq_restart || _error=1 fi notrack_nft remove || _error=1 # shellcheck disable=SC2015 [ -z "$_error" ] && output_okn || output_failn } # shellcheck disable=SC2015 service_triggers() { local wan wan6 i if [ -n "$hdp_boot_flag" ]; then output "Setting trigger (on_boot) " procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" reload 'on_interface_up' && output_okn || output_failn else . "${IPKG_INSTROOT}/lib/functions/network.sh" network_flush_cache network_find_wan wan wan="${wan:-wan}" if [ -n "$procd_trigger_wan6" ]; then network_find_wan6 wan6 wan6="${wan6:-wan6}" fi 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 output '\n' procd_add_config_trigger "config.change" "$packageName" "/etc/init.d/${packageName}" reload 'on_config_change' fi } service_started() { { [ -n "$force_dns" ] || [ -n "$notrack_dns" ]; } && procd_set_config_changed firewall; } service_stopped() { { [ -n "$force_dns" ] || [ -n "$notrack_dns" ]; } && procd_set_config_changed firewall; } restart() { reload "$@"; } dnsmasq_instance_append_force_dns_port() { local cfg="$1" instance_port [ "$(uci_get 'dhcp' "$cfg")" = "dnsmasq" ] || return 1 config_get instance_port "$cfg" 'port' '53' [ "$instance_port" = "0" ] && return 0 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 for i in $canaryDomains; do uci_add_list_if_new 'dhcp' "$cfg" 'server' "/${i}/" done fi case $address in 0.0.0.0|::ffff:0.0.0.0) address='127.0.0.1';; ::) address='::1';; esac uci_add_list_if_new 'dhcp' "$cfg" 'server' "${address}#${port}" uci_add_list_if_new 'dhcp' "$cfg" 'doh_server' "${address}#${port}" ;; remove) for i in $(uci_get 'dhcp' "$cfg" 'doh_server'); do uci_remove_list 'dhcp' "$cfg" 'server' "$i" uci_remove_list 'dhcp' "$cfg" 'doh_server' "$i" done for i in $canaryDomains; do uci_remove_list 'dhcp' "$cfg" 'server' "/${i}/" uci_remove_list 'dhcp' "$cfg" 'doh_server' "/${i}/" done ;; esac } dhcp_backup() { _dnsmasq_create_server_backup() { local cfg="$1" i [ "$(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 uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' '-1' else uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' "$(uci_get 'dhcp' "$cfg" noresolv)" fi uci_set 'dhcp' "$cfg" 'noresolv' 1 fi if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_server')" ]; then if [ -z "$(uci_get 'dhcp' "$cfg" 'server')" ]; then uci_add_list 'dhcp' "$cfg" 'doh_backup_server' "" fi for i in $(uci_get 'dhcp' "$cfg" 'server'); do uci_add_list 'dhcp' "$cfg" 'doh_backup_server' "$i" if [ "$i" = "$(echo "$i" | tr -d /\#)" ]; then uci_remove_list 'dhcp' "$cfg" 'server' "$i" fi done fi return 0 } # shellcheck disable=SC2317 _dnsmasq_restore_server_backup() { local cfg="$1" i [ "$(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' else uci_set 'dhcp' "$cfg" 'noresolv' "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" fi uci_remove 'dhcp' "$cfg" 'doh_backup_noresolv' fi if uci_get 'dhcp' "$cfg" 'doh_backup_server' >/dev/null 2>&1; then dnsmasq_doh_server "$cfg" 'remove' for i in $(uci_get 'dhcp' "$cfg" 'doh_backup_server'); do uci_add_list_if_new 'dhcp' "$cfg" 'server' "$i" done uci_remove 'dhcp' "$cfg" 'doh_backup_server' fi } local i config_load 'dhcp' case "$1" in create) if [ "$dnsmasq_config_update" = "*" ]; then config_foreach _dnsmasq_create_server_backup 'dnsmasq' elif [ -n "$dnsmasq_config_update" ]; then for i in $dnsmasq_config_update; do if [ -n "$(uci_get 'dhcp' "@dnsmasq[$i]")" ]; then _dnsmasq_create_server_backup "@dnsmasq[$i]" elif [ -n "$(uci_get 'dhcp' "$i")" ]; then _dnsmasq_create_server_backup "$i" fi done fi ;; restore) config_foreach _dnsmasq_restore_server_backup 'dnsmasq' ;; esac }