#!/bin/sh /etc/rc.common
# photondns - high performance DNS forwarder with adaptive failover

START=75
USE_PROCD=1

PROG=/usr/bin/photondns
CONF=/var/etc/photondns.toml
RULE_DIR=/etc/photondns
REDIRECT_LOCK_FILE=/etc/photondns/redirect.lock

get_config() {
	config_get enabled $1 enabled 0
	config_get listen_address $1 listen_address "0.0.0.0"
	config_get listen_port $1 listen_port 15533
	config_get udp $1 udp 1
	config_get tcp $1 tcp 1
	config_get udp_sockets $1 udp_sockets 0
	config_get tcp_idle_timeout $1 tcp_idle_timeout 30
	config_get doh $1 doh 0
	config_get doh_port $1 doh_port 8054
	config_get doh_path $1 doh_path "/dns-query"
	config_get doh_cert $1 doh_cert ""
	config_get doh_key $1 doh_key ""
	config_get log_level $1 log_level "info"
	config_get log_file $1 log_file "/var/log/photondns.log"
	config_get api_port $1 api_port 8053
	config_get redirect $1 redirect 0
	config_get dns_hijack $1 dns_hijack 0
	config_get cache $1 cache 1
	config_get cache_size $1 cache_size 65536
	config_get min_ttl $1 min_ttl 0
	config_get max_ttl $1 max_ttl 86400
	config_get ttl_multiply $1 ttl_multiply 1.0
	config_get negative_ttl $1 negative_ttl 30
	config_get serve_stale $1 serve_stale 1
	config_get stale_ttl $1 stale_ttl 86400
	config_get stale_client_ttl $1 stale_client_ttl 30
	config_get prefetch $1 prefetch 1
	config_get prefetch_margin $1 prefetch_margin 10
	config_get prefetch_min_hits $1 prefetch_min_hits 2
	config_get dump_cache $1 dump_cache 1
	config_get dump_interval $1 dump_interval 0
	config_get strategy $1 strategy "parallel"
	config_get hedge_delay $1 hedge_delay 250
	config_get query_timeout $1 query_timeout 5000
	config_get health_check_interval $1 health_check_interval 10
	config_get health_check_domain $1 health_check_domain "www.gstatic.com"
	config_get fail_threshold $1 fail_threshold 3
	config_get recover_threshold $1 recover_threshold 2
	config_get cooldown $1 cooldown 15
	config_get bootstrap_dns $1 bootstrap_dns "223.5.5.5"
	config_get insecure_skip_verify $1 insecure_skip_verify 0
	config_get idle_timeout $1 idle_timeout 30
	config_get reject_type65 $1 reject_type65 0
	config_get aaaa_mode $1 aaaa_mode "allow"
	config_get hosts_ttl $1 hosts_ttl 300
	config_get block_special $1 block_special 1
	config_get block_private_ptr $1 block_private_ptr 1
	config_get prewarm $1 prewarm 1
	config_get prewarm_interval $1 prewarm_interval 3000
	config_get lan_hosts $1 lan_hosts 1
	config_get lan_suffix $1 lan_suffix "lan"
	config_get lan_leases $1 lan_leases "/tmp/dhcp.leases"
	config_get lan_refresh $1 lan_refresh 30
	config_get lan_ttl $1 lan_ttl 60
	config_get china_list $1 china_list 1
	config_get adblock $1 adblock 0
	config_get query_log_size $1 query_log_size 5000
	config_get auto_update $1 auto_update 0
	config_get update_day $1 update_day "*"
	config_get update_time $1 update_time 4
}

append_upstream() {
	upstream_list="${upstream_list}\"$1\", "
}

append_backup() {
	backup_list="${backup_list}\"$1\", "
}

append_local() {
	local_list="${local_list}\"$1\", "
}

bool_str() {
	[ "$1" = "1" ] && echo "true" || echo "false"
}

generate_config() {
	mkdir -p /var/etc
	upstream_list="" backup_list="" local_list=""
	config_list_foreach main upstream append_upstream
	config_list_foreach main backup_upstream append_backup
	config_list_foreach main local_upstream append_local
	[ -z "$upstream_list" ] && upstream_list="\"tcp://1.1.1.1\", "

	local cache_enabled=$(bool_str $cache)
	local dump_file=""
	[ "$dump_cache" = "1" ] && dump_file="$RULE_DIR/cache.dump"

	# china list only makes sense with a "local" group to route to
	local china_list_file=""
	[ "$china_list" = "1" ] && [ -n "$local_list" ] && [ -f "$RULE_DIR/china_list.txt" ] && \
		china_list_file="$RULE_DIR/china_list.txt"

	local prewarm_file=""
	[ "$prewarm" = "1" ] && [ -f "$RULE_DIR/prewarm.txt" ] && \
		prewarm_file="$RULE_DIR/prewarm.txt"

	local lan_hosts_file=""
	[ -f "$RULE_DIR/lan_hosts.txt" ] && lan_hosts_file="$RULE_DIR/lan_hosts.txt"

	local ad_list_file=""
	[ "$adblock" = "1" ] && [ -f "$RULE_DIR/ad_list.txt" ] && \
		ad_list_file="$RULE_DIR/ad_list.txt"

	local doh_listen=""
	[ "$doh" = "1" ] && doh_listen="$listen_address:$doh_port"

	cat > $CONF <<-EOF
	# generated from /etc/config/photondns - do not edit
	[server]
	listen = ["$listen_address:$listen_port"]
	udp = $(bool_str $udp)
	tcp = $(bool_str $tcp)
	udp_sockets = $udp_sockets
	tcp_idle_timeout = $tcp_idle_timeout
	doh_listen = "$doh_listen"
	doh_path = "$doh_path"
	doh_cert = "$doh_cert"
	doh_key = "$doh_key"

	[cache]
	enabled = $cache_enabled
	size = $cache_size
	min_ttl = $min_ttl
	max_ttl = $max_ttl
	ttl_multiply = $ttl_multiply
	negative_ttl = $negative_ttl
	serve_stale = $(bool_str $serve_stale)
	stale_ttl = $stale_ttl
	stale_client_ttl = $stale_client_ttl
	prefetch = $(bool_str $prefetch)
	prefetch_margin = $prefetch_margin
	prefetch_min_hits = $prefetch_min_hits
	dump_file = "$dump_file"
	dump_interval = $dump_interval

	[api]
	# control API is localhost-only on purpose: it is unauthenticated and can
	# read the per-client query log and flush the cache. LuCI reaches it via
	# rpcd on 127.0.0.1, so binding it to $listen_address would expose it to the
	# whole LAN for no benefit.
	listen = "127.0.0.1:$api_port"

	[log]
	level = "$log_level"
	file = "$log_file"
	query_log_size = $query_log_size

	[failover]
	health_check_interval = $health_check_interval
	health_check_domain = "$health_check_domain"
	fail_threshold = $fail_threshold
	recover_threshold = $recover_threshold
	cooldown = $cooldown

	[routing]
	hosts_file = "$RULE_DIR/hosts.txt"
	block_file = "$RULE_DIR/block.txt"
	local_domains_file = "$RULE_DIR/local_domains.txt"
	china_list_file = "$china_list_file"
	ad_list_file = "$ad_list_file"
	redirect_file = "$RULE_DIR/redirect.txt"
	hosts_ttl = $hosts_ttl
	reject_type65 = $(bool_str $reject_type65)
	aaaa_mode = "$aaaa_mode"
	block_special = $(bool_str $block_special)
	block_private_ptr = $(bool_str $block_private_ptr)

	[prewarm]
	domains_file = "$prewarm_file"
	interval = $prewarm_interval

	[lan]
	enabled = $(bool_str $lan_hosts)
	leases_file = "$lan_leases"
	extra_hosts_file = "$lan_hosts_file"
	suffix = "$lan_suffix"
	refresh_interval = $lan_refresh
	ttl = $lan_ttl

	[[group]]
	name = "main"
	strategy = "$strategy"
	upstreams = [${upstream_list%, }]
	backups = [${backup_list%, }]
	hedge_delay_ms = $hedge_delay
	timeout_ms = $query_timeout
	insecure_skip_verify = $(bool_str $insecure_skip_verify)
	bootstrap = "$bootstrap_dns"
	idle_timeout = $idle_timeout
	EOF

	[ -n "$local_list" ] && cat >> $CONF <<-EOF

	[[group]]
	name = "local"
	strategy = "$strategy"
	upstreams = [${local_list%, }]
	hedge_delay_ms = $hedge_delay
	timeout_ms = $query_timeout
	insecure_skip_verify = $(bool_str $insecure_skip_verify)
	bootstrap = "$bootstrap_dns"
	idle_timeout = $idle_timeout
	EOF
}

service_triggers() {
	procd_add_reload_trigger "photondns"
}

CRON_FILE=/etc/crontabs/root

setcron() {
	sed -i '/photondns-chinalist\|photondns-adlist/d' $CRON_FILE 2>/dev/null
	if [ "$auto_update" = "1" ]; then
		[ "$china_list" = "1" ] && echo "0 $update_time * * $update_day /usr/bin/photondns-chinalist" >> $CRON_FILE
		[ "$adblock" = "1" ] && echo "10 $update_time * * $update_day /usr/bin/photondns-adlist" >> $CRON_FILE
	fi
	crontab $CRON_FILE 2>/dev/null
}

delcron() {
	sed -i '/photondns-chinalist\|photondns-adlist/d' $CRON_FILE 2>/dev/null
	crontab $CRON_FILE 2>/dev/null
}

uci_set_or_del() {
	local key="$1"
	local val="$2"
	if [ -n "$val" ]; then
		uci set "$key=$val"
	else
		uci -q del "$key"
	fi
}

dhcp_setting() {
	[ ! -f "$REDIRECT_LOCK_FILE" ] && {
		printf "noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv)\n"
		printf "resolvfile=$(uci -q get dhcp.@dnsmasq[0].resolvfile)\n"
		printf "cachesize=$(uci -q get dhcp.@dnsmasq[0].cachesize)\n"
		printf "rebind_protection=$(uci -q get dhcp.@dnsmasq[0].rebind_protection)\n"
	} > $REDIRECT_LOCK_FILE
	sed -i "/list server/d" /etc/config/dhcp
	uci add_list dhcp.@dnsmasq[0].server="127.0.0.1#$listen_port"
	uci set dhcp.@dnsmasq[0].rebind_protection='0'
	uci set dhcp.@dnsmasq[0].noresolv='1'
	uci set dhcp.@dnsmasq[0].cachesize='0'
	uci commit dhcp
}

restore_dhcp_setting() {
	if [ -f "$REDIRECT_LOCK_FILE" ]; then
		sed -i "/list server/d" /etc/config/dhcp
		. "$REDIRECT_LOCK_FILE"
		uci_set_or_del dhcp.@dnsmasq[0].noresolv "$noresolv"
		uci_set_or_del dhcp.@dnsmasq[0].resolvfile "$resolvfile"
		uci_set_or_del dhcp.@dnsmasq[0].cachesize "$cachesize"
		uci_set_or_del dhcp.@dnsmasq[0].rebind_protection "$rebind_protection"
		uci commit dhcp
		rm -f "$REDIRECT_LOCK_FILE"
	fi
}

reload_dnsmasq() {
	[ -x /etc/init.d/dnsmasq ] && /etc/init.d/dnsmasq reload
}

start_service() {
	config_load "photondns"
	config_foreach get_config "photondns"
	[ "$enabled" -ne 1 ] && return 1

	mkdir -p $RULE_DIR
	generate_config

	procd_open_instance photondns
	procd_set_param command $PROG -c $CONF
	procd_set_param file $CONF
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_set_param respawn 3600 5 0
	procd_set_param limits nofile="65535 65535"
	procd_close_instance

	# Everything below is post-start housekeeping and none of it is a
	# prerequisite for photondns itself: dnsmasq is a *consumer* of this
	# resolver rather than a dependency, the cron entries only schedule list
	# updates, and restore_dhcp_setting merely undoes a previous redirect.
	#
	# It used to run inline, and it cost 4.04s of every boot. procd's rcS is
	# strictly serial -- rcS.c does `q.max_running_tasks = 1` -- so that did
	# not just delay photondns, it delayed every service behind S75. sysntpd
	# is S98, which meant the clock was not set until ~19s after reset.
	#
	# Measured on a photonicat2 with every /etc/rc.d/S* wrapped in a kmsg
	# timestamp, same board, same build:
	#
	#     start_service   4.041s -> 0.084s
	#     S98sysntpd     18.957s -> 14.513s   (when the clock gets set)
	#     rcS finishes    ~24.4s -> 19.97s
	#
	# Detached rather than reordered because the cost is not one slow call we
	# could simply move: it only shows up during boot, when dnsmasq is itself
	# still starting. Timed by hand on a running system the same steps take
	# ~0.35s, so this has to come off the critical path rather than be tuned.
	photondns_post_start &
}

# Runs detached from start_service; see the note there. Kept in the order the
# inline version used, because restore_dhcp_setting/dhcp_setting edit
# /etc/config/dhcp and reload_dnsmasq is what makes those edits take effect.
photondns_post_start() {
	setcron

	if [ "$redirect" -eq 1 ]; then
		dhcp_setting
	else
		restore_dhcp_setting
	fi
	reload_dnsmasq

	# force redirect all LAN DNS to photondns (fw4)
	if [ "$redirect" -eq 1 ] && [ "$dns_hijack" -eq 1 ] && [ -f /sbin/fw4 ]; then
		nft --check list table inet photondns >/dev/null 2>&1 && nft delete table inet photondns
		nft add table inet photondns
		nft add chain inet photondns prerouting "{ type nat hook prerouting priority -95; policy accept; }"
		nft add rule inet photondns prerouting "meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :$listen_port comment \"photondns DNS hijack\""
	else
		nft --check list table inet photondns >/dev/null 2>&1 && nft delete table inet photondns
	fi
}

stop_service() {
	config_load "photondns"
	config_foreach get_config "photondns"
	[ "$enabled" -eq 0 ] && restore_dhcp_setting && reload_dnsmasq
	[ "$enabled" -eq 0 ] && delcron
	nft --check list table inet photondns >/dev/null 2>&1 && nft delete table inet photondns
	return 0
}
