#!/bin/sh /etc/rc.common

START=94
USE_PROCD=1

PROG=/usr/sbin/tailscale_helper
PROGD=/usr/sbin/tailscaled

service_triggers() {
	procd_add_reload_trigger "tailscale"
}

_any_enabled=0
_note_enabled() {
	local e
	config_get_bool e "$1" 'enabled' 0
	[ "$e" = "1" ] && _any_enabled=1
}

# A reload used to be a full stop+start. That tore the firewall zone down and
# rebuilt it, so every Save&Apply cost two fw4 reloads and briefly reopened the
# kill-switch. procd already diffs the instance definition and restarts
# tailscaled only when it actually changed, and the helper reconciles firewall,
# DNS and kill-switch idempotently — so plain `start` is enough.
#
# The exception is being switched off: only stop_service restores the
# forwardings the kill-switch disabled and removes our zone.
reload_service() {
	config_load 'tailscale'
	_any_enabled=0
	config_foreach _note_enabled 'tailscale'

	if [ "$_any_enabled" = "1" ]; then
		start
	else
		stop
	fi
}

append_comma() { advertise_routes="${advertise_routes:+$advertise_routes,}$1"; }
append_space() { _list_val="${_list_val:+$_list_val }$1"; }

start_instance() {
	local cfg="$1"
	local enabled port config_path fw_mode std_out std_err state_file
	local accept_routes accept_dns hostname advertise_exit_node exit_node
	local advertise_routes disable_snat_subnet_routes access flags
	local login_server authkey dns_mode

	config_get_bool enabled "$cfg" 'enabled' 0
	[ "$enabled" -eq 0 ] && return

	config_get port "$cfg" 'port' '41641'
	config_get config_path "$cfg" 'config_path' '/etc/tailscale'
	config_get state_file "$cfg" 'state_file' "$config_path/tailscaled.state"
	config_get fw_mode "$cfg" 'fw_mode'
	config_get_bool std_out "$cfg" 'log_stdout' 0
	config_get_bool std_err "$cfg" 'log_stderr' 0

	[ -z "$fw_mode" ] && { [ -x /sbin/fw4 ] && fw_mode="nftables" || fw_mode="iptables"; }
	mkdir -p "$config_path" "$(dirname "$state_file")"

	config_get_bool accept_routes "$cfg" 'accept_routes' 0
	config_get hostname "$cfg" 'hostname'

	# dns_mode supersedes the old accept_dns flag; accept_dns is still honoured
	# when dns_mode is absent so existing configs keep working after an upgrade.
	config_get dns_mode "$cfg" 'dns_mode'
	config_get_bool accept_dns "$cfg" 'accept_dns' 0
	case "$dns_mode" in
		magicdns)         accept_dns=1 ;;
		dnsmasq|disabled) accept_dns=0 ;;
		"")               [ "$accept_dns" = "1" ] && dns_mode=magicdns || dns_mode=disabled ;;
		*)                dns_mode=disabled; accept_dns=0 ;;
	esac

	config_get_bool advertise_exit_node "$cfg" 'advertise_exit_node' 0
	config_get exit_node "$cfg" 'exit_node'

	advertise_routes=""
	config_list_foreach "$cfg" 'advertise_routes' append_comma
	config_get_bool disable_snat_subnet_routes "$cfg" 'disable_snat_subnet_routes' 0

	_list_val=""; config_list_foreach "$cfg" 'access' append_space; access="$_list_val"
	_list_val=""; config_list_foreach "$cfg" 'flags' append_space; flags="$_list_val"

	config_get login_server "$cfg" 'login_server'
	config_get authkey "$cfg" 'authkey'

	procd_open_instance "tailscaled"
	procd_set_param command "$PROGD"
	procd_append_param command --no-logs-no-support
	procd_append_param command --port "$port"
	procd_append_param command --state "$state_file"
	# Hint tailscaled which fw backend to use — tailscale-lite respects this
	# env var. Without it, auto-detect can pick wrong backend on hybrid hosts.
	procd_set_param env GOGC=10
	[ -n "$fw_mode" ] && procd_append_param env "TS_DEBUG_FIREWALL_MODE=$fw_mode"
	procd_set_param respawn
	procd_set_param stdout "$std_out"
	procd_set_param stderr "$std_err"
	procd_close_instance

	# authkey travels in the environment, never in argv: helper's command line
	# is world-readable through /proc/<pid>/cmdline (`ps`), and the key grants
	# tailnet enrolment to whoever reads it.
	{
		ACCESS="$access" \
		TS_AUTHKEY="$authkey" \
		$PROG \
			--accept-routes="$accept_routes" \
			--accept-dns="$accept_dns" \
			--hostname="$hostname" \
			--advertise-exit-node="$advertise_exit_node" \
			--exit-node="$exit_node" \
			--advertise-routes="$advertise_routes" \
			--snat-subnet-routes="$([ "$disable_snat_subnet_routes" = "1" ] && echo "0" || echo "1")" \
			--login-server="$login_server" \
			--dns-mode="$dns_mode" \
			$flags
	} </dev/null >/tmp/tailscale_helper.log 2>&1 &
}

start_service() {
	config_load 'tailscale'
	config_foreach start_instance 'tailscale'
}

stop_service() {
	# Give helper a chance to finish a running UCI transaction. Helper is
	# backgrounded by start_instance (& shell), so procd doesn't know about
	# it — we must signal it manually. Kill hard only if it's still alive
	# after the grace window.
	# Probe with `killall -0`, not `pgrep -x`: busybox pgrep matches -x against
	# argv[0], which here is the full path /usr/sbin/tailscale_helper, so a
	# bare name never matched and this whole block used to be dead code.
	# killall compares the (15-char truncated) comm and finds it correctly.
	if killall -0 tailscale_helper 2>/dev/null; then
		killall -TERM tailscale_helper 2>/dev/null
		count=0
		while killall -0 tailscale_helper 2>/dev/null && [ $count -lt 5 ]; do
			sleep 1
			count=$((count + 1))
		done
		killall -KILL tailscale_helper 2>/dev/null
	fi
	# tailscaled itself is procd-managed. procd stops it on its own via
	# SIGTERM + term_timeout + SIGKILL once start_service returns without
	# reopening the instance. No manual killall — that would also hit any
	# unrelated tailscaled process.
	rm -f /var/lock/tailscale.lock

	# Restore kill-switch state from ts_saved_enabled sibling keys that
	# helper stores in /etc/config/firewall.
	for sect in $(uci show firewall 2>/dev/null \
		| grep '\.ts_saved_enabled=' \
		| cut -d'.' -f2 | cut -d'=' -f1); do
		saved=$(uci -q get "firewall.$sect.ts_saved_enabled")
		if [ "$saved" = "<unset>" ]; then
			uci -q delete "firewall.$sect.enabled"
		else
			uci -q set "firewall.$sect.enabled=$saved"
		fi
		uci -q delete "firewall.$sect.ts_saved_enabled"
	done
	# Migration: remove old tmpfs state file.
	rm -f /var/lib/tailscale/killswitch_lan_wan

	# Drop the MagicDNS forward we may have added to dnsmasq. Leaving it behind
	# would point the tailnet zone at a resolver that is no longer running.
	dns_changed=0
	for entry in $(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null \
		| tr ' ' '\n' | grep '/100\.100\.100\.100$'); do
		uci -q del_list "dhcp.@dnsmasq[0].server=$entry" && dns_changed=1
	done
	if [ "$dns_changed" = "1" ] && [ -n "$(uci changes dhcp)" ]; then
		uci commit dhcp && {
			/etc/init.d/dnsmasq reload >/dev/null 2>&1 \
				|| /etc/init.d/dnsmasq restart >/dev/null 2>&1
		}
	fi

	uci -q delete firewall.tszone
	uci -q delete firewall.ts_ac_lan
	uci -q delete firewall.ts_ac_wan
	uci -q delete firewall.lan_ac_ts
	uci -q delete firewall.wan_ac_ts
	if [ -n "$(uci changes firewall)" ]; then
		uci commit firewall && /etc/init.d/firewall reload >/dev/null 2>&1
	fi
}
