#!/bin/bash
# OpenMPTCProuter VPS service script

# Unit files are installed and removed by the install script, which restarts
# this service at the end: once a unit is found, don't ask systemd again on
# every loop. A unit not found (or a failed lookup) is looked up again.
_unit_exists() {
	declare -gA _unit_found
	[ -n "${_unit_found[$1]}" ] && return 0
	systemctl list-unit-files --no-legend "$1" 2>/dev/null | grep -q '[^[:space:]]' || return 1
	_unit_found[$1]=1
}

# Flags of the first MPTCP endpoint on a device, found the way multipath
# finds them (dev field compared whole, flags in the 4th field), in the
# endpoint list _multipath read: empty when the device has no endpoint.
# A substring match on "dev <device>" would give eth0 the flags of eth0.100.
_endpoint_mode() {
	local -a f
	local i
	endpoint_mode=""
	while read -r -a f; do
		for ((i = 0; i < ${#f[@]} - 1; i++)); do
			if [ "${f[i]}" = dev ] && [ "${f[i+1]}" = "$1" ]; then
				endpoint_mode="${f[3]}"
				return
			fi
		done
	done <<< "$endpoints"
}

_multipath() {
	local intf lo_global endpoints=""
	# Force multipath status
	# NET_IFACE used to come from Shorewall's params.net; derived directly
	# from the default route now that Shorewall is gone (matches
	# omradmin.py's own _detect_default_iface()).
	NET_IFACE="$(ip -4 route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if ($i=="dev"){print $(i+1); exit}}')"
	# Same answer for every interface: look once, not up to twice per interface
	lo_global="$(ip a show dev lo | grep -v inet6 | grep global)"
	# With the upstream MPTCP stack, "multipath <device>" lists the endpoints
	# three times and runs ip | jq just to print the mode: with the lo check
	# above, some 25 processes per interface every 10 s, on a VPS with about
	# twenty tunnel interfaces. Read the list once and only run multipath to
	# change a mode.
	if [ -f /proc/sys/net/mptcp/enabled ]; then
		endpoints="$(ip mptcp endpoint show 2>/dev/null)"
		# Each multipath call also deleted IPv4-mapped endpoints: still do it
		case "$endpoints" in
			*::ffff*) multipath lo >/dev/null 2>&1 ;;
		esac
	fi
	for intf in `ls -1 /sys/class/net`; do
		if [ "$intf" != "bonding_masters" ]; then
			if { [ -n "$lo_global" ] && [ "$intf" = "lo" ]; } || { [ "$intf" = "$NET_IFACE" ] && [ -z "$lo_global" ]; }; then
				[ -f /proc/sys/net/mptcp/mptcp_enabled ] && [ "$(multipath $intf | tr -d '\n')" != "$intf is in default mode" ] && multipath $intf on >/dev/null 2>&1
				if [ -f /proc/sys/net/mptcp/enabled ]; then
					_endpoint_mode "$intf"
					[ "$endpoint_mode" != "signal" ] && {
						multipath $intf signal >/dev/null 2>&1
						ip mptcp limits set subflows 8 add_addr_accepted 8 >/dev/null 2>&1
					}
				fi
			elif [ -f /proc/sys/net/mptcp/enabled ]; then
				_endpoint_mode "$intf"
				[ -n "$endpoint_mode" ] && multipath $intf off >/dev/null 2>&1
			else
				[ "$(multipath $intf | tr -d '\n')" != "$intf is deactivated" ] && multipath $intf off >/dev/null 2>&1
			fi
		fi
	done
}

_glorytun_udp() {
	#if [ -n "$(systemctl -a | grep 'glorytun-udp')" ]; then
	if _unit_exists glorytun-udp@.service; then
		# The unit template being installed doesn't mean any user actually
		# selected Glorytun-UDP (the install script sets it up unconditionally).
		# Only chase a missing tunnel when it's the currently active VPN,
		# same gating _glorytun_tcp()/_dsvpn() use, to avoid restarting an
		# unused tunnel in a loop (https://github.com/Ysurac/openmptcprouter-vps/issues/131).
		if [ -f /etc/openmptcprouter-vps-admin/current-vpn ] && [ "$(cat /etc/openmptcprouter-vps-admin/current-vpn)" = "glorytun_udp" ]; then
			[ -z "$(glorytun show dev gt-udp-tun0 2>/dev/null | grep tunnel)" ] && {
				logger -t "OMR-Service" "Restart Glorytun-UDP"
				systemctl -q restart 'glorytun-udp@*'
				sleep 10
			}
			if [ -z "$(pgrep post.sh)" ]; then
				for intf in /etc/glorytun-udp/tun*; do
					[ "$(echo $intf | grep key)" = "" ] && timeout 70 /etc/glorytun-udp/post.sh ${intf} &
				done
				#ip link set mtu 9000 dev gt-udp-tun0 >/dev/null 2>&1
			fi
		fi
	fi
}

_glorytun_tcp() {
	#if [ -n "$(systemctl -a | grep 'glorytun-tcp')" ]; then
	if _unit_exists glorytun-tcp@.service; then
		for intf in /etc/glorytun-tcp/tun*; do
			[ "$(echo $intf | grep key)" = "" ] && timeout 10 /etc/glorytun-tcp/post.sh ${intf}
		done
		if [ -f /etc/openmptcprouter-vps-admin/current-vpn ] && [ "$(cat /etc/openmptcprouter-vps-admin/current-vpn)" = "glorytun_tcp" ]; then
			localip="$(cat /etc/glorytun-tcp/tun0 | grep LOCALIP | cut -d '=' -f2)"
			[ -z "$localip" ] && localip="10.255.255.1"
			remoteip="$(printf '%s\n' "$localip" | sed 's/\.1$/.2/')"
			if [ "$(ping -c 6 -w 50 $remoteip | grep '100%')" != "" ] && ([ -z "$(pgrep glorytun-tcp)" ] || [ "$(expr $(date +%s) - $(stat -c %Y /proc/$(pgrep glorytun-tcp)/exe ))" -gt "600" ]); then
				logger -t "OMR-Service" "No answer from VPN client end, restart Glorytun-TCP"
				systemctl restart glorytun-tcp@tun0
				sleep 10
			fi
		fi
		#ip link set mtu 9000 dev gt-tun0 >/dev/null 2>&1
	fi
}

_dsvpn() {
	#if [ -n "$(systemctl -a | grep 'dsvpn')" ]; then
	if _unit_exists dsvpn-server@.service; then
		[ -n "$(ip -6 r show 64:ff9b::/96 dev dsvpn0)" ] && ip -6 r del 64:ff9b::/96 dev dsvpn0 >/dev/null 2>&1
		if [ -f /etc/openmptcprouter-vps-admin/current-vpn ] && [ "$(cat /etc/openmptcprouter-vps-admin/current-vpn)" = "dsvpn" ]; then
			localip="$(cat /etc/dsvpn/dsvpn0 | grep LOCALTUNIP | cut -d '=' -f2)"
			[ -z "$localip" ] && localip="10.255.251.1"
			remoteip="$(printf '%s\n' "$localip" | sed 's/\.1$/.2/')"
			if [ "$(ping -c 5 -w 5 $remoteip | grep '100%')" != "" ] && [ "$(expr $(date +%s) - $(stat -c %Y /proc/$(pgrep dsvpn)/exe ))" -gt "300" ]; then
				logger -t "OMR-Service" "No answer from VPN client end, restart DSVPN"
				systemctl restart dsvpn-server@dsvpn0
			fi
			#ip link set mtu 9000 dev dsvpn0 >/dev/null 2>&1
		fi
	fi
}

_softethervpn() {
	if _unit_exists softether-vpnserver.service; then
		if [ -z "$(ip a show dev tap_softether | grep '10.255.210.1')" ]; then
			ip a add 10.255.210.1/24 dev tap_softether >/dev/null 2>&1
			ip link set promisc on tap_softether >/dev/null 2>&1
		fi
	fi
}

_shadowsocks() {
	if _unit_exists shadowsocks-libev-manager@.service; then
		[ -z "$(pgrep ss-server)" ] && {
			logger -t "OMR-Service" "ss-server not detected, restart Shadowsocks libev"
			systemctl restart shadowsocks-libev-manager@manager
		}
	fi
}

_shadowsocks_go() {
	if _unit_exists shadowsocks-go.service; then
		[ -z "$(pgrep shadowsocks-go)" ] && {
			logger -t "OMR-Service" "shadowsocks-go not detected, restart Shadowsocks go"
			systemctl restart shadowsocks-go
		}
	fi
}

_xray() {
	if _unit_exists xray.service; then
		[ -z "$(pgrep xray)" ] && {
			logger -t "OMR-Service" "xray not detected, restart XRay"
			systemctl restart xray
		}
	fi
}

_v2ray() {
	if _unit_exists v2ray.service; then
		[ -z "$(pgrep v2ray)" ] && {
			logger -t "OMR-Service" "v2ray not detected, restart V2Ray"
			systemctl restart v2ray
		}
	fi
}

_wireguard() {
	#if [ -n "$(systemctl -a | grep 'wg')" ]; then
	if _unit_exists wg-quick@.service; then
		[ -z "$(ip a show dev wg0 | grep '10.255.247.1')" ] && ip a add 10.255.247.1/24 dev wg0 >/dev/null 2>&1
		[ -z "$(ip a show dev client-wg0 | grep '10.255.246.1')" ] && ip a add 10.255.246.1/24 dev client-wg0 >/dev/null 2>&1
	fi
}

_mqvpn() {
	if _unit_exists mqvpn.service; then
		[ -z "$(pgrep mqvpn)" ] && {
			logger -t "OMR-Service" "mqvpn not detected, restart MQVPN"
			systemctl restart mqvpn
		}
	fi
}


# omr-admin needs well over one loop pass to answer after a (re)start on a
# small VPS, longer still at boot: restarting it as soon as the API is silent
# kills every start before it finishes. Leave it 90s after it became active;
# a unit that is not active (failed, start limit hit) is still restarted.
_omr_api() {
	local state since now
	state="$(systemctl show -P ActiveState omr-admin 2>/dev/null)"
	[ "$state" = "activating" ] && return
	if [ "$state" = "active" ]; then
		since="$(systemctl show -P ActiveEnterTimestampMonotonic omr-admin 2>/dev/null)"
		now="$(cut -d. -f1 /proc/uptime)"
		[ -n "$since" ] && [ $((now - since / 1000000)) -lt 90 ] && return
	fi
	[ -z "$(pgrep curl)" ] && [ -z "$(curl -s -k -m 30 https://127.0.0.1:65500/)" ] && {
		logger -t "OMR-Service" "Can't contact API, restart OMR-Admin"
		systemctl -q restart omr-admin
	}
}

# True if $1 (a network as ipcalc prints it, a.b.c.d/prefix) is a router LAN
# that may be routed to it: inside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
# or 100.64.0.0/10, and clear of the VPS's own tunnel networks in
# 10.255.0.0/16. Same rule as omradmin.py's _lan_network(): the API only
# checks this with client2client, so a stored config can still hold
# 0.0.0.0/0 or a 10.255.x subnet. Overlap between users is fine here.
_lan_routable() {
	local addr prefix a b c d octet value allowed net len mask
	addr="${1%/*}"
	prefix="${1#*/}"
	[ "$prefix" = "$1" ] && prefix=32
	[[ "$prefix" =~ ^[0-9]{1,2}$ ]] && [ "$(( 10#$prefix ))" -le 32 ] || return 1
	IFS=. read -r a b c d <<< "$addr"
	for octet in "$a" "$b" "$c" "$d"; do
		[[ "$octet" =~ ^[0-9]{1,3}$ ]] && [ "$(( 10#$octet ))" -le 255 ] || return 1
	done
	value=$(( (10#$a << 24) | (10#$b << 16) | (10#$c << 8) | 10#$d ))
	prefix=$(( 10#$prefix ))
	# Overlaps 10.255.0.0/16: equal on the shorter of the two prefixes.
	len=$(( prefix < 16 ? prefix : 16 ))
	mask=$(( len ? (0xffffffff << (32 - len)) & 0xffffffff : 0 ))
	[ $(( value & mask )) -eq $(( (10 << 24 | 255 << 16) & mask )) ] && return 1
	for allowed in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10; do
		len="${allowed#*/}"
		IFS=. read -r a b c d <<< "${allowed%/*}"
		net=$(( (a << 24) | (b << 16) | (c << 8) | d ))
		mask=$(( (0xffffffff << (32 - len)) & 0xffffffff ))
		[ "$prefix" -ge "$len" ] && [ $(( value & mask )) -eq "$net" ] && return 0
	done
	return 1
}

# The users' LANs only change with omr-admin-config.json, but reading them
# took 3 jq per user and 3 ipcalc (a Perl script) per LAN every 10 s: parse
# the file again only when it changed, and just check the routes and ccd
# entries in between.
_lan_route_key=""
_lan_route_list=""
_lan_route() {
	local config=/etc/openmptcprouter-vps-admin/omr-admin-config.json key network networkonly netmask vpnremoteip username
	key="$(stat -c '%y %z %s %i' "$config" 2>/dev/null)"
	if [ -z "$key" ] || [ "$key" != "$_lan_route_key" ]; then
		_lan_route_list="$(jq -c '.users[0][]?' "$config" |
		while IFS=$"\n" read -r c; do
			if [ -n "$c" ]; then
				vpnremoteip=$(echo "$c" | jq -r '.vpnremoteip')
				username=$(echo "$c" | jq -r '.username')
				if [ -n "$vpnremoteip" ] && [ "$vpnremoteip" != "null" ]; then
					echo "$c" | jq -c -r '.lanips[]? //empty' |
					while IFS=$"\n" read -r d; do
						if [ "$d" != "" ]; then
							network=$(ipcalc -n $d | grep Network | awk '{print $2}')
							networkonly=$(ipcalc -n $d | grep Network | awk '{print $2}' | cut -d/ -f1)
							netmask=$(ipcalc -n $d | grep Netmask | awk '{print $2}')
							_lan_routable "$network" || continue
							printf '%s|%s|%s|%s|%s\n' "$network" "$networkonly" "$netmask" "$vpnremoteip" "$username"
						fi
					done
				fi
			fi
		done)"
		_lan_route_key="$key"
	fi
	while IFS='|' read -r network networkonly netmask vpnremoteip username; do
		[ -n "$network" ] && [ -z "$(ip r show $network via $vpnremoteip)" ] && ip r replace $network via $vpnremoteip >/dev/null 2>&1
		[ -n "$networkonly" ] && [ -n "$netmask" ] && ([ ! -f /etc/openvpn/ccd/${username} ] || ! grep -Fqx -- "iroute $networkonly $netmask" /etc/openvpn/ccd/${username}) && echo "iroute $networkonly $netmask" >> /etc/openvpn/ccd/${username}
	done <<< "$_lan_route_list"
}

_gre_tunnels() {
	# OMR_ADDR used to come from Shorewall's params.vpn; read directly from
	# the same omr-admin-config.json field omradmin.py's nft engine now
	# treats as the source of truth (see _render_fw_ports there). It is the
	# vpnremoteip of the user each intf file belongs to: add_gre_tunnels()
	# writes files for other users too and records the owner as USERNAME.
	# Files written before USERNAME existed belong to openmptcprouter.
	for intf in /etc/openmptcprouter-vps-admin/intf/*; do
		if [ -f "$intf" ]; then
			# A file that lacks a key must not inherit the previous file's.
			INTF="" INTFADDR="" INTFNETMASK="" NETWORK="" LOCALIP="" REMOTEIP="" NETMASK="" BROADCASTIP="" USERNAME="" USERID=""
			. "$(readlink -f "$intf")"
			OMR_ADDR="$(jq -r --arg user "${USERNAME:-openmptcprouter}" '.users[0][$user].vpnremoteip // empty' /etc/openmptcprouter-vps-admin/omr-admin-config.json)"
			[ -n "$OMR_ADDR" ] || continue
			iface="$(basename $intf)"
			if [ "$(ip tunnel show $iface 2>/dev/null | awk '{print $4}')" != "$OMR_ADDR" ]; then
				[ -n "$(ip tunnel show $iface 2>/dev/null)" ] && ip tunnel del $iface >/dev/null 2>&1
				ip tunnel add $iface mode gre local $INTFADDR remote $OMR_ADDR >/dev/null 2>&1
				ip link set $iface up >/dev/null 2>&1
				ip addr add $LOCALIP dev $iface >/dev/null 2>&1
				ip route add $NETWORK dev $iface >/dev/null 2>&1
			fi
		fi
	done
}

_openvpn_bonding() {
	if [ "$(ip link show ovpnbonding1 2>/dev/null)" != "" ] && ([ "$(ip link show ovpnbonding1 2>/dev/null | grep SLAVE)" = "" ] || [ "$(ip link show omr-bonding 2>/dev/null | grep DOWN)" != "" ] || [ "$(ip link show | grep ovpnbonding | grep -c SLAVE | tr -d '\n')" != "8" ]); then
		echo 0 > /sys/class/net/omr-bonding/bonding/mode >/dev/null 2>&1
		ip link set ovpnbonding1 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding1 up >/dev/null 2>&1
		ip link set ovpnbonding2 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding2 up >/dev/null 2>&1
		ip link set ovpnbonding3 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding3 up >/dev/null 2>&1
		ip link set ovpnbonding4 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding4 up >/dev/null 2>&1
		ip link set ovpnbonding5 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding5 up >/dev/null 2>&1
		ip link set ovpnbonding6 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding6 up >/dev/null 2>&1
		ip link set ovpnbonding7 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding7 up >/dev/null 2>&1
		ip link set ovpnbonding8 master omr-bonding >/dev/null 2>&1
		ip link set ovpnbonding8 up >/dev/null 2>&1
		ip link set omr-bonding up mtu 1440 >/dev/null 2>&1
		ip a add 10.255.248.1 dev omr-bonding >/dev/null 2>&1
		ip r add 10.255.248.0/24 dev omr-bonding >/dev/null 2>&1
		ip r add 10.255.248.2 dev omr-bonding src 10.255.248.1 >/dev/null 2>&1
	fi
}

_vpn1() {
	local vpn1route
	vpn1route="$(ip -4 route show default dev vpn1 2>/dev/null | head -n 1)"
	if [ -n "$vpn1route" ]; then
		# Preserve each route as one argument vector. Iterating over $(ip route)
		# splits a route into words and produces commands such as "ip route add
		# via table 991337". Install the policy-table copies before removing the
		# main-table default so a failed copy cannot cut connectivity.
		# "ip route show dev vpn1" leaves the dev field out of its output
		# (it is the filter), so it is put back on each copy and on the del:
		# without it the connected route fails with "No such device".
		while IFS= read -r route; do
			[ -n "$route" ] || continue
			read -r -a route_args <<< "$route"
			ip -4 route replace table 991337 "${route_args[@]}" dev vpn1 || return
		done < <(ip -4 route show dev vpn1 2>/dev/null)
		read -r -a default_args <<< "$vpn1route"
		ip -4 route del "${default_args[@]}" dev vpn1
	fi
}

sysctl -p /etc/sysctl.d/90-shadowsocks.conf >/dev/null 2>&1
# 90-shadowsocks.conf carries the configured tcp_rmem/tcp_wmem; omr-net-mem's
# file holds what this VPS's RAM allows. Re-applying only the former would
# undo that on every omr restart, so the generated one goes on top.
[ -f /etc/sysctl.d/99-omr-net-mem.conf ] && sysctl -p /etc/sysctl.d/99-omr-net-mem.conf >/dev/null 2>&1
modprobe bonding >/dev/null 2>&1
ip link add omr-bonding type bond >/dev/null 2>&1
#[ -n "$(uname -r | grep '6.1')" ] && {
#	stap -g /usr/share/systemtap-mptcp/mptcp-app.stap 2>&1 &
#}

gre_tunnels="$(jq -c '.gre_tunnels' /etc/openmptcprouter-vps-admin/omr-admin-config.json)"
lan_routes="$(jq -c '.lan_routes' /etc/openmptcprouter-vps-admin/omr-admin-config.json)"

while true; do
	_glorytun_udp
	_glorytun_tcp
	_shadowsocks
	_shadowsocks_go
	_xray
	_v2ray
	_dsvpn
	_softethervpn
	_wireguard
	_mqvpn
	_multipath
	_omr_api
	[ "$lan_routes" != "false" ] && _lan_route
	[ "$gre_tunnels" != "false" ] && _gre_tunnels
	_openvpn_bonding
	_vpn1
	sleep 10
done
