#!/bin/sh
# vim: set noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 :

name=$0
basename="$(basename "$0")"

. /lib/functions.sh
. "${OMR_LIB_DIR:-/usr/share/omr/lib}/omr-state.sh"

export OMR_TRACKER_STATUS
export OMR_TRACKER_PREV_STATUS
export OMR_TRACKER_STATUS_MSG


_log() {
	logger -p daemon.info -t "${basename}" "$@"
}

_post_tracking() {
	[ -d /usr/share/omr/v2ray-post-tracking.d ] && {
		for tracker_bin in /usr/share/omr/v2ray-post-tracking.d/*; do
			[ -x "$tracker_bin" ] && (
				_log() {
					logger -t "v2ray-post-tracking-${tracker_bin##*/}" "$*"
				}
				. "$tracker_bin" 2>&1
			)
		done
	}
	[ -d /usr/share/omr/proxy-post-tracking.d ] && {
		for tracker_bin in /usr/share/omr/proxy-post-tracking.d/*; do
			[ -x "$tracker_bin" ] && (
				_log() {
					logger -t "proxy-post-tracking-${tracker_bin##*/}" "$*"
				}
				. "$tracker_bin" 2>&1
			)
		done
	}
}

_ping_server() {
	local host=$1
	ping \
	    -w "${OMR_TRACKER_TIMEOUT:-5}" \
	    -c 1 \
	    -q \
	    "${host}" >/dev/null 2>&1 && {
		server_ping=true
	}
}

_send_mail_alert_v2ray() {
	local type="$1"
	local mail_alert mail_to OMR_SYSNAME mail_subject mail_message
	mail_alert="$(uci -q get omr-tracker.proxy.mail_alert)"
	[ "$mail_alert" != "1" ] && return
	mail_to="$(uci -q get mail.default.to | tr -d '\r\n')"
	case "$mail_to" in
		-*|*[[:space:]]*) mail_to="" ;;
	esac
	[ -z "$mail_to" ] && return
	OMR_SYSNAME="$(uci -q get system.@system[0].hostname)"
	mail_subject="$(uci -q get omr-tracker.defaults.mail_${type}_subject)"
	mail_message="$(uci -q get omr-tracker.defaults.mail_${type}_message)"
	if [ -n "$mail_subject" ] && [ -n "$mail_message" ]; then
		mail_subject=$(echo "$mail_subject" | sed -e "s/%SYSNAME%/$OMR_SYSNAME/g" -e "s/%INTERFACE%/V2Ray Proxy/g" -e "s/%DEVICE%/V2Ray Proxy/g" -e "s/%MESSAGE%/$OMR_TRACKER_STATUS_MSG/g")
		mail_subject="$(printf '%s' "$mail_subject" | tr -d '\r\n')"
		mail_message=$(echo "$mail_message" | sed -e "s/%SYSNAME%/$OMR_SYSNAME/g" -e "s/%INTERFACE%/V2Ray Proxy/g" -e "s/%DEVICE%/V2Ray Proxy/g" -e "s/%MESSAGE%/$OMR_TRACKER_STATUS_MSG/g")
		printf 'Subject: %s\n\n%s\n' "$mail_subject" "$mail_message" | sendmail "$mail_to"
	else
		if [ "$type" = "up" ]; then
			mail_subject="$(printf '%s' "$OMR_SYSNAME: V2Ray Proxy is UP." | tr -d '\r\n')"
			printf 'Subject: %s\n\n' "$mail_subject" | sendmail "$mail_to"
		else
			mail_subject="$(printf '%s' "$OMR_SYSNAME: V2Ray Proxy is down" | tr -d '\r\n')"
			printf 'Subject: %s\n\nConnection failure of V2Ray proxy detected. The reason is "%s".\n' "$mail_subject" "$OMR_TRACKER_STATUS_MSG" | sendmail "$mail_to"
		fi
	fi
}

# The redirect is the "goto v2r_rules_pre_src_<proto>" rule of the
# v2r_rules_pre_<proto> chains. List those chains only: "nft list ruleset"
# printed every rule and every set element (omr-bypass's sets) on each
# check.
_v2ray_redirect_live() {
	nft list chain inet fw4 v2r_rules_pre_tcp 2>/dev/null | grep -q 'goto v2r_rules_pre_src_tcp' ||
		nft list chain inet fw4 v2r_rules_pre_udp 2>/dev/null | grep -q 'goto v2r_rules_pre_src_udp'
}

_v2ray_rules_missing() {
	# After rules_down, the v2r_ chains still exist with base rules -- only the
	# redirect ("goto v2r_rules_pre_src_*") is actually removed. Checking for
	# chain existence alone (grep 'chain v2r') never detects this, so rules_up
	# never gets called once v2ray comes back up (issue #4339).
	if ! _v2ray_redirect_live; then
		return 0
	fi
	return 1
}

_v2ray_rules_present() {
	# Same rationale as _v2ray_rules_missing() above (issue #4339): the v2r_
	# chains persist after rules_down, so bare chain existence ('chain v2r')
	# always says "present" even when torn down. Check the actual redirect.
	if _v2ray_redirect_live; then
		return 0
	fi
	return 1
}

_get_ip() {
	local _rest
	read -r _get_ip_time _rest < /proc/uptime
	_get_ip_time="${_get_ip_time%%.*}"
	[ "$(uci -q get openmptcprouter.omr)" = "router" ] || { uci -q set openmptcprouter.omr=router; uci -q commit openmptcprouter; }
	if [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ]; then
		check_ipv4_website="$(uci -q get openmptcprouter.settings.check_ipv4_website)"
		[ -z "$check_ipv4_website" ] && check_ipv4_website="http://ip.openmptcprouter.com"
		check_ipv6_website="$(uci -q get openmptcprouter.settings.check_ipv6_website)"
		[ -z "$check_ipv6_website" ] && check_ipv6_website="http://ipv6.openmptcprouter.com"
		public_ipv4="$(curl -s -4 -m 3 "$check_ipv4_website")"
		omr_state_set openmptcprouter omr detected_public_ipv4 "${public_ipv4}"
		[ -n "${public_ipv4}" ] && {
			uci -q set upnpd.config.external_ip="${public_ipv4}"
			uci -q commit upnpd
		}
		if [ "$(omr_state_get openmptcprouter.omr.v2ray)" != "down" ]; then
			omr_state_set openmptcprouter omr detected_ss_ipv4 "$(curl -s -4 --socks5 "${proxy}" --max-time 3 "$check_ipv4_website")"
		else
			omr_state_del openmptcprouter omr detected_ss_ipv4
		fi
		if [ "$(uci -q get openmptcprouter.settings.disable_ipv6)" != "1" ]; then
			omr_state_set openmptcprouter omr detected_public_ipv6 "$(curl -s -6 -m 3 "$check_ipv6_website")"
		else
			omr_state_del openmptcprouter omr detected_public_ipv6
		#	uci -q set openmptcprouter.omr.detected_ss_ipv6=$(curl -s -6 --socks5 ":::1111" --max-time 3 http://ip.openmptcprouter.com)
		fi
	fi
}

# Keep the generic proxy state (openmptcprouter.omr.proxy) in sync with this
# tracker's view. Guarded on the current value so the steady state costs one
# uci get per cycle and never a commit.
_set_omr_proxy_state() {
	[ "$(omr_state_get openmptcprouter.omr.proxy)" = "$1" ] && return
	omr_state_set openmptcprouter omr proxy "$1"
}

# _get_ip again when a detected address is missing: the IPv4 one on every
# cycle, the IPv6 one once in 15 minutes. A router without IPv6 never gets
# one, and its 3 curls ran on every healthy cycle for good.
_get_ip_if_missing() {
	local _now _rest
	if [ "$(omr_state_get openmptcprouter.omr.detected_public_ipv4)" = "" ]; then
		_get_ip
	elif [ "$(uci -q get openmptcprouter.settings.disable_ipv6)" != "1" ] && [ "$(omr_state_get openmptcprouter.omr.detected_public_ipv6)" = "" ]; then
		read -r _now _rest < /proc/uptime
		[ $((${_now%%.*} - _get_ip_time)) -ge 900 ] && _get_ip
	fi
}

# The hosts that did not answer since the last success, each one once: the
# list grew by a host on every failed check for as long as an outage lasted
_add_nocontact() {
	case ", $nocontact, " in
		*", $1, "*) ;;
		*) nocontact="${nocontact:+$nocontact, }$1" ;;
	esac
}

timeout=${OMR_TRACKER_TIMEOUT:-5}
interval=${OMR_TRACKER_INTERVAL:-10}
retry=${OMR_TRACKER_TRIES:-4}
proxy=${OMR_TRACKER_PROXY:-127.0.0.1:1111}
hosts=${OMR_TRACKER_HOSTS:-1.1.1.1 1.0.0.1}
wait_test=${OMR_TRACKER_WAIT_TEST:-0}

nodns=0

last=0
nocontact=""
_get_ip_time=0
[ "$(uci -q get openmptcprouter.omr)" = "router" ] || { uci -q set openmptcprouter.omr=router; uci -q commit openmptcprouter; }
omr_state_del openmptcprouter omr v2ray
_get_ip

# See omr-tracker-xray for why: avoids racing a freshly (re)launched
# instance's first check against the daemon (re)start/reload cascade that
# just spawned it.
sleep 15

OMR_TRACKER_PREV_STATUS=""
while true; do
	host="${hosts%% *}"
	[ "$host" = "$hosts" ] || {
		hosts="${hosts#* } $host"
	}
	if [ "$(curl -s -I -w '%{http_code}' --socks5 "${proxy}" --max-time "${timeout}" "$host" -o /dev/null)" != "000" ]; then
		nocontact=""
		[ "${last}" -ge "${retry}" ] || [ "$(omr_state_get openmptcprouter.omr.v2ray)" = "" ] && {
			_log "V2Ray is up (can contact via http ${host})"
			OMR_TRACKER_STATUS_MSG="V2Ray is up (can contact via http ${host})"
			omr_state_set openmptcprouter omr v2ray "up"
			/etc/init.d/openmptcprouter-vps set_vps_firewall
			_send_mail_alert_v2ray "up"
			script_alert_up="$(uci -q get omr-tracker.proxy.script_alert_up)"
			[ -n "$script_alert_up" ] && eval $script_alert_up
			_get_ip
		}
		if _v2ray_rules_missing; then
			_log "Reload V2Ray rules"
			/etc/init.d/v2ray rules_up 2> /dev/null
			/etc/init.d/openmptcprouter-vps set_vps_firewall >/dev/null 2>&1
			set_server_fw() {
				_log "Set firewall on server $1"
				/etc/init.d/openmptcprouter-vps set_vps_firewall "$1" >/dev/null 2>&1
			}
			config_load openmptcprouter
			config_foreach set_server_fw server
			_get_ip
		fi
		_get_ip_if_missing
		last=0
		OMR_TRACKER_STATUS="OK"
		_set_omr_proxy_state "up"
	else
		last=$((last + 1 ))
		_add_nocontact "$host"
		[ "${last}" -ge "${retry}" ] && {
			if _v2ray_rules_present; then
				_log "V2Ray is down (can't contact via http ${nocontact})"
				OMR_TRACKER_STATUS_MSG="V2Ray is down (can't contact via http ${nocontact})"
				omr_state_set openmptcprouter omr v2ray "down"
				_set_omr_proxy_state "down"
				/etc/init.d/openmptcprouter-vps set_vps_firewall
				/etc/init.d/v2ray rules_down 2> /dev/null
				_get_ip
				server_ping=false
				server="$(uci -q get v2ray.omrout.s_vless_address)"
				_ping_server "$server"
				if [ "$server_ping" = false ]; then
					_log "Server ($server) seems down, no answer to ping"
					OMR_TRACKER_STATUS_MSG="${OMR_TRACKER_STATUS_MSG} - Server ($server) seems down, no answer to ping"
				fi
				_send_mail_alert_v2ray "down"
				script_alert_down="$(uci -q get omr-tracker.proxy.script_alert_down)"
				[ -n "$script_alert_down" ] && eval $script_alert_down
				sleep $wait_test
			elif [ ! -f /etc/nftables.d/90-proxy-v2ray-rules.nft.down ] && [ -z "$(pgrep -f '/etc/[a-z.]*/[KS0-9]*v2ray ')" ]; then
				# Rules neither loaded nor parked, and no v2ray start or
				# restart running (it clears them first): v2ray never set
				# them up, a restart writes them. Parked rules are down on
				# purpose, by this tracker during an outage or by the v2ray
				# start until the proxy answers (wait_proxy): a restart can't
				# make the server answer, and each one reloads and restarts
				# fw4, which empties the sets dnsmasq fills for omr-bypass.
				_log "V2Ray SOCKS5 unreachable and rules missing, restarting V2Ray"
				# out of this tracker's cgroup and stderr: procd kills what is
				# left in the cgroup when the tracker stops (omr-tracker
				# restart), and a restart killed half-way could leave v2ray
				# stopped and omr-bypass's lock behind
				sh -c 'echo $$ >/sys/fs/cgroup/cgroup.procs 2>/dev/null; exec /etc/init.d/v2ray restart' >/dev/null 2>&1 &
				last=0
				sleep $wait_test
			fi
		}
		OMR_TRACKER_STATUS="ERROR"
	fi
	_post_tracking
	OMR_TRACKER_PREV_STATUS="$OMR_TRACKER_STATUS"
	sleep "${interval}"
done
