#!/bin/sh
# ForestVPN install check (PRD INS-2, ADR 0108 section 5, PLAN WP A-4).
#
#   check.sh [check]   the refusals. Nothing is installed or changed (the tun module is loaded if the firmware has it).
#                      Exit 0 = fine, 78 = refused.
#   check.sh ensure    the init script's start-time check: the same, minus the storage line (a condition of installing), and
#                      what `check` accepts as "installable" but is missing (the tun module) is installed
#                      now, from the router's own feeds, when no package manager holds its lock. Exit 0 = ready, 78 = refused.
#
# opkg stops at a refusal: nothing is installed. apk 3 does not (tested on 25.12.5): a failed pre-install only marks the package
# broken and the install carries on, so there the refusal is a printed sentence and the init script refuses to start. An
# installer for 25.12 must run this file BEFORE `apk add` to give the PRD's "nothing was installed" (WP A-7).
#
# The package's preinst IS this file (the Makefile includes it verbatim): preinst runs before anything is installed, so
# it cannot call a file of the package, and it runs inside the package manager's lock, so it never calls the package
# manager (opkg and apk both refuse to run nested, verified). It reads the feed indexes the manager already fetched.
# An argument of "install" or "upgrade" (what opkg passes) is a plain `check`.
#
# A refusal prints the sentence of PRD 5.8, then the code `ne.host_unsupported reason=<word>` (docs/errors.md), exit 78
# (the number `fv up` itself uses). Never a module built for another kernel: a feed package for the tun or ipset module is
# taken only when its version starts with the running kernel's (`uname -r`).
#
# No backslash anywhere in this file: the SDK writes the preinst with `echo "$VAR"`, and dash's echo eats backslash escapes.
#
# Test seam: FVPN_CHECK_ROOT=<dir> reads every path below from that fake root (a file stands for /dev/net/tun, the free
# space is the number in <dir>/free_kb, the kernel is <dir>/proc/sys/kernel/osrelease, nothing is loaded or installed).

# Free storage in kB for the package and one update (PRD INS-2), per package architecture, because the binary differs per
# architecture (a MIPS fv is 3.9 MB, an x86_64 one 4.4 MB). need = roundup_256(3.25 x gzip-9(fv)/1024 + 256): 1.25 for the flash
# filesystem's per-page cost over gzip, plus the 2 the updater demands (the package file twice). sdk.sh refuses to package an fv
# whose need is over its row here and prints the number to put in, so a binary that grew cannot ship with a stale row. This file is
# also the setup scripts' pre-install check, which runs before any package is chosen, so the numbers live here and are not
# substituted into the package's copy only. The MIPS rows sit one 256 kB step above the formula (the fv is 1 kB of gzip from the step
# below it, and every size change would otherwise be a refused build). The arches published before MIPS keep the 8192 kB they
# shipped with: the formula is unmeasured and NAND (ubifs) compresses worse than gzip, so it only lowers once gate SIZE measures it. Gate SIZE (df /overlay on a 16 MB device) replaces the 1.25 and the 256 with measured numbers.
FVPN_NEED_KB="aarch64_cortex-a53=8192 aarch64_generic=8192 aarch64_cortex-a72=8192 aarch64_cortex-a76=8192 arm_cortex-a7_neon-vfpv4=8192 arm_cortex-a9_vfpv3-d16=8192 arm_cortex-a15_neon-vfpv4=8192 arm_cortex-a9=8192 x86_64=8192 mipsel_24kc=6656 mipsel_74kc=6656 mips_24kc=6656"
# 128 MB boards report 108-123 MB: the kernel and its carve-outs take the rest (the 128 MB QEMU guests: 104,228 kB (armvirt) and 106,924 kB (x86)).
FVPN_NEED_RAM_KB=100000
# The package architectures we ship: packaging/openwrt/arches.tsv (a test keeps the two equal).
FVPN_ARCHES="aarch64_cortex-a53 aarch64_generic aarch64_cortex-a72 aarch64_cortex-a76 arm_cortex-a7_neon-vfpv4 arm_cortex-a9_vfpv3-d16 arm_cortex-a15_neon-vfpv4 arm_cortex-a9 x86_64 mipsel_24kc mipsel_74kc mips_24kc"

fvpn_refuse() { # <reason> <sentence> [packages the user can add first]
	echo "ForestVPN cannot run on this router"
	echo "$2"
	[ -z "$3" ] || echo "Add these packages from your router's software list first: $3"
	echo "ne.host_unsupported reason=$1"
	if [ "$FVPN_MODE" = ensure ]; then :
	elif [ -x "$FVPN_ROOT/usr/bin/apk" ]; then
		# apk 3 (25.12) does not stop at a failed pre-install: it marks the package broken and installs it anyway
		echo "ForestVPN will not start on this router. Remove it with: apk del forestvpn"
	else echo "Nothing was installed. Nothing on your router changed."; fi
	exit 78
}

fvpn_kernel() { if [ -n "$FVPN_ROOT" ]; then cat "$FVPN_ROOT/proc/sys/kernel/osrelease" 2>/dev/null; else uname -r; fi; }

fvpn_free_kb() {
	if [ -n "$FVPN_ROOT" ]; then cat "$FVPN_ROOT/free_kb" 2>/dev/null || echo 0
	else df -Pk / 2>/dev/null | awk 'NR == 2 { print $4 }'; fi
}

# fvpn_feed <pkg> [kmod]: 0 = the feeds carry it (a kmod only for this kernel), 1 = they do not, 2 = cannot tell
# (no index fetched yet, or the manager is busy). The answer is read from what the manager already holds.
fvpn_feed() {
	_pkg=$1; _kernel=$(fvpn_kernel)
	if [ -x "$FVPN_ROOT/usr/bin/apk" ]; then
		# apk answers a search under its own install lock (tested): 0 = found, 1 = no such package
		_out=$(apk search -e "$_pkg" 2>&1); _rc=$?
		case $_out in *WARNING*|*ERROR*) return 2 ;; esac
		[ "$_rc" = 0 ] || [ "$_rc" = 1 ] || return 2
		_ver=$(echo "$_out" | sed -n "s/^$_pkg-//p" | head -n 1)
	else
		_dir=$FVPN_ROOT/var/opkg-lists
		ls "$_dir"/* >/dev/null 2>&1 || return 2
		_ver=$(for _f in "$_dir"/*; do [ "${_f%.sig}" = "$_f" ] || continue; gzip -dc "$_f" 2>/dev/null || cat "$_f"; done |
			awk -v p="$_pkg" '$1 == "Package:" { cur = $2 } $1 == "Version:" && cur == p && !seen { print $2; seen = 1 }')   # no early exit: it would break the pipe
	fi
	[ -n "$_ver" ] || return 1
	[ "$2" = kmod ] || return 0
	case $_ver in "$_kernel"-*) return 0 ;; *) return 1 ;; esac
}

fvpn_install() { # <pkg>...  (ensure only: the package manager's lock is free then)
	if [ -x /usr/bin/apk ]; then apk update >/dev/null 2>&1; apk add "$@" >/dev/null 2>&1
	else opkg update >/dev/null 2>&1; opkg install "$@" >/dev/null 2>&1; fi
}

fvpn_have_tun() { [ -c "$FVPN_ROOT/dev/net/tun" ] || { [ -n "$FVPN_ROOT" ] && [ -e "$FVPN_ROOT/dev/net/tun" ]; }; }

# the module of this kernel, if the firmware has it (a fake root never loads anything)
fvpn_load_tun() { [ -z "$FVPN_ROOT" ] && modprobe tun >/dev/null 2>&1 && fvpn_have_tun; }

# nftables beside fw3: the tool, and on a real router a kernel that answers it
fvpn_have_nft() {
	[ -x "$FVPN_ROOT/usr/sbin/nft" ] || return 1
	[ -n "$FVPN_ROOT" ] && return 0
	nft list tables >/dev/null 2>&1
}

fvpn_check() {
	FVPN_MODE=${1:-check}; FVPN_ROOT=${FVPN_CHECK_ROOT:-}
	case $FVPN_MODE in ensure) ;; *) FVPN_MODE=check ;; esac
	_tun="This router's firmware is missing the part ForestVPN needs to make a tunnel, and there is no matching package for it."
	_nft="This router's firewall is missing nftables, which ForestVPN needs to keep your devices off the internet when the VPN is down."

	# firmware family and CPU type
	[ -f "$FVPN_ROOT/etc/openwrt_release" ] && [ -f "$FVPN_ROOT/etc/rc.common" ] && [ -x "$FVPN_ROOT/sbin/procd" ] ||
		fvpn_refuse arch "This router's firmware is not supported."
	_arch=$(sed -n "s/^DISTRIB_ARCH=//p" "$FVPN_ROOT/etc/openwrt_release" | tr -d "'" | tr -d '"')
	case " $FVPN_ARCHES " in *" $_arch "*) ;; *)
		fvpn_refuse arch "This router has a ${_arch:-unknown} processor. ForestVPN does not support it yet." ;;
	esac
	# every arch in FVPN_ARCHES has a row in FVPN_NEED_KB (check_matrix.sh keeps the two lists equal)
	FVPN_NEED_FREE_KB=8192
	for _p in $FVPN_NEED_KB; do [ "${_p%%=*}" = "$_arch" ] && FVPN_NEED_FREE_KB=${_p#*=}; done

	_ram=$(awk '/^MemTotal:/ { print $2 }' "$FVPN_ROOT/proc/meminfo" 2>/dev/null)
	[ "${_ram:-0}" -ge "$FVPN_NEED_RAM_KB" ] ||
		fvpn_refuse ram "This router has $(( ${_ram:-0} / 1024 )) MB of memory. ForestVPN needs at least 128 MB."

	_free=$(fvpn_free_kb)
	[ "$FVPN_MODE" = ensure ] || [ "${_free:-0}" -ge "$FVPN_NEED_FREE_KB" ] ||
		fvpn_refuse space "This router has $(( ${_free:-0} / 1024 )) MB of free storage. ForestVPN needs $(( (FVPN_NEED_FREE_KB + 1023) / 1024 )) MB."

	# the tunnel device: present, else this kernel's own module, else a matching package in the feeds
	if ! fvpn_have_tun && ! fvpn_load_tun; then
		fvpn_feed kmod-tun kmod; [ $? != 1 ] || fvpn_refuse tun "$_tun"
		if [ "$FVPN_MODE" = ensure ]; then
			fvpn_install kmod-tun; fvpn_load_tun || fvpn_refuse tun "$_tun"
		fi
	fi

	# the old firewall (fw3): the kill switch is its own nftables table beside it (seam S14), and a fw3 firmware may have the
	# kernel parts but not the tool (the photonicat's vendor feed has neither). Refused in both modes, the packages named:
	# nothing installs them on its own.
	if [ -x "$FVPN_ROOT/sbin/fw3" ] && [ ! -x "$FVPN_ROOT/sbin/fw4" ] && ! fvpn_have_nft; then
		fvpn_refuse nft "$_nft" "nftables-nojson kmod-nft-core kmod-nft-nat"
	fi
	return 0
}

fvpn_check "$1"
exit $?
