#!/bin/sh /etc/rc.common
#
# procd service for meduzavpnd.
#
# The daemon reads a JSON settings file; OpenWrt configures things in UCI. This
# script is the translation, and it runs on every start, so /etc/config/meduzavpn
# is the only place a setting has to be written -- which is what section 6b of
# docs/router-and-tv-clients-design.md means by "UCI is the source of truth".
#
# The generated file lives in /var, which on OpenWrt is tmpfs: it is derived
# data, it must never be edited, and regenerating it costs the flash nothing.
# The daemon's *state* -- the session it signed in with, and which protocol was
# proved to work on this uplink -- goes to /etc/meduzavpn instead, because /var
# is gone after a reboot and a memory that does not survive a power cut is the
# one case it exists for.

USE_PROCD=1

# After the network (20) and the firewall (19), because there is no point
# testing candidate protocols before there is a WAN to test them over. The
# daemon retries on its own anyway, so being early would only waste attempts.
START=95
# Early in the shutdown, while the network is still up: the daemon has to talk
# to `ip` to put the routing back, and a router whose tunnel rules outlive its
# daemon has no route to the internet and no software left to fix it.
STOP=10

PROG=/usr/sbin/meduzavpnd
CLI=/usr/bin/meduzavpn
RUNDIR=/var/run/meduzavpn
CONFFILE=$RUNDIR/config.json
STATEDIR=/etc/meduzavpn

# write_settings turns the UCI section into the daemon's settings file.
#
# It uses jshn rather than printf so that a location with a quote in it, or an
# empty list, produces valid JSON instead of a file the daemon refuses to parse
# with an error nobody can connect the two halves of.
write_settings() {
	. /usr/share/libubox/jshn.sh

	local service location protocol log_level api_base mtu
	local killswitch ipv6 auto_connect dns

	config_load meduzavpn
	config_get service      general service_id
	config_get location     general location
	config_get protocol     general protocol 'auto'
	config_get log_level    general log_level 'info'
	config_get api_base     general api_base
	config_get mtu          general mtu 0
	config_get dns          general dns
	config_get_bool killswitch   general killswitch 0
	config_get_bool ipv6         general ipv6 0
	config_get_bool auto_connect general auto_connect 1

	# 'location' is a second spelling of 'service_id': the daemon resolves a
	# service by id, by name or by location, so one field serves both and there
	# is no second lookup to write.
	[ -n "$service" ] || service=$location

	json_init
	[ -n "$api_base" ] && json_add_string api_base "$api_base"
	[ -n "$service" ] && json_add_string service "$service"
	[ -n "$protocol" ] && json_add_string protocol "$protocol"
	[ -n "$log_level" ] && json_add_string log_level "$log_level"
	[ "${mtu:-0}" -gt 0 ] 2>/dev/null && json_add_int mtu "$mtu"
	json_add_boolean kill_switch "$killswitch"
	json_add_boolean ipv6 "$ipv6"
	json_add_boolean auto_connect "$auto_connect"
	if [ -n "$dns" ]; then
		json_add_array dns
		local one
		for one in $dns; do json_add_string "" "$one"; done
		json_close_array
	fi

	mkdir -p "$RUNDIR"
	chmod 0700 "$RUNDIR"
	# Written through a temporary file: procd may be starting the daemon while
	# this runs on a reload, and a half-written settings file is a daemon that
	# will not start for a reason that has nothing to do with the settings.
	json_dump > "$CONFFILE.new" || return 1
	chmod 0600 "$CONFFILE.new"
	mv "$CONFFILE.new" "$CONFFILE"
}

# warn_about_policy says out loud that per-device policy is configured but not
# yet enforced.
#
# A setting that is accepted and does nothing is worse than one that is
# missing: the owner writes a rule for the television, sees no error, and
# believes the television is covered. Saying so in the log is the least this
# can do until the routing lands.
warn_about_policy() {
	local count=0
	count_policy() { count=$((count + 1)); }
	config_load meduzavpn
	config_foreach count_policy policy
	[ "$count" -gt 0 ] && \
		logger -t meduzavpn -p daemon.warn \
			"$count per-device policy rule(s) are configured but this version does not enforce them yet: everything behind this router follows the tunnel"
	return 0
}

warn_about_killswitch() {
	local killswitch
	config_load meduzavpn
	config_get_bool killswitch general killswitch 0
	[ "$killswitch" = 1 ] && \
		logger -t meduzavpn -p daemon.warn \
			"kill switch is on: while the tunnel is down, every device in this house loses the internet -- the LAN and the router itself stay reachable, nothing else does"
	return 0
}

start_service() {
	local enabled
	config_load meduzavpn
	config_get_bool enabled general enabled 0
	if [ "$enabled" != 1 ]; then
		logger -t meduzavpn -p daemon.notice \
			"not starting: disabled in /etc/config/meduzavpn (uci set meduzavpn.general.enabled=1)"
		return 0
	fi

	if ! write_settings; then
		logger -t meduzavpn -p daemon.err "could not write $CONFFILE; not starting"
		return 1
	fi
	warn_about_killswitch
	warn_about_policy

	mkdir -p "$STATEDIR"
	chmod 0700 "$STATEDIR"

	procd_open_instance meduzavpnd
	procd_set_param command "$PROG" --config "$CONFFILE" --state-dir "$STATEDIR"
	# Stamped so the account's device list says how this copy arrived, the same
	# way the .deb and .rpm do it.
	procd_set_param env MEDUZAVPN_INSTALL=openwrt
	# respawn threshold=300 timeout=5 retry=0: give up never. A router that
	# stopped retrying is a router somebody has to drive to.
	procd_set_param respawn 300 5 0
	# Long enough for the daemon to take the tunnel down and put the routing,
	# the DNS and the firewall back before procd reaches for SIGKILL. Cut short,
	# the rules stay and the house has no internet.
	procd_set_param term_timeout 30
	procd_set_param stdout 1
	procd_set_param stderr 1
	procd_close_instance
}

# A settings change cannot be applied to a running daemon -- it reads its file
# once at start -- so a reload is a restart, and a restart drops the tunnel.
# procd is told to do it that way rather than pretending otherwise.
service_triggers() {
	procd_add_reload_trigger meduzavpn
}

reload_service() {
	stop
	start
}
