#!/bin/sh /etc/rc.common
# Copyright (C) 2018-2026 Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
# Released under GPL 3. See LICENSE for the full terms.

START=85

USE_PROCD=1

EXTRA_COMMANDS="set_pihole backup_send backup_get backup_list set_vps_firewall get_openvpn_key set_gre_tunnel set_vxlan token set_bypass_ips set_vpn_ip set_mptcp_dscp_vps set_mptcp_weight_vps set_mptcp_pins_vps set_dscp_classify_vps set_wireguard"

. /usr/lib/unbound/iptools.sh
# omr_vps_curl: curl with the VPS API certificate public key pinned
. "${OMR_LIB_DIR:-/usr/share/omr/lib}/omr-vps-curl.sh"

if [ -e /usr/sbin/iptables-nft ]; then
	IPTABLES="/usr/sbin/iptables-nft"
	IPTABLESRESTORE="/usr/sbin/iptables-nft-restore"
	IPTABLESSAVE="/usr/sbin/iptables-nft-save"
	IP6TABLES="/usr/sbin/ip6tables-nft"
	IP6TABLESRESTORE="/usr/sbin/ip6tables-nft-restore"
	IP6TABLESSAVE="/usr/sbin/ip6tables-nft-save"
else
	IPTABLES="/usr/sbin/iptables"
	IPTABLESRESTORE="/usr/sbin/iptables-restore"
	IPTABLESSAVE="/usr/sbin/iptables-save"
	IP6TABLES="/usr/sbin/ip6tables"
	IP6TABLESRESTORE="/usr/sbin/ip6tables-restore"
	IP6TABLESSAVE="/usr/sbin/ip6tables-save"
fi

# Overridable so the test suite can point this at a mock instead of shelling
# out to the real init script
FIREWALL_INIT="${FIREWALL_INIT:-/etc/init.d/firewall}"


_parse_result() {
	result=$(echo "$1" | jsonfilter -q -e '@.result')
	echo $result
}

# Failed logins are logged once, then at most every 10 minutes per server
# with the count held back, and once more when the server answers again.
# While the VPS API restarts, each caller that wanted a token logged a line of
# its own: the Pi-hole sync alone asks on every tracker cycle. The old guard
# (the last 3 log lines all saying "vps token") never matched most messages.
# A failure more than an hour after the last one starts a new count.
_token_failed() { # <server> <message>
	local marker="/tmp/.omr-vps-tokenfail-$1" now first last count seen
	now="$(date +%s)"
	[ -f "$marker" ] && read -r first last count seen < "$marker"
	if [ -z "$seen" ] || [ "$((now - seen))" -ge 3600 ]; then
		first=$now last=0 count=0
	fi
	count=$((count + 1))
	if [ "$((now - last))" -ge 600 ]; then
		if [ "$count" -gt 1 ]; then
			logger -t "OMR-VPS" "$2 (failing for $((now - first))s, $((count - 1)) more failed logins)"
		else
			logger -t "OMR-VPS" "$2"
		fi
		last=$now
	fi
	echo "$first $last $count $now" > "$marker"
}

_token_ok() { # <server>
	local marker="/tmp/.omr-vps-tokenfail-$1" now first last count seen
	[ -f "$marker" ] || return 0
	read -r first last count seen < "$marker"
	rm -f "$marker"
	now="$(date +%s)"
	[ -n "$seen" ] && [ "$((now - seen))" -lt 3600 ] && \
		logger -t "OMR-VPS" "Server $1 API answers again, after ${count} failed logins in $((now - first))s"
	return 0
}

# The token is shared with omr-metrics and the status page, which read it
# with "uci get": it stays in the config. Left staged, the next "uci commit
# openmptcprouter" of any other process (mptcp's on each interface event)
# wrote it to flash with its own changes. Committed here, but only when it
# is the only change pending: a commit flushes every other process's too.
# Otherwise it goes with the commit that follows them (start_service's).
_token_save() {
	[ "$(uci -q get openmptcprouter.${servername}.token)" = "$token" ] && return 0
	uci -q set openmptcprouter.${servername}.token="$token"
	[ -z "$(uci -q changes openmptcprouter | grep -v -E "^-?openmptcprouter\.${servername}\.token(=|\$)")" ] && \
		uci -q commit openmptcprouter
	return 0
}

_login() {
	local username password auth
	#server="$(uci -q get openmptcprouter.${servername}.ip)"
	#[ -z "$server" ] && server="$(uci -q get shadowsocks-libev.sss0.server)"
	username="$(uci -q get openmptcprouter.${servername}.username)"
	password="$(uci -q get openmptcprouter.${servername}.password)"
	serverport="$(uci -q get openmptcprouter.${servername}.port)"
	#[ -z "$server" ] && server="$(uci -q get openmptcprouter.${servername}.ip)"
	if [ -z "$username" ] || [ -z "$password" ]; then
		logger -t "OMR-VPS" "No credentials for server ${servername} yet, not asking for a token"
		return
	fi
	if [ -z "$token" ]; then
		login_on_server() {
			[ -n "$token" ] && return
			server=$1
			#auth=`curl --max-time 10 -s -k -H "Content-Type: application/json" -X POST -d '{"username":"'$username'","password":"'$password'"}' https://$server:$serverport/login`
			#resolve="$(resolveip -t 5 $server)"
			valid_ip6=$(valid_subnet6 $server)
			# curl URL-encodes both fields and reads the password on stdin:
			# pasted raw into the form body, a '&', '+', '=' or '%' in the
			# key reached the API altered, and on the command line it
			# showed in ps.
			if [ "$valid_ip6" = "ok" ]; then
				auth=`printf '%s' "$password" | omr_vps_curl -6 --max-time 10 -s -k -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -X POST --data-urlencode "username=${username}" --data-urlencode "password@-" https://[$server]:$serverport/token`
			else
				auth=`printf '%s' "$password" | omr_vps_curl --max-time 10 -s -k -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -X POST --data-urlencode "username=${username}" --data-urlencode "password@-" https://$server:$serverport/token`
			fi
			# An answer that carries no access_token is the server rejecting
			# us, not a broken path: retrying it once per interface only adds
			# fail2ban strikes. Only retry when nothing came back at all.
			if [ -z "$auth" ]; then
				for intf in $(multipath 2>/dev/null | awk '/default/ { print $1 }'); do
					if [ "$valid_ip6" = "ok" ]; then
						auth=`printf '%s' "$password" | omr_vps_curl -6 --max-time 10 -s -k --interface $intf -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -X POST --data-urlencode "username=${username}" --data-urlencode "password@-" https://[$server]:$serverport/token`
					else
						auth=`printf '%s' "$password" | omr_vps_curl --max-time 10 -s -k --interface $intf -H "accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -X POST --data-urlencode "username=${username}" --data-urlencode "password@-" https://$server:$serverport/token`
					fi
					[ -n "$auth" ] && break
				done
			fi
			[ -n "$auth" ] && {
				token="$(echo "$auth" | jsonfilter -q -e '@.access_token')"
				_token_save
			}
		}
		config_load openmptcprouter
		config_list_foreach ${servername} ip login_on_server
	fi
	[ -n "$token" ] && _token_ok "$servername"
}

_ping_server() {
	server="$1"
	ret=$(ping -c 3 -w 3 -Q 184 $server 2>&1) && echo "$ret" | grep -sq "bytes from" && return
	false
}

# Report a call the server refused. A refused call used to be
# indistinguishable from an accepted one: the body is handed back to the caller
# either way and nobody looks at it, so whatever the server rejected simply
# never happened, with nothing in any log. That is how a port forward set to
# "IPv4 and IPv6" vanished -- it sends ipproto=any, which omradmin's strict
# ipv4/ipv6 enum answers 422 (issue #4369) -- and how a 500 on GET /config went
# unnoticed for days (issue #4367).
#
# FastAPI reports its own rejections as {"detail": ...} and an unhandled crash
# as a non-JSON body; no OMR endpoint returns either on success. A handled
# {"error": ...} stays quiet on purpose: "Read only user" and friends are
# routine. So does an empty result -- that means the transport failed, every
# caller already handles it, and logging it would fill the log whenever the VPS
# is simply unreachable.
#
# Rate-limited per route an hour apart, so a lasting misconfiguration keeps
# reminding instead of scrolling past once or spamming every pass. /tmp is
# tmpfs, so the timers reset on reboot.
_api_log_refusal() {
	local route result marker now last
	route="$1"
	result="$2"
	case "$result" in
		'{'*'"detail"'*|[!{]*) ;;
		*) return ;;
	esac
	marker="/tmp/.omr-apierr-$(printf '%s' "$route" | tr -c 'A-Za-z0-9' '_')"
	now="$(date +%s)"
	last="$(cat "$marker" 2>/dev/null)"
	[ -z "$last" ] && last=0
	[ "$((now - last))" -ge 3600 ] && {
		logger -t "OMR-VPS" "Server refused ${route}: $(echo $result | cut -c1-200)"
		printf '%s' "$now" > "$marker"
	}
	return 0
}

# VPS answers end up in uci batch here-docs (set x=$value): a value that
# jsonfilter decodes to a newline would start a uci command of its own, so the
# VPS could set any option on the router. No field is multi-line (keys and
# certificates come base64), so drop the \n and \r escapes; escaped
# backslashes are kept as they are. json-c also takes raw control bytes inside
# a string and gives them back as they are: those become spaces and the lines
# are joined, which is still valid JSON between the tokens.
_json_one_line() {
	tr '\000-\011\013-\037' ' ' | sed -e ':a' -e '$!N' -e '$!ba' -e 's/\n/ /g' \
		-e 's/\\\\/@@OMRBS@@/g' -e 's/\\[nr]//g' -e 's/\\u000[aAdD]//g' -e 's/@@OMRBS@@/\\\\/g'
}

_get_json() {
	local route result
	route=$1
	[ -z "$token" ] && _login
	[ -n "$token" ] && {
		#resolve="$(resolveip -t 5 $server)"
		valid_ip6=$(valid_subnet6 $server)
		if [ "$valid_ip6" = "ok" ]; then
			result=`omr_vps_curl -6 --max-time 10 -s -k -H "accept: application/json" -H "Authorization: Bearer $token" https://[$server]:$serverport/$route`
		else
			result=`omr_vps_curl --max-time 10 -s -k -H "accept: application/json" -H "Authorization: Bearer $token" https://$server:$serverport/$route`
		fi
		if [ -z "$result" ]; then
			for intf in $(multipath 2>/dev/null | awk '/default/ { print $1 }'); do
				if [ "$valid_ip6" = "ok" ]; then
					result=`omr_vps_curl -6 --max-time 10 -s -k --interface $intf -H "accept: application/json" -H "Authorization: Bearer $token" https://[$server]:$serverport/$route`
				else
					result=`omr_vps_curl --max-time 10 -s -k --interface $intf -H "accept: application/json" -H "Authorization: Bearer $token" https://$server:$serverport/$route`
				fi
				[ -n "$result" ] && break
			done
		fi
		if [ "$(echo $result | grep 'Could not validate credentials')" ]; then
			token=''
			_login
			if [ "$valid_ip6" = "ok" ]; then
				result=`omr_vps_curl -6 --max-time 10 -s -k -H "accept: application/json" -H "Authorization: Bearer $token" https://[$server]:$serverport/$route`
			else
				result=`omr_vps_curl --max-time 10 -s -k -H "accept: application/json" -H "Authorization: Bearer $token" https://$server:$serverport/$route`
			fi
			[ "$(echo $result | grep 'Could not validate credentials')" ] && result=''
		fi
		_api_log_refusal "$route" "$result"
		# Quoted: unquoted, a * or ? in the answer was expanded to file names
		echo "$result" | _json_one_line
	} || {
		echo ''
	}
}

# The body of _set_json: $settings, or the file $datafile when one is given
_set_json_body() {
	if [ -n "$datafile" ]; then
		cat "$datafile"
	else
		printf '%s' "$settings"
	fi
}

# _set_json <route> <json> [<file>]: with <file>, the body is read from it.
# Given as "@<file>" in <json>, the path itself was the body: the VPS got no
# backup, answered 422, and the backup was reported sent.
_set_json() {
	local route result settings datafile
	route=$1
	settings="$2"
	datafile="$3"
	[ -z "$token" ] && _login
	[ -n "$token" ] && {
		#resolve="$(resolveip -t 5 $server)"
		valid_ip6=$(valid_subnet6 $server)
		# The body goes on stdin (printf is an ash builtin): as a -d
		# argument, a body over 128 KB (the bypass list of a large ASN)
		# made the exec of curl fail with E2BIG, every time
		if [ "$valid_ip6" = "ok" ]; then
			result=`_set_json_body | omr_vps_curl -6 --max-time 10 -s -k -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://[$server]:$serverport/$route`
		else
			result=`_set_json_body | omr_vps_curl --max-time 10 -s -k -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://$server:$serverport/$route`
		fi
		if [ -z "$result" ]; then
			for intf in $(multipath 2>/dev/null | awk '/default/ { print $1 }'); do
				if [ "$valid_ip6" = "ok" ]; then
					result=`_set_json_body | omr_vps_curl -6 --max-time 10 -s -k --interface $intf -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://[$server]:$serverport/$route`
				else
					result=`_set_json_body | omr_vps_curl --max-time 10 -s -k --interface $intf -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://$server:$serverport/$route`
				fi
				[ -n "$result" ] && break
			done
		fi
		if [ "$(echo $result | grep 'Could not validate credentials')" ]; then
			token=''
			_login
			if [ "$valid_ip6" = "ok" ]; then
				result=`_set_json_body | omr_vps_curl -6 --max-time 10 -s -k -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://[$server]:$serverport/$route`
			else
				result=`_set_json_body | omr_vps_curl --max-time 10 -s -k -H "Authorization: Bearer $token" -H "Content-Type: application/json" -X POST --data-binary @- https://$server:$serverport/$route`
			fi
			[ "$(echo $result | grep 'Could not validate credentials')" ] && result=''
		fi
		_api_log_refusal "$route" "$result"
		# Quoted: unquoted, a * or ? in the answer was expanded to file names
		echo "$result" | _json_one_line
	} || {
		echo ''
	}
}

_set_glorytun_vps() {
	local enabled port key
	enabled="$(uci -q get glorytun.vpn.enable)"
	[ "$enabled" != "1" ] && enabled="$(uci -q get glorytun-udp.vpn.enable)"
	[ "$enabled" != "1" ] && echo "Glorytun disabled" && return
	port="$(uci -q get glorytun.vpn.port)"
	key="$(uci -q get glorytun.vpn.key)"
	chacha="$(uci -q get glorytun.vpn.chacha20)"
	if [ "$chacha" = "1" ]; then
		chacha="true"
	else
		chacha="false"
	fi
	[ -z "$key" ] && echo "Glorytun key not set" && return
	local current_port current_key current_chacha
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_port="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.port')"
	current_key="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.key')"
	current_chacha="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.chacha')"
	if [ "$current_port" != "$port" ] || [ "$current_key" != "$key" ] || [ "$current_chacha" != "$chacha" ]; then
		local settings
		settings='{"port": '$port',"key":"'$key'", "chacha": "'$chacha'"}'
		echo $(_set_json "glorytun" "$settings")
	else
		echo 1
	fi
}

# Only the server the config is pulled from (master, or current) ever clears
# get_config, in _get_vps_config: raised on a backup server it stayed 1, and
# omr-tracker's 003-up restarted this service on every tracker cycle.
# A master that omr-tracker-server keeps waiting while a backup is in use
# (omr-tracker.server.failback) is not: its config would move the proxy and
# the VPNs back to it.
_is_config_server() {
	[ "$(uci -q -P /var/state get openmptcprouter.$1.current)" = "1" ] && return 0
	[ "$(uci -q get openmptcprouter.$1.master)" = "1" ] && [ "$(uci -q -P /var/state get openmptcprouter.omr.failback_hold)" != "1" ]
}

_want_config() {
	_is_config_server "$servername" && uci -q set openmptcprouter.${servername}.get_config="1"
}

_set_openvpn_vps() {
	local enabled port key
	enabled="$(uci -q get openvpn.omr.enabled)"
	[ "$enabled" != "1" ] && echo "OpenVPN disabled" && return
	port="$(uci -q get openvpn.omr.port)"
	cipher="$(uci -q get openvpn.omr.cipher)"
	key="$(base64 /etc/luci-uploads/client.key | tr -d "\n")"
	local current_port current_cipher current_key
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_key="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_key')"
	current_port="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.port')"
	current_cipher="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.cipher')"
	if [ "$current_key" != "$key" ]; then
		_want_config
	fi
	if [ "$current_port" != "$port" ] || [ "$current_cipher" != "$cipher" ]; then
		local settings
		settings='{"port": '$port', "cipher": "'$cipher'"}'
		echo $(_set_json "openvpn" "$settings")
	else
		echo 1
	fi
}

_set_softethervpn_vps() {
	local enabled port key
	enabled="$(uci -q get softethervpn.openmptcprouter.enable)"
	[ "$enabled" != "1" ] && echo "SoftEtherVPN disabled" && return
	#port="$(uci -q get softethervpn.openmptcprouter.port)"
	cipher="$(uci -q get softethervpn.openmptcprouter.encryption)"
	password="$(uci -q get softethervpn.openmptcprouter.password)"
	local current_port current_cipher
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	#current_port="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.port')"
	current_cipher="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.cipher')"
	current_password="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.password')"
	if [ "$current_password" != "$password" ]; then
		_want_config
	fi
	#if [ "$current_port" != "$port" ] || [ "$current_cipher" != "$cipher" ]; then
	if [ "$current_cipher" != "$cipher" ]; then
		local settings
		#settings='{"port": '$port', "cipher": "'$cipher'"}'
		settings='{"cipher": "'$cipher'", "password": "'$password'"}'
		echo $(_set_json "softethervpn" "$settings")
	else
		echo 1
	fi
}

# Echo $1 without its leading zeros when it is a decimal integer the server
# takes, else $2 (the server's value) when that is one, else nothing
_mlvpn_vps_int() {
	local v
	for v in "$1" "$2"; do
		case "$v" in ''|*[!0-9]*) continue ;; esac
		while :; do
			case "$v" in
				0?*) v="${v#0}" ;;
				*) break ;;
			esac
		done
		[ "${#v}" -le 9 ] || continue
		echo "$v"
		return
	done
}

_set_mlvpn_vps() {
	local enabled port key timeout reorder_buffer_size loss_tolerence password cleartext_data
	enabled="$(uci -q get mlvpn.general.enable)"
	[ "$enabled" != "1" ] && echo "MLVPN disabled" && return
	local current_timeout current_reorder_buffer_size current_loss_tolerence current_password current_cleartext_data
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_timeout="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.timeout')"
	current_reorder_buffer_size="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.reorder_buffer_size')"
	current_loss_tolerence="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.loss_tolerence')"
	current_password="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.key')"
	current_cleartext_data="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.cleartext_data')"
	# The server requires every field, as an integer but the password, and
	# refuses the whole request otherwise: an unset cleartext_data (the
	# shipped config has none) was sent as "", an unset timeout as nothing
	# at all (invalid JSON), and nothing was ever applied. A value that is
	# not a number falls back to the server's own.
	timeout="$(_mlvpn_vps_int "$(uci -q get mlvpn.general.timeout)" "$current_timeout")"
	reorder_buffer_size="$(_mlvpn_vps_int "$(uci -q get mlvpn.general.reorder_buffer_size)" "$current_reorder_buffer_size")"
	# LuCI and the mlvpn init use loss_tolerance; the misspelled option is
	# the one older configs have, and the API field keeps that spelling
	loss_tolerence="$(uci -q get mlvpn.general.loss_tolerance)"
	[ -z "$loss_tolerence" ] && loss_tolerence="$(uci -q get mlvpn.general.loss_tolerence)"
	loss_tolerence="$(_mlvpn_vps_int "$loss_tolerence" "$current_loss_tolerence")"
	# unset is 0 for the mlvpn init
	cleartext_data="$(uci -q get mlvpn.general.cleartext_data)"
	[ -z "$cleartext_data" ] && cleartext_data="0"
	cleartext_data="$(_mlvpn_vps_int "$cleartext_data" "$current_cleartext_data")"
	# the characters the server takes in this quoted ini value; none of
	# them needs escaping in the JSON string
	password="$(uci -q get mlvpn.general.password)"
	if [ "${#password}" -gt 128 ] || [ "$(printf '%s' "$password" | wc -l)" -ne 0 ] || \
	   ! printf '%s' "$password" | LC_ALL=C grep -qxE "[]A-Za-z0-9!#\$&'()*+,./:;<=>?@[^_\`{|}~-]+"; then
		password="$current_password"
	fi
	if [ -z "$timeout" ] || [ -z "$reorder_buffer_size" ] || [ -z "$loss_tolerence" ] || [ -z "$cleartext_data" ] || [ -z "$password" ]; then
		echo "MLVPN settings incomplete"
		return
	fi
	if [ "$current_timeout" != "$timeout" ] || [ "$current_reorder_buffer_size" != "$reorder_buffer_size" ] || [ "$current_loss_tolerence" != "$loss_tolerence" ] || [ "$current_password" != "$password" ] || [ "$current_cleartext_data" != "$cleartext_data" ]; then
		local settings
		settings='{"timeout": '$timeout', "reorder_buffer_size": '$reorder_buffer_size', "loss_tolerence": '$loss_tolerence', "password": "'$password'", "cleartext_data": '$cleartext_data'}'
		echo $(_set_json "mlvpn" "$settings")
	else
		echo 1
	fi
}

# Echo the value the config pull should use for one mqvpn option: nothing when
# the router already has its own (the user configured it on the MQVPN page and
# _set_mqvpn_vps pushes it to the server), the server's value otherwise so a
# fresh setup is still seeded from it.
_mqvpn_local_wins() {
	[ -n "$(uci -q get "$1")" ] && return 0
	echo "$2"
}

# Echo the pin of the server's mqvpn certificate key from VPS config $1, or
# nothing when it sends none (omr-admin before 0.18+20261005) or a malformed
# one. Stored in mqvpn.server.pinned_pubkey, it makes mqvpn (0.16.3) accept
# only a server holding that key instead of any server, which Insecure did and
# which handed the auth key to whoever answered (GHSA-qq6x-5r9f-2w3m). It comes
# over the pinned API connection that carries the auth key itself.
_mqvpn_vps_pin() {
	local pin
	pin="$(echo "$1" | jsonfilter -q -e '@.mqvpn.pinned_pubkey')"
	pin="${pin#sha256//}"
	_omr_vps_pin_valid "$pin" && echo "$pin"
}

# The router's reorder rules as the members of the JSON array the server takes
# (at most 16, the server's limit), in uci order. A rule the server would
# refuse is left out: it refuses the whole request, scheduler included.
_mqvpn_reorder_rules_json() {
	local i=0 n=0 proto port profile out=""
	while [ "$n" -lt 16 ] && [ -n "$(uci -q get "mqvpn.@reorder_rule[$i]")" ]; do
		proto="$(uci -q get "mqvpn.@reorder_rule[$i].proto")"
		port="$(uci -q get "mqvpn.@reorder_rule[$i].port")"
		profile="$(uci -q get "mqvpn.@reorder_rule[$i].profile")"
		i=$((i + 1))
		case "$proto" in udp|tcp) ;; *) continue ;; esac
		case "$port" in ''|*[!0-9]*) continue ;; esac
		[ "${#port}" -le 5 ] && [ "$port" -le 65535 ] || continue
		case "$profile" in cellular_bond|fiber_lte|quic_bulk|low_latency|default_udp) ;; *) continue ;; esac
		out="${out:+$out, }{\"proto\": \"$proto\", \"port\": $port, \"profile\": \"$profile\"}"
		n=$((n + 1))
	done
	echo "$out"
}

# The reorder rules of VPS config $1, written as _mqvpn_reorder_rules_json does
_mqvpn_vps_reorder_rules() {
	local i=0 proto port profile out=""
	while [ "$i" -lt 16 ]; do
		proto="$(echo "$1" | jsonfilter -q -e "@.mqvpn.reorder_rules[$i].proto")"
		[ -z "$proto" ] && break
		port="$(echo "$1" | jsonfilter -q -e "@.mqvpn.reorder_rules[$i].port")"
		profile="$(echo "$1" | jsonfilter -q -e "@.mqvpn.reorder_rules[$i].profile")"
		out="${out:+$out, }{\"proto\": \"$proto\", \"port\": $port, \"profile\": \"$profile\"}"
		i=$((i + 1))
	done
	echo "$out"
}

_set_mqvpn_vps() {
	local enabled port key scheduler cc fec_enable fec_scheme reinjection_control reinjection_mode
	local current_port current_scheduler current_cc current_fec_enable current_fec_scheme
	local current_reinjection_control current_reinjection_mode
	enabled="$(uci -q get mqvpn.settings.enable)"
	[ "$enabled" != "1" ] && echo "MQVPN disabled" && return
	# mqvpn.server.port is the user intent: push it to the server when it differs
	port="$(uci -q get mqvpn.server.port)"
	[ -z "$port" ] && echo 1 && return
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_port="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.port')"
	[ -z "$current_port" ] && echo 1 && return
	key="$(uci -q get mqvpn.auth.key)"
	[ -z "$key" ] && echo "MQVPN key not set" && return

	current_scheduler="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.scheduler')"
	current_cc="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.cc')"
	current_fec_enable="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.fec_enable')"
	current_fec_scheme="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.fec_scheme')"
	current_reinjection_control="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reinjection_control')"
	current_reinjection_mode="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reinjection_mode')"

	scheduler="$(uci -q get mqvpn.multipath.scheduler)"
	[ -z "$scheduler" ] && scheduler="$current_scheduler"
	[ -z "$scheduler" ] && scheduler="minrtt"
	cc="$(uci -q get mqvpn.multipath.cc)"
	[ -z "$cc" ] && cc="$current_cc"
	# "None" is offered by the LuCI list but is not in the server's cc enum:
	# sending it fails the whole request with a validation error, which would
	# take the scheduler and FEC down with it.
	case "$cc" in
		bbr2|bbr|cubic|new_reno|copa|unlimited) ;;
		*) cc="bbr2" ;;
	esac
	fec_enable="$(uci -q get mqvpn.multipath.fec_enable)"
	case "$fec_enable" in
		1) fec_enable="true" ;;
		0) fec_enable="false" ;;
		*) fec_enable="$current_fec_enable"; [ -z "$fec_enable" ] && fec_enable="false" ;;
	esac
	fec_scheme="$(uci -q get mqvpn.multipath.fec_scheme)"
	[ -z "$fec_scheme" ] && fec_scheme="$current_fec_scheme"
	case "$fec_scheme" in
		galois_calculation|packet_mask|reed_solomon|xor) ;;
		*) fec_scheme="xor" ;;
	esac
	reinjection_control="$(uci -q get mqvpn.multipath.reinjection_control)"
	case "$reinjection_control" in
		1) reinjection_control="true" ;;
		0) reinjection_control="false" ;;
		*) reinjection_control="$current_reinjection_control"; [ -z "$reinjection_control" ] && reinjection_control="false" ;;
	esac
	reinjection_mode="$(uci -q get mqvpn.multipath.reinjection_mode)"
	[ -z "$reinjection_mode" ] && reinjection_mode="$current_reinjection_mode"
	case "$reinjection_mode" in
		default|deadline|dgram) ;;
		*) reinjection_mode="default" ;;
	esac

	# The reorder buffer only works when both ends enable it: the client
	# stamps its packets only once the server echoed the reorder header, and
	# the server's own setting decides that. Never sent before, it stayed off
	# on the server (omr-admin's default) whatever the MQVPN page said.
	local reorder_enabled reorder_max_wait_ms reorder_cap_packets reorder_rules
	local current_reorder_enabled current_reorder_max_wait_ms current_reorder_cap_packets current_reorder_rules
	current_reorder_enabled="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.enabled')"
	[ "$current_reorder_enabled" = "auto" ] && current_reorder_enabled="on"
	current_reorder_max_wait_ms="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.max_wait_ms')"
	current_reorder_cap_packets="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.cap_packets')"
	case "$(uci -q get mqvpn.reorder.enabled)" in
		1|on|true|yes) reorder_enabled="on" ;;
		0|off|false|no) reorder_enabled="off" ;;
		*) reorder_enabled="$current_reorder_enabled" ;;
	esac
	reorder_max_wait_ms="$(uci -q get mqvpn.reorder.max_wait_ms)"
	[ -z "$reorder_max_wait_ms" ] && reorder_max_wait_ms="$current_reorder_max_wait_ms"
	case "$reorder_max_wait_ms" in
		''|*[!0-9]*) reorder_max_wait_ms="30" ;;
	esac
	[ "${#reorder_max_wait_ms}" -gt 9 ] && reorder_max_wait_ms="30"
	reorder_cap_packets="$(uci -q get mqvpn.reorder.cap_packets)"
	[ -z "$reorder_cap_packets" ] && reorder_cap_packets="$current_reorder_cap_packets"
	# the server refuses the whole request when it is not a power of two
	case "$reorder_cap_packets" in
		''|*[!0-9]*) reorder_cap_packets="1024" ;;
	esac
	if [ "${#reorder_cap_packets}" -gt 10 ] || [ "$reorder_cap_packets" -lt 1 ] || \
	   [ "$reorder_cap_packets" -gt 2147483648 ] || [ $((reorder_cap_packets & (reorder_cap_packets - 1))) -ne 0 ]; then
		reorder_cap_packets="1024"
	fi
	# The rules go only when the router has some: without, the config pull
	# adopts the server's.
	reorder_rules="$(_mqvpn_reorder_rules_json)"
	current_reorder_rules="$(_mqvpn_vps_reorder_rules "$vps_config")"

	# mqvpn 0.18.0: PathMTU and PMTUD have to match on both ends, the TUN
	# batching follows the router's choice. Only a server that reports them
	# (omr-admin 0.18+20261026) takes them; without a local value the
	# server's stays, as the config pull then adopts it.
	local path_mtu pmtud tun_offload tun_gro
	local current_path_mtu current_pmtud current_tun_offload current_tun_gro
	current_path_mtu="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.path_mtu')"
	current_pmtud="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.pmtud')"
	current_tun_offload="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.tun_offload')"
	current_tun_gro="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.tun_gro')"
	# they go back into the request: anything else counts as not reported
	case "$current_path_mtu" in *[!0-9]*|?????*) current_path_mtu="" ;; esac
	case "$current_pmtud" in true|false) ;; *) current_pmtud="" ;; esac
	case "$current_tun_offload" in true|false) ;; *) current_tun_offload="" ;; esac
	case "$current_tun_gro" in true|false) ;; *) current_tun_gro="" ;; esac
	path_mtu="$(uci -q get mqvpn.interface.path_mtu)"
	# leading zeros dropped as the mqvpn init does, so that both ends get
	# the same value
	while :; do
		case "$path_mtu" in
			0?*) path_mtu="${path_mtu#0}" ;;
			*) break ;;
		esac
	done
	# the server refuses the whole request on any other value
	case "$path_mtu" in
		''|*[!0-9]*|?????*) path_mtu="$current_path_mtu" ;;
		0) ;;
		*) [ "$path_mtu" -ge 1280 ] && [ "$path_mtu" -le 9000 ] || path_mtu="$current_path_mtu" ;;
	esac
	_mqvpn_bool() {
		case "$(uci -q get "$1")" in
			1|on|true|yes) echo "true" ;;
			0|off|false|no) echo "false" ;;
			*) echo "$2" ;;
		esac
	}
	pmtud="$(_mqvpn_bool mqvpn.interface.pmtud "$current_pmtud")"
	tun_offload="$(_mqvpn_bool mqvpn.advanced.tun_offload "$current_tun_offload")"
	tun_gro="$(_mqvpn_bool mqvpn.advanced.tun_gro "$current_tun_gro")"

	local push=0
	[ "$current_port" != "$port" ] && push=1
	[ -n "$current_scheduler" ] && [ "$current_scheduler" != "$scheduler" ] && push=1
	[ -n "$current_cc" ] && [ "$current_cc" != "$cc" ] && push=1
	[ -n "$current_fec_enable" ] && [ "$current_fec_enable" != "$fec_enable" ] && push=1
	[ -n "$current_fec_scheme" ] && [ "$current_fec_scheme" != "$fec_scheme" ] && push=1
	[ -n "$current_reinjection_control" ] && [ "$current_reinjection_control" != "$reinjection_control" ] && push=1
	[ -n "$current_reinjection_mode" ] && [ "$current_reinjection_mode" != "$reinjection_mode" ] && push=1
	# a server without reorder support has no reorder in its config: comparing
	# with nothing would push on every sync
	if [ -n "$current_reorder_enabled" ] && [ -n "$reorder_enabled" ]; then
		[ "$current_reorder_enabled" != "$reorder_enabled" ] && push=1
		[ "$current_reorder_max_wait_ms" != "$reorder_max_wait_ms" ] && push=1
		[ "$current_reorder_cap_packets" != "$reorder_cap_packets" ] && push=1
		[ -n "$reorder_rules" ] && [ "$current_reorder_rules" != "$reorder_rules" ] && push=1
	fi
	[ -n "$current_path_mtu" ] && [ "$current_path_mtu" != "$path_mtu" ] && push=1
	[ -n "$current_pmtud" ] && [ "$current_pmtud" != "$pmtud" ] && push=1
	[ -n "$current_tun_offload" ] && [ "$current_tun_offload" != "$tun_offload" ] && push=1
	[ -n "$current_tun_gro" ] && [ "$current_tun_gro" != "$tun_gro" ] && push=1
	if [ "$push" = "1" ]; then
		local settings result
		settings='{"key": "'$key'", "port": '$port', "scheduler": "'$scheduler'", "cc": "'$cc'", "fec_enable": '$fec_enable', "fec_scheme": "'$fec_scheme'", "reinjection_control": '$reinjection_control', "reinjection_mode": "'$reinjection_mode'"'
		[ -n "$reorder_enabled" ] && \
			settings="$settings"', "reorder": {"enabled": "'$reorder_enabled'", "max_wait_ms": '$reorder_max_wait_ms', "cap_packets": '$reorder_cap_packets'}'
		[ -n "$reorder_rules" ] && settings="$settings"', "reorder_rules": ['$reorder_rules']'
		[ -n "$current_path_mtu" ] && settings="$settings"', "path_mtu": '$path_mtu
		[ -n "$current_pmtud" ] && settings="$settings"', "pmtud": '$pmtud
		[ -n "$current_tun_offload" ] && settings="$settings"', "tun_offload": '$tun_offload
		[ -n "$current_tun_gro" ] && settings="$settings"', "tun_gro": '$tun_gro
		settings="$settings}"
		# Only what the server applied is pulled back and restarted for: a
		# refusal (a read only user, a server-wide setting the router may
		# not change, MQVPN not installed) is an answer too, and every sync
		# asked for the whole config again and omr-schedule restarted the
		# services for it. The same refusal is sent again hourly only.
		local cache="/tmp/.mqvpn-vps-push-${servername}" desired
		desired="$(printf '%s' "${server}|$settings" | md5sum | cut -d' ' -f1)"
		_vps_push_wait "$cache" "$desired" && { echo 1; return; }
		result=$(_set_json "mqvpn" "$settings")
		# no answer at all: tried again on the next sync, as before
		[ -n "$result" ] && _vps_push_done "$cache" "$desired" "$result"
		if [ "$(_parse_result "$result")" = "done" ]; then
			_want_config
			[ "$current_port" != "$port" ] && /etc/init.d/mqvpn restart >/dev/null 2>&1
		fi
		echo $result
	else
		echo 1
	fi
}

_set_wireguard_vps() {
	local enabled port key
	ipskey=""
	# Only the tunnels mptcpovervpn makes to the VPS (wg<wan>, a
	# 10.255.247.x address, its public key kept next to the private one):
	# one the user made in LuCI has no public_key option, and its empty
	# key made the VPS refuse the whole list, those peers with it
	_get_wg_ipskey() {
		local interface=$1 addr endpoint
		proto=$(uci -q get network.${interface}.proto)
		if [ "$proto" = "wireguard" ]; then
			ip=""
			for addr in $(uci -q get network.${interface}.addresses); do
				case "${addr%%/*}" in
					10.255.247.|10.255.247.*[!0-9]*) ;;
					10.255.247.*) ip="${addr%%/*}"; break ;;
				esac
			done
			key="$(uci -q get network.${interface}.public_key)"
			# a WireGuard key is 32 bytes in base64: 44 characters
			case "$key" in
				*[!A-Za-z0-9+/=]*) key="" ;;
			esac
			[ -n "$ip" ] && [ "${#key}" = 44 ] || return 0
			# A tunnel to another server isn't a peer of this one: every
			# server's VPN is 10.255.247.0/24, so its address alone doesn't
			# tell, and this server refused the list when another user of
			# it had that address
			endpoint="$(uci -q get network.@wireguard_${interface}[0].endpoint_host)"
			if [ -n "$endpoint" ]; then
				case " $(uci -q get openmptcprouter.${servername}.ip) " in
					*" $endpoint "*) ;;
					*) return 0 ;;
				esac
			fi
			if [ -z "$ipskey" ]; then
				ipskey='{"ip": "'$ip'", "key": "'$key'"}'
			else
				ipskey=$ipskey',{"ip": "'$ip'", "key": "'$key'"}'
			fi
		fi
	}
	config_load network
	config_foreach _get_wg_ipskey interface
	local settings
	if [ -n "$ipskey" ]; then
		settings='{"peers": ['$ipskey']}'
		echo $(_set_json "wireguard" "$settings")
	else
		echo 1
	fi
}

# set_wireguard <server>: the router's WireGuard peers, to that server only.
# A server only gets the tunnels pointed at it (_get_wg_ipskey), so a backup
# that mptcpovervpn moved them to on failover refused them until its next
# full sync, up to an hour later: mptcpovervpn calls this then. With the
# stored token (a login only when it is refused), and not within 10 minutes
# of a failed login to that server.
set_wireguard() {
	local now first last count seen
	servername=$1
	case "$servername" in
		''|*[!A-Za-z0-9_]*) return 1 ;;
	esac
	[ "$(uci -q get openmptcprouter.${servername})" = "server" ] || return 1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return 0
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return 0
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return 0
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return 0
	if [ -f "/tmp/.omr-vps-tokenfail-${servername}" ]; then
		read -r first last count seen < "/tmp/.omr-vps-tokenfail-${servername}"
		now="$(date +%s)"
		case "$seen" in
			''|*[!0-9]*) ;;
			*) [ "$((now - seen))" -lt 600 ] && return 1 ;;
		esac
	fi
	# One at a time: mptcpovervpn can be restarted twice in a row (the
	# tracker, then a network change), and a run that waited sends the
	# peers as they are by then
	if command -v flock >/dev/null 2>&1 && [ -d /var/lock ] && [ -w /var/lock ]; then
		exec 9>/var/lock/omr-set-wireguard.lock
		flock 9
	fi
	_get_config_stored_token
	if [ -z "$token" ]; then
		_token_failed "$servername" "Can't get ${servername} token, WireGuard peers not sent"
		return 1
	fi
	[ -n "$vps_config" ] || return 1
	[ "$(echo "$vps_config" | jsonfilter -q -e '@.user.permission')" = "ro" ] && return 0
	[ -n "$(_set_wireguard_vps)" ] || {
		logger -t "OMR-VPS" "WireGuard peers not set on server ${servername}, sent again at its next sync"
		return 1
	}
	return 0
}

# The server's WireGuard public key, for mptcpovervpn's peer. It was only
# kept from the config server's config: a backup's stayed unknown, and the
# tunnels re-pointed at it on failover had an empty peer key. Returns 0 when
# the key changed.
_set_wgkey_from_vps() {
	local wgkey
	wgkey="$(echo "$vps_config" | jsonfilter -q -e '@.wireguard.key')"
	# a WireGuard key is 32 bytes in base64: 44 characters
	case "$wgkey" in
		*[!A-Za-z0-9+/=]*) return 1 ;;
	esac
	[ "${#wgkey}" = 44 ] || return 1
	[ "$(uci -q get openmptcprouter.${servername}.wgkey)" != "$wgkey" ] || return 1
	uci -q set openmptcprouter.${servername}.wgkey="$wgkey"
}

# get_openvpn_key <server>: rc.common passes the server as $1 (it read $2, always
# empty, so no key was ever fetched and _login logged an empty server name)
get_openvpn_key() {
	servername=$1
	[ -z "$servername" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	_get_config_stored_token
	[ -z "$vps_config" ] && return
	# omr-tracker-server calls this when it makes the server current, just
	# before it restarts mptcpovervpn
	_set_wgkey_from_vps && uci -q commit openmptcprouter
	openvpn_client_key="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_key')"
	[ -n "$openvpn_client_key" ] && {
		# 0600, also for a key written 0644 by an older version
		( umask 077; echo $openvpn_client_key | base64 -d > /etc/luci-uploads/client.key )
		chmod 0600 /etc/luci-uploads/client.key
	}
	openvpn_client_crt="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_crt')"
	[ -n "$openvpn_client_crt" ] && {
		echo $openvpn_client_crt | base64 -d > /etc/luci-uploads/client.crt
	}
	openvpn_client_ca="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_ca')"
	[ -n "$openvpn_client_ca" ] && {
		echo $openvpn_client_ca | base64 -d > /etc/luci-uploads/ca.crt
	}
	[ -n "$openvpn_client_key" ] && [ -n "$openvpn_client_crt" ] && [ -n "$openvpn_client_ca" ] && {
		[ "$(uci -q get openvpn.omr.enabled)" = "1" ] && /etc/init.d/openvpn restart
	}
}

_get_ss_redir() {
	config_get cf_ebpf $1 ebpf
	[ "$cf_ebpf" = "1" ] && ebpf="true"
	config_get cf_fast_open $1 fast_open
	[ "$cf_fast_open" = "1" ] && fast_open="true"
	config_get cf_no_delay $1 no_delay
	[ "$cf_no_delay" = "1" ] && no_delay="true"
	config_get cf_mptcp $1 mptcp
	[ "$cf_mptcp" = "1" ] && mptcp="true"
}

_get_ss_server() {
	config_get cf_obfs $1 obfs
	[ "$cf_obfs" = "1" ] && obfs="true"
	config_get obfs_plugin $1 obfs_plugin "v2ray"
	config_get obfs_type $1 obfs_type "http"
}

_set_ss_server_vps() {
	local disabled port key method
	config_load shadowsocks-libev
	config_get disabled sss0 disabled
	[ "$disabled" = "1" ] && return
	config_get port sss0 server_port
	#config_get server $1 server
	config_get key sss0 key
	key="$(echo $key | sed 's/+/-/g; s/\//_/g;')"
	[ -z "$key" ] && return
	config_get method sss0 method
	local current_port current_key current_method
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_port="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.port')"
	current_key="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.key')"
	current_method="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.method')"
	current_ebpf="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.ebpf')"
	current_obfs="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs')"
	current_obfs_plugin="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs_plugin')"
	current_obfs_type="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs_type')"
	current_fast_open="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.fast_open')"
	current_no_delay="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.no_delay')"
	current_mptcp="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.mptcp')"
	
	ebpf="false"
	fast_open="false"
	no_delay="false"
	mptcp="false"
	obfs="false"
	obfs_plugin="v2ray"
	obfs_type="http"
	config_load shadowsocks-libev
	config_foreach _get_ss_redir ss_redir
	config_foreach _get_ss_server server

	if [ "$current_mptcp" != "$mptcp" ] || [ "$current_obfs_plugin" != "$obfs_plugin" ] || [ "$current_obfs_type" != "$obfs_type" ] || [ "$current_port" != "$port" ] || [ "$current_method" != "$method" ] || [ "$current_key" != "$key" ] || [ "$current_ebpf" != "$ebpf" ] || [ "$current_obfs" != "$obfs" ] || [ "$current_fast_open" != "$fast_open" ] || [ "$current_no_delay" != "$no_delay" ]; then
		local settings
		settings='{"port": '$port',"method":"'$method'","fast_open":'$fast_open',"reuse_port":true,"no_delay":'$no_delay',"mptcp":'$mptcp',"key":"'$key'","ebpf":'$ebpf',"obfs":'$obfs',"obfs_plugin":"'$obfs_plugin'","obfs_type":"'$obfs_type'"}'
		result=$(_set_json "shadowsocks" "$settings")
	fi
}

_set_ssgo_server_vps() {
	local disabled port key method
	config_load shadowsocks-rust
	config_get disabled sss0 disabled
	[ "$disabled" = "1" ] && return
	config_get port sss0 server_port
	#config_get server $1 server
	config_get key sss0 key
	key="$(echo $key | sed 's/+/-/g; s/\//_/g;')"
	[ -z "$key" ] && return
	config_get method sss0 method
	local current_port current_key current_method
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_port="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.port')"
	current_key="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.password')"
	current_method="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.protocol')"
#	current_fast_open="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.fast_open')"
#	current_mptcp="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.mptcp')"
	
	ebpf="false"
	fast_open="false"
	no_delay="false"
	mptcp="false"
	config_load shadowsocks-rust
	config_foreach _get_ss_redir ss_redir
	config_foreach _get_ss_server server
	# Force disable fast open for now du to problem on 6.1 with MPTCP
	fast_open="false"

	#if [ "$current_mptcp" != "$mptcp" ] || [ "$current_port" != "$port" ] || [ "$current_method" != "$method" ] || [ "$current_fast_open" != "$fast_open" ]; then
	if [ "$current_port" != "$port" ] || [ "$current_method" != "$method" ]; then
		local settings
		settings='{"port": '$port',"method":"'$method'","fast_open":'$fast_open',"reuse_port":false,"mptcp":'$mptcp'}'
		result=$(_set_json "shadowsocks-go" "$settings")
	fi
}

_set_v2ray_server_vps() {
	enabled=$(uci -q get v2ray.main.enabled)
	[ "$enabled" != "1" ] && return
	userid=$(uci -q get v2ray.omrout.s_vless_user_id)
	[ -z "$userid" ] && return
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_userid="$(echo "$vps_config" | jsonfilter -q -e '@.v2ray.config.key')"

	if [ "$current_userid" != "$userid" ]; then
		local settings
		settings='{"userid": "'$userid'"}'
		echo $(_set_json "v2ray" "$settings")
	fi
}

_set_xray_server_vps() {
	enabled=$(uci -q get xray.main.enabled)
	userid=$(uci -q get xray.omrout.s_vless_user_id)
	protocol=$(uci -q get xray.omrout.protocol)
	ss_method=$(uci -q get xray.omrout.s_shadowsocks_method)
	# An empty method makes the xray-server.json shadowsocks inbound invalid
	# and prevents the whole xray service from starting on the VPS
	[ -z "$ss_method" ] && ss_method="2022-blake3-aes-256-gcm"
	transport=$(uci -q get xray.omrout.ss_network)
	# The VPS takes tcp, grpc or xhttp: any other (kcp, ws, or the http and
	# quic xray removed) got a 422, and the same POST at every sync
	case "$transport" in
		grpc|xhttp) ;;
		*) transport="tcp" ;;
	esac
	if [ "$protocol" = "vless-reality" ] && [ "$enabled" = "1" ]; then
		vless_reality='true'
	else
		vless_reality='false'
	fi
	[ -z "$userid" ] && return
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	current_userid="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.key')"
	current_vlessreality="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.vless_reality')"
	current_method="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.ss_method')"
	current_transport="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.transport')"

	if [ "$current_userid" != "$userid" ] || [ "$current_vlessreality" != "$vless_reality" ] || [ "$current_method" != "$ss_method" ] || [ "$current_transport" != "$transport" ]; then
		local settings
		settings='{"userid": "'$userid'","vless_reality": '$vless_reality',"ss_method": "'$ss_method'","transport": "'$transport'"}'
		echo $(_set_json "xray" "$settings")
	fi
}

# The uuid of this router's XRay reverse client (VPS->LAN port forwards):
# the VPS gives each user one of its own, and replaces the one shared by
# every user of a multi-user VPS, so a changed reverse_key must be taken at
# any sync, not only when _set_config_from_vps runs.
_get_xray_reverse_key() {
	local reverse_key
	[ -n "$(uci -q get xray.omrout)" ] || return
	reverse_key="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.reverse_key')"
	[ -n "$reverse_key" ] || return
	[ "$reverse_key" = "$(uci -q get xray.omrout.s_vless_reverse_user_id)" ] && return
	logger -t "OMR-VPS" "New XRay reverse key from the server"
	uci -q set xray.omrout.s_vless_reverse_user_id="$reverse_key"
	uci -q commit xray
	[ "$(uci -q get xray.main.enabled)" = "1" ] && /etc/init.d/xray restart >/dev/null 2>&1
}

# Sets vpsip, the address the proxies and the VPNs are given: the first one
# of the server, resolved once per sync. _set_config_from_vps gave them a host
# name as it is, then _get_vps_config the address it resolves to: each client
# changed, and restarted, twice per config pull.
_get_vpsip() {
	local ipresolve ip6resolve
	[ -n "$vpsip" ] && [ "$vpsip_server" = "$servername" ] && return
	vpsip="$(uci -q get openmptcprouter.${servername}.ip | awk '{print $1}')"
	ipresolve="$(resolveip -t 5 -4 $vpsip | head -n 1)"
	if [ -n "$ipresolve" ]; then
		vpsip="$ipresolve"
	else
		ip6resolve="$(resolveip -t 5 -6 $vpsip | head -n 1)"
		[ -n "$ip6resolve" ] && vpsip="$ip6resolve"
	fi
	vpsip_server="$servername"
}

_get_vps_config() {
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	vps_lastchange="$(echo "$vps_config" | jsonfilter -q -e '@.vps.lastchange')"
	# _config_service has just applied this answer: the local lastchange is
	# only updated at its end, so it was applied a second time in the same pull
	if [ "$vps_config_applied" != "$servername" ] && [ -n "$(uci -q get openmptcprouter.${servername}.lastchange)" ] && [ -n "$vps_lastchange" ] && [ "$vps_lastchange" -gt "$(uci -q get openmptcprouter.${servername}.lastchange)" ]; then
		_set_config_from_vps
	fi

	piholeomr="$(uci -q get openmptcprouter.${servername}.pihole)"
	pihole="$(echo "$vps_config" | jsonfilter -q -e '@.pihole.state')"
	if [ "$pihole" = "true" ] && [ "$piholeomr" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.${servername}.pihole='1'
			commit openmptcprouter
		EOF
	elif [ "$pihole" = "false" ] && [ "$piholeomr" != "0" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.${servername}.pihole='0'
			commit openmptcprouter
		EOF
	fi

	vpsinternet="$(echo "$vps_config" | jsonfilter -q -e '@.network.internet')"
	if [ "$vpsinternet" = "false" ] && [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.settings.external_check='0'
			commit openmptcprouter
		EOF
	fi

	vps_kernel="$(echo "$vps_config" | jsonfilter -q -e '@.vps.kernel')"
	vps_machine="$(echo "$vps_config" | jsonfilter -q -e '@.vps.machine')"
	vps_omr_version="$(echo "$vps_config" | jsonfilter -q -e '@.vps.omr_version')"
	uci -q batch <<-EOF >/dev/null
		set openmptcprouter.${servername}.kernel=$vps_kernel
		set openmptcprouter.${servername}.machine=$vps_machine
		set openmptcprouter.${servername}.omr_version=$vps_omr_version
	EOF
	vpn="$(uci -q get openmptcprouter.settings.vpn)"

	glorytun_state=0
	glorytun_change=0
	if [ "$vpn" = "glorytun_tcp" ]; then
		glorytun_state=1
		uci -q set glorytun.vpn.proto='tcp'
		client_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.tcp.client_ip')"
		host_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.tcp.host_ip')"
		port="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.port')"
		if [ "$(uci -q get glorytun.vpn.port)" != "$port" ] && [ "$port" != "" ]; then
			uci -q batch <<-EOF >/dev/null
				set glorytun.vpn.port=$port
			EOF
			glorytun_change=1
		fi
		if [ "$client_ip" != "dhcp" ] && [ -n "$client_ip" ]; then
			if [ "$host_ip" != "$(uci -q get glorytun.vpn.remoteip)" ] || [ "$client_ip" != "$(uci -q get glorytun.vpn.localip)" ]; then
				uci -q batch <<-EOF >/dev/null
					set glorytun.vpn.localip=$client_ip
					set glorytun.vpn.remoteip=$host_ip
				EOF
				glorytun_change=1
			fi
			if [ "$(uci -q get network.omrvpn.proto)" != 'none' ]; then
				uci -q batch <<-EOF >/dev/null
					set network.omrvpn.proto='none'
					commit network
				EOF
				ifup omrvpn >/dev/null 2>&1
				glorytun_change=1
			fi
		else
			if [ "$(uci -q get glorytun.vpn.remoteip)" != "" ] || [ "$(uci -q get glorytun.vpn.localip)" != "" ]; then
				uci -q batch <<-EOF >/dev/null
					delete glorytun.vpn.localip
					delete glorytun.vpn.remoteip
				EOF
				glorytun_change=1
			fi
			if [ "$(uci -q get network.omrvpn.proto)" != 'dhcp' ]; then
				uci -q batch <<-EOF >/dev/null
					set network.omrvpn.proto='dhcp'
					commit network
				EOF
				ifup omrvpn >/dev/null 2>&1
				glorytun_change=1
			fi
		fi
	fi
	if [ "$vpn" = "glorytun_udp" ]; then
		glorytun_state=1
		#uci -q set glorytun.vpn.proto='udp'
		client_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.udp.client_ip')"
		host_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.udp.host_ip')"
		port="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.port')"
		if [ "$(uci -q get glorytun-udp.vpn.port)" != "$port" ] && [ "$port" != "" ]; then
			uci -q batch <<-EOF >/dev/null
				set glorytun-udp.vpn.port=$port
			EOF
		fi
		if [ "$client_ip" != "dhcp" ] && [ -n "$client_ip" ]; then
			if [ "$host_ip" != "$(uci -q get glorytun-udp.vpn.remoteip)" ] || [ "$client_ip" != "$(uci -q get glorytun-udp.vpn.localip)" ]; then
				uci -q batch <<-EOF >/dev/null
					set glorytun-udp.vpn.localip=$client_ip
					set glorytun-udp.vpn.remoteip=$host_ip
				EOF
				glorytun_change=1
			fi
			if [ "$(uci -q get network.omrvpn.proto)" = "dhcp" ]; then
				uci -q batch <<-EOF >/dev/null
					set network.omrvpn.proto='none'
					commit network
				EOF
				ifup omrvpn >/dev/null 2>&1
				glorytun_change=1
			fi
		else
			if [ "$(uci -q get glorytun-udp.vpn.remoteip)" != "" ] || [ "$(uci -q get glorytun-udp.vpn.localip)" != "" ]; then
				uci -q batch <<-EOF >/dev/null
					delete glorytun-udp.vpn.localip
					delete glorytun-udp.vpn.remoteip
				EOF
				glorytun_change=1
			fi
			if [ "$(uci -q get network.omrvpn.proto)" != "dhcp" ]; then
				uci -q batch <<-EOF >/dev/null
					set network.omrvpn.proto='dhcp'
					commit network
				EOF
				ifup omrvpn >/dev/null 2>&1
				glorytun_change=1
			fi
		fi
	fi
	if [ "$vpn" = "mqvpn" ] || [ "$vpn" = "openvpn" ]; then
		# mqvpn and openvpn assign the tunnel IP themselves: a dhcp proto
		# (softether or glorytun-dhcp era, or set for openvpn by the wizard)
		# keeps omrvpn pending forever in netifd, which breaks everything
		# that depends on it (VXLAN tunlink, client2client routes, ...)
		if [ "$(uci -q get network.omrvpn.proto)" = "dhcp" ]; then
			uci -q batch <<-EOF >/dev/null
				set network.omrvpn.proto='none'
				commit network
			EOF
			ifup omrvpn >/dev/null 2>&1
		fi
	fi
	_get_vpsip
	if [ -n "$(uci -q get shadowsocks-libev.sss0)" ] && [ "$(uci -q get shadowsocks-libev.sss0.server)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		config_load shadowsocks-libev
		config_foreach _set_ss_server server "server" $vpsip
		uci -q batch <<-EOF >/dev/null
			set shadowsocks-libev.sss0.server="$vpsip"
			commit shadowsocks-libev
		EOF
		if [ "$(uci -q get shadowsocks-libev.sss0.disabled)" = "0" ]; then
			logger -t "OMR-VPS" "Restart shadowsocks..."
			/etc/init.d/shadowsocks-libev restart
		fi
	fi
	if [ -n "$(uci -q get shadowsocks-rust.sss0)" ] && [ "$(uci -q get shadowsocks-rust.sss0.server)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		config_load shadowsocks-rust
		config_foreach _set_ssrust_server server "server" $vpsip
		uci -q batch <<-EOF >/dev/null
			set shadowsocks-rust.sss0.server="$vpsip"
			commit shadowsocks-rust
		EOF
		if [ "$(uci -q get shadowsocks-rust.sss0.disabled)" = "0" ]; then
			logger -t "OMR-VPS" "Restart shadowsocks Rust..."
			/etc/init.d/shadowsocks-rust restart
		fi
	fi
	if [ -n "$(uci -q get v2ray.omrout)" ] && [ "$(uci -q get v2ray.omrout.s_vmess_address)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set v2ray.omrout.s_vmess_address="$vpsip"
			set v2ray.omrout.s_vless_address="$vpsip"
			set v2ray.omrout.s_trojan_address="$vpsip"
			set v2ray.omrout.s_socks_address="$vpsip"
			commit v2ray
		EOF
		if [ "$(uci -q get v2ray.main.enabled)" = "1" ]; then
			logger -t "OMR-VPS" "Restart V2Ray..."
			/etc/init.d/v2ray restart
		fi
	fi
	if [ -n "$(uci -q get xray.omrout)" ] && [ "$(uci -q get xray.omrout.s_vmess_address)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set xray.omrout.s_vmess_address="$vpsip"
			set xray.omrout.s_vless_address="$vpsip"
			set xray.omrout.s_trojan_address="$vpsip"
			set xray.omrout.s_socks_address="$vpsip"
			set xray.omrout.s_shadowsocks_address="$vpsip"
			set xray.omrout.s_vless_reality_address="$vpsip"
			commit xray
		EOF
		if [ "$(uci -q get xray.main.enabled)" = "1" ]; then
			logger -t "OMR-VPS" "Restart XRay..."
			/etc/init.d/xray restart
		fi
	fi
	if [ -n "$(uci -q get openvpn.omr)" ] && [ -z "$(uci -q get openvpn.omr.remote | grep $vpsip)" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			delete openvpn.omr.remote
			add_list openvpn.omr.remote="$vpsip"
			commit openvpn
		EOF
		if [ "$(uci -q get openvpn.omr.enabled)" = "1" ]; then
			logger -t "OMR-VPS" "Restart OpenVPN..."
			/etc/init.d/openvpn restart
		fi
	fi
	port="$(echo "$vps_config" | jsonfilter -q -e '@.dsvpn.port')"
	localip="$(echo "$vps_config" | jsonfilter -q -e '@.dsvpn.client_ip')"
	remoteip="$(echo "$vps_config" | jsonfilter -q -e '@.dsvpn.host_ip')"
	# An answer without DSVPN (an older VPS, or a user DSVPN was never set
	# up for) emptied the port or the addresses and restarted DSVPN broken
	if [ -n "$(uci -q get dsvpn.vpn)" ] && [ -n "$port" ] && [ -n "$remoteip" ] && { { [ "$(uci -q get dsvpn.vpn.host)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; } || [ "$(uci -q get dsvpn.vpn.port)" != "$port" ] || [ "$(uci -q get dsvpn.vpn.localip)" != "$localip" ] || [ "$(uci -q get dsvpn.vpn.remoteip)" != "$remoteip" ]; }; then
		uci -q batch <<-EOF >/dev/null
			set dsvpn.vpn.port=$port
			set dsvpn.vpn.localip=$localip
			set dsvpn.vpn.remoteip=$remoteip
			set dsvpn.vpn.host="$vpsip"
			commit dsvpn
		EOF
		if [ "$(uci -q get dsvpn.vpn.enable)" = "1" ]; then
			logger -t "OMR-VPS" "Restart DSVPN..."
			/etc/init.d/dsvpn restart
		fi
	fi

	mqvpn_port_tmp="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.port')"
	[ -z "$mqvpn_port_tmp" ] && mqvpn_port_tmp="65443"
	# mqvpn.server.port is user intent (pushed to the VPS by _set_mqvpn_vps):
	# only default it from the VPS when not set locally
	[ -n "$(uci -q get mqvpn.server.port)" ] && mqvpn_port_tmp="$(uci -q get mqvpn.server.port)"
	if [ -n "$(uci -q get mqvpn.server)" ] && { [ "$(uci -q get mqvpn.server.ip)" != "$vpsip" ] || [ "$(uci -q get mqvpn.server.port)" != "$mqvpn_port_tmp" ]; } && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set mqvpn.server.ip="$vpsip"
			set mqvpn.server.port="$mqvpn_port_tmp"
			commit mqvpn
		EOF
		if [ "$(uci -q get mqvpn.settings.enable)" = "1" ]; then
			logger -t "OMR-VPS" "Restart MQVPN..."
			/etc/init.d/mqvpn restart
		fi
	fi

	port="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.port')"
	if [ -n "$(uci -q get softethervpn.openmptcprouter)" ] && [ -n "$port" ] && { { [ "$(uci -q get softethervpn.openmptcprouter.host)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; } || [ "$(uci -q get softethervpn.openmptcprouter.port)" != "$port" ]; }; then
		uci -q batch <<-EOF >/dev/null
			set softethervpn.openmptcprouter.port=$port
			set softethervpn.openmptcprouter.host="$vpsip"
			commit softethervpn
		EOF
		# omrvpn is shared by all VPNs: only switch it to DHCP when
		# SoftEtherVPN is really the VPN in use
		if [ "$vpn" = "softethervpn" ] || [ "$vpn" = "softether" ]; then
			uci -q batch <<-EOF >/dev/null
				set network.omrvpn.proto='dhcp'
				commit network
			EOF
			ifup omrvpn >/dev/null 2>&1
		fi
		if [ "$(uci -q get softethervpn.openmptcprouter.enable)" = "1" ] && [ -f /etc/init.d/softethervpnclient ]; then
			logger -t "OMR-VPS" "Restart SoftEtherVPN..."
			/etc/init.d/softethervpnclient restart
		fi
	fi

	if [ -n "$(uci -q get mlvpn.general)" ] && [ -z "$(uci -q get openmptcprouter.settings.mptcpovervpn)" ] && [ "$(uci -q get mlvpn.general.host)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ] && [ -f /etc/init.d/mlvpn ]; then
		uci -q batch <<-EOF >/dev/null
			set mlvpn.general=mlvpn
			set mlvpn.general.host="$vpsip"
			commit mlvpn
		EOF
		if [ "$(uci -q get mlvpn.general.enable)" = "1" ]; then
			logger -t "OMR-VPS" "Restart MLVPN..."
			/etc/init.d/mlvpn restart
		fi
	fi
	if [ -n "$(uci -q get glorytun.vpn)" ] && [ -z "$(uci -q get openmptcprouter.settings.mptcpovervpn)" ] && [ "$(uci -q get glorytun.vpn.host)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set glorytun.vpn.host="$vpsip"
		EOF
		glorytun_change=1
	fi
	if [ -n "$(uci -q get glorytun-udp.vpn)" ] && [ -z "$(uci -q get openmptcprouter.settings.mptcpovervpn)" ] && [ "$(uci -q get glorytun-udp.vpn.host)" != "$vpsip" ] && [ "$(uci -q get openmptcprouter.settings.ha)" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set glorytun-udp.vpn.host="$vpsip"
		EOF
		glorytun_change=1
	fi

	if [ "$glorytun_change" != "0" ]; then
		uci -q batch <<-EOF >/dev/null
			commit glorytun
			commit glorytun-udp
		EOF
		if [ "$(uci -q get glorytun.vpn.enable)" = "1" ]; then
			logger -t "OMR-VPS" "Restart glorytun..."
			/etc/init.d/glorytun restart >/dev/null 2>&1
		fi
		if [ "$(uci -q get glorytun-udp.vpn.enable)" = "1" ]; then
			logger -t "OMR-VPS" "Restart glorytun-udp..."
			/etc/init.d/glorytun-udp restart >/dev/null 2>&1
		fi
	fi
}

# Every config has the server's kernel, an error answer has none
_vps_config_valid() {
	[ -n "$vps_config" ] && [ -n "$(echo "$vps_config" | jsonfilter -q -e '@.vps.kernel')" ]
}

# "uci get <package>" prints nothing even when the package is installed:
# tested that way, the shadowsocks and xray parts of the GRE tunnels never ran
_gre_uci_pkg() {
	[ -n "$(uci -q show "$1" 2>/dev/null | head -n 1)" ]
}

# _gre_ss_sections <package> <i> <public address> <server port> <method> <secret option> <secret> <redir template>
# The server, ss-redir and rules sections of a public IP. The two switches
# (server and rules "disabled") are only set when the sections are created,
# a user's choice is kept on every later sync.
_gre_ss_sections() {
	local pkg=$1 i=$2 publicaddr=$3 port=$4 method=$5 secretopt=$6 secret=$7 redir=$8
	local new_server new_rule
	[ -n "$(uci -q get "${pkg}.oip${i}server")" ] || new_server=1
	[ -n "$(uci -q get "${pkg}.oip${i}_rule")" ] || new_rule=1
	uci -q batch <<-EOF >/dev/null
		set ${pkg}.oip${i}server=server
		set ${pkg}.oip${i}server.label="Server with public IP $publicaddr"
		set ${pkg}.oip${i}server.server="$(uci -q get "${pkg}.sss0.server")"
		set ${pkg}.oip${i}server.server_port="$port"
		set ${pkg}.oip${i}server.method="$method"
		set ${pkg}.oip${i}server.${secretopt}="$secret"
		set ${pkg}.oip${i}=ss_redir
		set ${pkg}.oip${i}.label="ss-redir for public IP $publicaddr"
		set ${pkg}.oip${i}.server="oip${i}server"
		set ${pkg}.oip${i}.local_port="230$i"
		set ${pkg}.oip${i}.local_address="$(uci -q get "${pkg}.${redir}.local_address")"
		set ${pkg}.oip${i}.mode='tcp_and_udp'
		set ${pkg}.oip${i}.reuse_port='1'
		set ${pkg}.oip${i}.mptcp='1'
		set ${pkg}.oip${i}.ipv6_first='1'
		set ${pkg}.oip${i}.timeout="$(uci -q get "${pkg}.${redir}.timeout")"
		set ${pkg}.oip${i}.fast_open="$(uci -q get "${pkg}.${redir}.fast_open")"
		set ${pkg}.oip${i}.no_delay="$(uci -q get "${pkg}.${redir}.no_delay")"
		set ${pkg}.oip${i}_rule=ss_rules
		set ${pkg}.oip${i}_rule.label="Rules for public IP $publicaddr"
		set ${pkg}.oip${i}_rule.server="oip${i}server"
		set ${pkg}.oip${i}_rule.redir_tcp="oip${i}"
	EOF
	[ -n "$new_server" ] && uci -q set "${pkg}.oip${i}server.disabled=1"
	[ -n "$new_rule" ] && uci -q set "${pkg}.oip${i}_rule.disabled=1"
}

# _gre_proxy_sections <xray|v2ray> <i> <tunnel json>
# The outbound to the VPS user of a public IP, and the inbound that the
# public IP's rules redirect to. The outbound is the main one (omrout:
# protocol, transport, TLS pin, Reality key...) with the user of the public IP.
# The v2ray inbound has a port of its own (1820+i): both can be installed.
# _gre_proxy_drop <xray|v2ray> <i>: the sections of public IP <i>
_gre_proxy_drop() {
	local pkg=$1 i=$2
	uci -q batch <<-EOF >/dev/null
		delete ${pkg}.oip${i}server
		delete ${pkg}.oip${i}server_tunnel
		delete ${pkg}.oip${i}_redir
		delete ${pkg}.oip${i}_routing
		delete ${pkg}.oip${i}_transparent_proxy
		del_list ${pkg}.main.outbounds="oip${i}server_tunnel"
		del_list ${pkg}.main.inbounds="oip${i}_redir"
		del_list ${pkg}.main_routing.rules="oip${i}_routing"
	EOF
}

_gre_proxy_sections() {
	local pkg=$1 i=$2 tunnel=$3 puser ss2022 email line opt port values value nl='
'
	puser="$(echo "$tunnel" | jsonfilter -q -e "@.${pkg}.uuid")"
	ss2022="$(echo "$tunnel" | jsonfilter -q -e "@.${pkg}.ss2022")"
	email="$(echo "$tunnel" | jsonfilter -q -e "@.${pkg}.email")"
	if [ "$pkg" = "xray" ]; then
		port="180${i}"
	else
		port="$((1820 + i))"
	fi
	_gre_proxy_drop "$pkg" "$i"
	[ -n "$puser" ] || return
	uci -q set "${pkg}.oip${i}server_tunnel=outbound"
	uci -q show "${pkg}.omrout" | while IFS= read -r line; do
		opt="${line#${pkg}.omrout.}"
		[ "$opt" != "$line" ] || continue
		opt="${opt%%=*}"
		case "$opt" in
			tag|s_vless_reverse_*) continue ;;
		esac
		# A list element by element: joined into one value, the copy never
		# matched the main outbound (_gre_proxy_opts), and every sync
		# rewrote the tunnels and reloaded the network and the firewall.
		# uci get quotes a list element that holds a space.
		values="$(uci -q -d "$nl" get "${pkg}.omrout.${opt}")"
		case "$values" in
			*"$nl"*)
				uci -q delete "${pkg}.oip${i}server_tunnel.${opt}"
				echo "$values" | while IFS= read -r value; do
					case "$value" in
						"'"*" "*"'") value="${value#?}"; value="${value%?}" ;;
					esac
					uci -q add_list "${pkg}.oip${i}server_tunnel.${opt}=$value"
				done
				;;
			*) uci -q set "${pkg}.oip${i}server_tunnel.${opt}=$values" ;;
		esac
	done
	uci -q batch <<-EOF >/dev/null
		set ${pkg}.oip${i}server_tunnel.tag='oip${i}server_tunnel'
		set ${pkg}.oip${i}server_tunnel.s_socks_user_id="${puser}"
		set ${pkg}.oip${i}server_tunnel.s_trojan_user_id="${puser}"
		set ${pkg}.oip${i}server_tunnel.s_vmess_user_id="${puser}"
		set ${pkg}.oip${i}server_tunnel.s_vless_user_id="${puser}"
		add_list ${pkg}.main.outbounds="oip${i}server_tunnel"
		set ${pkg}.oip${i}_redir=inbound
		set ${pkg}.oip${i}_redir.tag='oip${i}_redir'
		set ${pkg}.oip${i}_redir.listen='0.0.0.0'
		set ${pkg}.oip${i}_redir.port='${port}'
		set ${pkg}.oip${i}_redir.protocol='dokodemo-door'
		set ${pkg}.oip${i}_redir.s_dokodemo_door_network='tcp'
		add_list ${pkg}.oip${i}_redir.s_dokodemo_door_network='udp'
		set ${pkg}.oip${i}_redir.ss_sockopt_tproxy='redirect'
		set ${pkg}.oip${i}_redir.ss_sockopt_tcp_fast_open='1'
		set ${pkg}.oip${i}_redir.ss_sockopt_mptcp='1'
		set ${pkg}.oip${i}_redir.s_dokodemo_door_follow_redirect='1'
		add_list ${pkg}.main.inbounds="oip${i}_redir"
		set ${pkg}.oip${i}_routing=routing_rule
		set ${pkg}.oip${i}_routing.tag="oip${i}_routing"
		set ${pkg}.oip${i}_routing.type='field'
		set ${pkg}.oip${i}_routing.inbound_tag="oip${i}_redir"
		set ${pkg}.oip${i}_routing.outbound_tag="oip${i}server_tunnel"
		add_list ${pkg}.main_routing.rules="oip${i}_routing"
		set ${pkg}.oip${i}_transparent_proxy=transparent_proxy
		set ${pkg}.oip${i}_transparent_proxy.proxy_mode='default'
		set ${pkg}.oip${i}_transparent_proxy.redirect_udp='1'
		set ${pkg}.oip${i}_transparent_proxy.redirect_port='${port}'
	EOF
	[ "$pkg" = "xray" ] && uci -q set "xray.oip${i}server_tunnel.s_vless_reality_user_id=${puser}"
	[ -n "$ss2022" ] && uci -q set "${pkg}.oip${i}server_tunnel.s_shadowsocks_password=$(uci -q get ${pkg}.omrout.s_shadowsocks_password | cut -d':' -f 1):$ss2022"
	[ -n "$email" ] && uci -q set "${pkg}.oip${i}server_tunnel.s_socks_email=$email"
}

# _gre_proxy_opts <xray|v2ray> <section>
# The options of an outbound but its user (tag, user ids, Shadowsocks
# password, SOCKS login, reverse tunnel), sorted
_gre_proxy_opts() {
	uci -q show "$1.$2" | sed -n "s/^$1\.$2\.//p" \
		| grep -v -E '^(tag|s_[a-z_]*user_id|s_shadowsocks_password|s_socks_email|s_vless_reverse_[a-z_]*)=' | sort
}

# Rewritten as one list: the zone's network can be a plain option
_gre_zone_vpn_add() {
	local intf all
	all="$(uci -q get firewall.zone_vpn.network)"
	case " $all " in
		*" $1 "*) return ;;
	esac
	uci -q delete firewall.zone_vpn.network
	for intf in $all $1; do
		uci -q add_list firewall.zone_vpn.network="$intf"
	done
}

# _gre_redir_outdated <package> <i>: the redir of public IP <i> has the
# options of the main one, hi1. A tunnel made while they were copied from
# "hi" (gone) kept the init's defaults: compared, it is rewritten once.
_gre_redir_outdated() {
	local opt
	for opt in local_address timeout fast_open no_delay; do
		[ "$(uci -q get "$1.oip$2.$opt")" != "$(uci -q get "$1.hi1.$opt")" ] && return 0
	done
	return 1
}

# True when the sections of public IP <i> differ from what the VPS sent
# (peeraddr, ipaddr, publicaddr, ssport, ssrust_password of _get_gre_tunnel)
_gre_tunnel_outdated() {
	local i=$1 tunnel=$2 pkg uuid
	[ "$(uci -q get network.oip${i}.ipaddr)" != "$peeraddr" ] && return 0
	[ "$(uci -q get network.oip${i}.gateway)" != "$ipaddr" ] && return 0
	[ "$(uci -q get network.oip${i}gre.ipaddr)" != "$vpnip_local" ] && return 0
	[ "$(uci -q get network.oip${i}gre.peeraddr)" != "$publicaddr" ] && return 0
	[ "$(uci -q get network.oip${i}_route.gateway)" != "$ipaddr" ] && return 0
	# The main server's address and key change with the VPS
	if _gre_uci_pkg shadowsocks-libev && [ -n "$ssport" ]; then
		[ "$(uci -q get shadowsocks-libev.oip${i}server.server_port)" != "$ssport" ] && return 0
		[ "$(uci -q get shadowsocks-libev.oip${i}server.server)" != "$(uci -q get shadowsocks-libev.sss0.server)" ] && return 0
		[ "$(uci -q get shadowsocks-libev.oip${i}server.key)" != "$(uci -q get shadowsocks-libev.sss0.key)" ] && return 0
		_gre_redir_outdated shadowsocks-libev "$i" && return 0
	fi
	if _gre_uci_pkg shadowsocks-rust && [ -n "$ssrust_password" ]; then
		[ "$(uci -q get shadowsocks-rust.oip${i}server.password)" != "$ssrust_password" ] && return 0
		[ "$(uci -q get shadowsocks-rust.oip${i}server.server)" != "$(uci -q get shadowsocks-rust.sss0.server)" ] && return 0
		_gre_redir_outdated shadowsocks-rust "$i" && return 0
	fi
	# The outbound is a copy of the main one: any change of it (protocol,
	# address, TLS pin, Reality key...) has to reach the copy
	for pkg in xray v2ray; do
		_gre_uci_pkg "$pkg" || continue
		uuid="$(echo "$tunnel" | jsonfilter -q -e "@.${pkg}.uuid")"
		# No user of the IP for it: no outbound, none to compare with
		if [ -z "$uuid" ]; then
			[ -n "$(uci -q get $pkg.oip${i}server_tunnel)" ] && return 0
			continue
		fi
		[ "$(uci -q get $pkg.oip${i}server_tunnel.s_vless_user_id)" != "$uuid" ] && return 0
		[ "$(_gre_proxy_opts $pkg omrout)" != "$(_gre_proxy_opts $pkg "oip${i}server_tunnel")" ] && return 0
	done
	return 1
}

# The sections of public IP <i>, which the VPS no longer has
_gre_drop_tunnel() {
	local i=$1 pkg
	uci -q batch <<-EOF >/dev/null
		delete network.oip${i}gre
		delete network.oip${i}
		delete network.oip${i}_route
		delete network.oip${i}_mark
		del_list firewall.zone_vpn.network="oip${i}gre"
		del_list firewall.zone_vpn.network="oip${i}"
	EOF
	for pkg in shadowsocks-libev shadowsocks-rust; do
		_gre_uci_pkg "$pkg" || continue
		uci -q batch <<-EOF >/dev/null
			delete ${pkg}.oip${i}server
			delete ${pkg}.oip${i}
			delete ${pkg}.oip${i}_rule
		EOF
	done
	for pkg in xray v2ray; do
		_gre_uci_pkg "$pkg" && _gre_proxy_drop "$pkg" "$i"
	done
}

_get_gre_tunnel() {
	local gre_tunnel_state vpnip_local tunnels tunnel i peeraddr ipaddr publicaddr ssport
	local ssrust_psk ssrust_method ssrust_password ss2022 changed dropped pkg
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	gre_tunnel_state="$(echo "$vps_config" | jsonfilter -q -e '@.gre_tunnel.enabled')"
	vpnip_local="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.remoteip')"
	tunnels=""
	[ "$gre_tunnel_state" = "true" ] && tunnels="$(echo "$vps_config" | jsonfilter -q -e '@.gre_tunnel.config[*]')"
	# shadowsocks-rust reaches a public IP through the VPS's xray
	# Shadowsocks 2022 inbound, as the xray user of that IP: the server key
	# of that inbound, then the user's
	ssrust_psk="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.sskey' | cut -d':' -f1)"
	ssrust_method="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.ss_method')"
	i=0
	# Not a pipe: the loop runs in this shell, so what it changed is
	# still known after it
	while IFS= read -r tunnel; do
		peeraddr="$(echo "$tunnel" | jsonfilter -q -e '@.remote_ip')"
		ipaddr="$(echo "$tunnel" | jsonfilter -q -e '@.local_ip')"
		publicaddr="$(echo "$tunnel" | jsonfilter -q -e '@.public_ip')"
		ssport="$(echo "$tunnel" | jsonfilter -q -e '@.shadowsocks_port')"
		ss2022="$(echo "$tunnel" | jsonfilter -q -e '@.xray.ss2022')"
		ssrust_password=""
		[ -n "$ssrust_psk" ] && [ -n "$ss2022" ] && ssrust_password="${ssrust_psk}:${ss2022}"
		if [ -n "$peeraddr" ] && [ -n "$ipaddr" ] && [ -n "$publicaddr" ] && [ -n "$vpnip_local" ] \
			&& _gre_tunnel_outdated "$i" "$tunnel"; then
			changed=1
			uci -q batch <<-EOF >/dev/null
				set network.oip${i}gre=interface
				set network.oip${i}gre.label="GRE tunnel for $publicaddr"
				set network.oip${i}gre.proto=gre
				set network.oip${i}gre.nohostroute='1'
				set network.oip${i}gre.ipv6='0'
				set network.oip${i}gre.defaultroute='0'
				set network.oip${i}gre.multipath='off'
				set network.oip${i}gre.peerdns='0'
				set network.oip${i}gre.ttl='255'
				set network.oip${i}gre.ip4table='vpn'
				set network.oip${i}gre.peeraddr="$publicaddr"
				set network.oip${i}gre.ipaddr="$vpnip_local"
				set network.oip${i}gre.tunlink='omrvpn'
				set network.oip${i}=interface
				set network.oip${i}.label="Tunnel for $publicaddr"
				set network.oip${i}.proto=static
				set network.oip${i}.nohostroute='1'
				set network.oip${i}.device="@oip${i}gre"
				set network.oip${i}.ipv6='0'
				set network.oip${i}.defaultroute='0'
				set network.oip${i}.multipath='off'
				set network.oip${i}.peerdns='0'
				set network.oip${i}.ip4table='vpn'
				set network.oip${i}.gateway="$ipaddr"
				set network.oip${i}.ipaddr="$peeraddr"
				set network.oip${i}.netmask="255.255.255.252"
				set network.oip${i}.lookup="667${i}"
				set network.oip${i}_route=route
				set network.oip${i}_route.interface="oip${i}"
				set network.oip${i}_route.target='0.0.0.0/0'
				set network.oip${i}_route.gateway="$ipaddr"
				set network.oip${i}_route.table="667${i}"
				set network.oip${i}_mark=rule
				set network.oip${i}_mark.mark="0x667${i}"
				set network.oip${i}_mark.lookup="667${i}"
				set network.oip${i}_mark.priority='2'
			EOF
			_gre_zone_vpn_add "oip${i}gre"
			_gre_zone_vpn_add "oip${i}"
			# hi1 for both: 1930-omr-shadowsocks renamed libev's "hi" to hi1,
			# and the redirs of the public IPs had the init's defaults
			if _gre_uci_pkg shadowsocks-libev && [ -n "$ssport" ]; then
				_gre_ss_sections shadowsocks-libev "$i" "$publicaddr" "$ssport" \
					"$(uci -q get shadowsocks-libev.sss0.method)" key "$(uci -q get shadowsocks-libev.sss0.key)" hi1
			fi
			if _gre_uci_pkg shadowsocks-rust && [ -n "$ssrust_password" ]; then
				_gre_ss_sections shadowsocks-rust "$i" "$publicaddr" 65252 \
					"${ssrust_method:-2022-blake3-aes-256-gcm}" password "$ssrust_password" hi1
			fi
			_gre_uci_pkg xray && _gre_proxy_sections xray "$i" "$tunnel"
			_gre_uci_pkg v2ray && _gre_proxy_sections v2ray "$i" "$tunnel"
		fi
		i=$((i+1))
	done <<-EOF
		$tunnels
	EOF
	[ -n "$tunnels" ] || i=0
	# The public IPs past the ones the VPS sent, or all of them once it
	# turned GRE off, stayed configured. Not on an error answer: it has no
	# gre_tunnel at all.
	if [ -n "$gre_tunnel_state" ] && _vps_config_valid; then
		while [ -n "$(uci -q get network.oip${i}gre)" ]; do
			_gre_drop_tunnel "$i"
			changed=1
			dropped=1
			i=$((i+1))
		done
	fi
	[ -n "$changed" ] || return 0
	logger -t "OMR-VPS" "GRE tunnels of the server public IPs updated"
	for pkg in network firewall shadowsocks-libev shadowsocks-rust xray v2ray; do
		[ -n "$(uci -q changes "$pkg")" ] && uci -q commit "$pkg"
	done
	# Committed only, the new or changed tunnels waited for an unrelated
	# network reload
	ubus call network reload >/dev/null 2>&1
	"$FIREWALL_INIT" reload >/dev/null 2>&1
	# A proxy only when a public IP of it is in use (rules enabled): the
	# sections are created disabled
	for pkg in shadowsocks-libev shadowsocks-rust; do
		_gre_uci_pkg "$pkg" || continue
		{ [ -n "$dropped" ] || uci -q show "$pkg" 2>/dev/null | grep -q "^${pkg}\.oip[0-9]*_rule\.disabled='0'"; } && /etc/init.d/"$pkg" reload >/dev/null 2>&1
	done
	for pkg in xray v2ray; do
		if [ "$(uci -q get $pkg.main.enabled)" = "1" ]; then
			/etc/init.d/$pkg reload >/dev/null 2>&1
		fi
	done
	return 0
}

# On the master (or current) server only: the GRE tunnels go through its VPN
_set_gre_tunnel_service() {
	local tokenserver
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return 0
	_is_config_server "$servername" || return 0
	vps_config=""
	tokenserver=$(_get_token $servername)
	_split_tokenserver "$tokenserver"
	[ -z "$token" ] && return 0
	_get_gre_tunnel
}

set_gre_tunnel() {
	config_load openmptcprouter
	config_foreach _set_gre_tunnel_service server
}

_set_vxlan_vps() {
	local settings result vxlan_enabled vxlan_current vxlan_mode vxlan_mode_current
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	if [ "$(uci -q get openmptcprouter.settings.vxlan)" = "1" ]; then
		vxlan_enabled="true"
	else
		vxlan_enabled="false"
	fi
	vxlan_mode="$(uci -q get openmptcprouter.settings.vxlan_mode)"
	[ "$vxlan_mode" = "l2" ] || vxlan_mode="l3"
	vxlan_current="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.enabled')"
	[ -z "$vxlan_current" ] && vxlan_current="false"
	vxlan_mode_current="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.mode')"
	[ -z "$vxlan_mode_current" ] && vxlan_mode_current="l3"
	if [ "$vxlan_current" != "$vxlan_enabled" ] || [ "$vxlan_mode_current" != "$vxlan_mode" ]; then
		settings='{"enable": '$vxlan_enabled', "mode": "'$vxlan_mode'"}'
		result=$(_set_json "vxlan" "$settings")
		[ "$(_parse_result "$result")" = "done" ] && vps_config=$(_get_json "config")
	fi
}

_bridge_name="omrvxlanbr"
_bridge_ifname="br-omrvxlan"
_vxlan_bridge_lookup=""
_vxlan_bridge_section=""
_vxlan_active_bridge_section=""

_vxlan_bridge_section_by_name_cb() {
	local section type name
	section="$1"
	[ -n "$_vxlan_bridge_section" ] && return
	config_get type "$section" type
	[ "$type" = "bridge" ] || return
	config_get name "$section" name
	if [ "$name" = "$_vxlan_bridge_lookup" ] || [ "$section" = "$_vxlan_bridge_lookup" ]; then
		_vxlan_bridge_section="$section"
	fi
}

_vxlan_bridge_section_by_name() {
	_vxlan_bridge_lookup="$1"
	_vxlan_bridge_section=""
	config_load network
	config_foreach _vxlan_bridge_section_by_name_cb device
	echo "$_vxlan_bridge_section"
}

_vxlan_active_bridge_section_cb() {
	local section target
	section="$1"
	[ -n "$_vxlan_active_bridge_section" ] && return
	config_get target "$section" omrvxlan_target_if
	[ -n "$target" ] && _vxlan_active_bridge_section="$section"
}

_vxlan_active_bridge_section() {
	if [ -n "$(uci -q get network.${_bridge_name})" ]; then
		echo "$_bridge_name"
		return
	fi
	_vxlan_active_bridge_section=""
	config_load network
	config_foreach _vxlan_active_bridge_section_cb device
	echo "$_vxlan_active_bridge_section"
}

# Which local network interface to bridge omrvxlan into (L2 mode); any
# configured interface is allowed, not just the LAN zone's. Falls back to
# lan when unset, malformed, nonexistent, or one of the tunnel's own
# interfaces (bridging into those would break the tunnel itself)
_vxlan_bridge_target_if() {
	local target
	target="$(uci -q get openmptcprouter.settings.vxlan_bridge_if)"
	case "$target" in
		''|*[!A-Za-z0-9_-]*|loopback|omrvpn|omrvxlan|omrvxlanip) target="lan" ;;
	esac
	[ -n "$(uci -q get network.$target)" ] || target="lan"
	echo "$target"
}

# Bridge omrvxlan into the configured interface's device (L2 mode): the
# tunnel carries that interface's broadcast domain instead of a routed P2P
# link. Defaults to lan when unset.
_vxlan_join_lan_bridge() {
	local target_if lan_device lan_ifname bridge_section
	target_if="$(_vxlan_bridge_target_if)"
	lan_device="$(uci -q get network.${target_if}.device)"
	lan_ifname="$(uci -q get network.${target_if}.ifname)"
	bridge_section="$(_vxlan_bridge_section_by_name "$lan_device")"
	if [ -n "$bridge_section" ]; then
		uci -q batch <<-EOF >/dev/null
			set network.${bridge_section}.omrvxlan_lan_device="$lan_device"
			set network.${bridge_section}.omrvxlan_target_if="$target_if"
			set network.${bridge_section}.omrvxlan_existing_bridge='1'
		EOF
		if [ -z "$(uci -q get network.${bridge_section}.ports | grep 'omrvxlan')" ]; then
			uci -q add_list network.${bridge_section}.ports='omrvxlan'
		fi
		return
	fi
	if [ "$lan_device" != "$_bridge_name" ]; then
		# Wrap the target interface's current device in a bridge so omrvxlan
		# can join it as a port; the original device and interface name are
		# stashed to unwind on disable or on a later target change
		uci -q batch <<-EOF >/dev/null
			set network.${_bridge_name}=device
			set network.${_bridge_name}.type='bridge'
			set network.${_bridge_name}.name="$_bridge_ifname"
			set network.${_bridge_name}.omrvxlan_lan_device="$lan_device"
			set network.${_bridge_name}.omrvxlan_target_if="$target_if"
		EOF
		[ -n "$lan_ifname" ] && uci -q set network.${_bridge_name}.omrvxlan_lan_ifname="$lan_ifname"
		uci -q add_list network.${_bridge_name}.ports="$lan_device"
		uci -q set network.${target_if}.device="$_bridge_ifname"
		uci -q delete network.${target_if}.ifname
	fi
	if [ -z "$(uci -q get network.${_bridge_name}.ports | grep 'omrvxlan')" ]; then
		uci -q add_list network.${_bridge_name}.ports='omrvxlan'
	fi
}

# Undo _vxlan_join_lan_bridge: drop omrvxlan from the bridge and, once it was
# the only extra port, restore the stashed interface's original device
_vxlan_leave_lan_bridge() {
	local bridge_section lan_device lan_ifname target_if existing_bridge
	bridge_section="$(_vxlan_active_bridge_section)"
	[ -z "$bridge_section" ] && bridge_section="$_bridge_name"
	lan_device="$(uci -q get network.${bridge_section}.omrvxlan_lan_device)"
	lan_ifname="$(uci -q get network.${bridge_section}.omrvxlan_lan_ifname)"
	target_if="$(uci -q get network.${bridge_section}.omrvxlan_target_if)"
	existing_bridge="$(uci -q get network.${bridge_section}.omrvxlan_existing_bridge)"
	[ -z "$target_if" ] && target_if="lan"
	uci -q del_list network.${bridge_section}.ports='omrvxlan'
	if [ "$existing_bridge" = "1" ]; then
		uci -q batch <<-EOF >/dev/null
			delete network.${bridge_section}.omrvxlan_lan_device
			delete network.${bridge_section}.omrvxlan_target_if
			delete network.${bridge_section}.omrvxlan_existing_bridge
		EOF
	elif [ -n "$lan_device" ]; then
		uci -q set network.${target_if}.device="$lan_device"
		if [ -n "$lan_ifname" ]; then
			uci -q set network.${target_if}.ifname="$lan_ifname"
		else
			uci -q delete network.${target_if}.ifname
		fi
		uci -q delete network.${bridge_section}
	fi
}

# Routed P2P mode: omrvxlan gets no address itself, omrvxlanip carries the
# tunnel IPs, both networks join the vpn firewall zone like any other VPN link
_vxlan_apply_l3() {
	local vxlan_ip6addr
	vxlan_ip6addr="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.remoteip6')"
	[ "$(uci -q get openmptcprouter.settings.disable_ipv6)" = "1" ] && vxlan_ip6addr=""
	uci -q batch <<-EOF >/dev/null
		set network.omrvxlanip=interface
		set network.omrvxlanip.label="VXLAN tunnel IPs"
		set network.omrvxlanip.proto=static
		set network.omrvxlanip.device="@omrvxlan"
		set network.omrvxlanip.ipaddr="$vxlan_ipaddr"
		set network.omrvxlanip.multipath='off'
		set network.omrvxlanip.defaultroute='0'
		set network.omrvxlanip.peerdns='0'
		delete network.omrvxlanip.ip6addr
	EOF
	[ -n "$vxlan_ip6addr" ] && uci -q set network.omrvxlanip.ip6addr="$vxlan_ip6addr"
	if [ -z "$(uci -q get firewall.zone_vpn.network | grep omrvxlan)" ]; then
		uci -q add_list firewall.zone_vpn.network="omrvxlan"
		uci -q add_list firewall.zone_vpn.network="omrvxlanip"
	fi
	ifup omrvxlanip >/dev/null 2>&1
}

_vxlan_teardown_l3() {
	ifdown omrvxlanip >/dev/null 2>&1
	uci -q batch <<-EOF >/dev/null
		delete network.omrvxlanip
		commit network
	EOF
	uci -q del_list firewall.zone_vpn.network="omrvxlan"
	uci -q del_list firewall.zone_vpn.network="omrvxlanip"
}

_get_vxlan() {
	local vxlan_state vxlan_vni vxlan_port vxlan_mtu vxlan_peeraddr vxlan_mode vxlan_ipaddr vxlan_bridge_if vxlan_bridge_section vxlan_bridge_if_stored vxlan_bridge_status
	[ -z "$servername" ] && servername=$1
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	# An error answer (a 500, FastAPI's {"detail": ...}) has no vxlan either:
	# taken as "disabled", it tore the tunnel down
	_vps_config_valid || return 0
	vxlan_state="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.enabled')"
	if [ "$vxlan_state" = "true" ] && [ "$(uci -q get openmptcprouter.settings.vpn)" != "none" ]; then
		vxlan_vni="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.vni')"
		vxlan_port="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.port')"
		vxlan_mtu="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.mtu')"
		vxlan_peeraddr="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.localip')"
		vxlan_mode="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.mode')"
		[ "$vxlan_mode" = "l2" ] || vxlan_mode="l3"
		vxlan_ipaddr="$(echo "$vps_config" | jsonfilter -q -e '@.vxlan.remoteip')"
		vxlan_bridge_if="$(_vxlan_bridge_target_if)"
		vxlan_bridge_section="$(_vxlan_active_bridge_section)"
		# A bridge from before this option existed has no stashed target;
		# it was always lan back then, so treat missing as lan rather than
		# as a mismatch (avoids an unnecessary rejoin on first run post-upgrade)
		vxlan_bridge_if_stored="$(uci -q get network.${vxlan_bridge_section:-$_bridge_name}.omrvxlan_target_if)"
		[ -z "$vxlan_bridge_if_stored" ] && vxlan_bridge_if_stored="lan"
		[ -z "$vxlan_vni" ] && return
		[ -z "$vxlan_peeraddr" ] && return
		[ "$vxlan_mode" = "l3" ] && [ -z "$vxlan_ipaddr" ] && return
		if [ "$(uci -q get network.omrvxlan.peeraddr)" != "$vxlan_peeraddr" ] || [ "$(uci -q get network.omrvxlan.vid)" != "$vxlan_vni" ] || [ "$(uci -q get network.omrvxlan.port)" != "$vxlan_port" ] || [ "$(uci -q get network.omrvxlan.mtu)" != "$vxlan_mtu" ] || { [ "$vxlan_mode" = "l2" ] && [ -z "$(uci -q get network.${vxlan_bridge_section:-$_bridge_name}.ports | grep 'omrvxlan')" ]; } || { [ "$vxlan_mode" = "l2" ] && [ -n "$vxlan_bridge_section" ] && [ "$vxlan_bridge_if_stored" != "$vxlan_bridge_if" ]; } || { [ "$vxlan_mode" = "l3" ] && [ "$(uci -q get network.omrvxlanip.ipaddr)" != "$vxlan_ipaddr" ]; }; then
			# If the mode changed since the last run, unwind whichever mode's
			# state is currently on disk before applying the new one. Also
			# unwind an L2 bridge whose target interface changed, so the
			# next join re-wraps the newly selected interface instead.
			[ -n "$(uci -q get network.omrvxlanip)" ] && [ "$vxlan_mode" = "l2" ] && _vxlan_teardown_l3
			[ -n "$vxlan_bridge_section" ] && [ "$vxlan_mode" = "l3" ] && _vxlan_leave_lan_bridge
			[ -n "$vxlan_bridge_section" ] && [ "$vxlan_mode" = "l2" ] && [ "$vxlan_bridge_if_stored" != "$vxlan_bridge_if" ] && _vxlan_leave_lan_bridge
			uci -q batch <<-EOF >/dev/null
				set network.omrvxlan=interface
				set network.omrvxlan.label="VXLAN tunnel over the VPN"
				set network.omrvxlan.proto=vxlan
				set network.omrvxlan.tunlink=omrvpn
				set network.omrvxlan.peeraddr="$vxlan_peeraddr"
				set network.omrvxlan.port="$vxlan_port"
				set network.omrvxlan.vid="$vxlan_vni"
				set network.omrvxlan.mtu="$vxlan_mtu"
				set network.omrvxlan.ttl='255'
				set network.omrvxlan.multipath='off'
				set network.omrvxlan.defaultroute='0'
				set network.omrvxlan.peerdns='0'
			EOF
			if [ "$vxlan_mode" = "l2" ]; then
				_vxlan_join_lan_bridge
			else
				_vxlan_apply_l3
			fi
			uci -q commit network
			# The vpn zone input chain only accepts ICMP: without this rule the
			# VXLAN UDP encap replies arriving on the VPN tunnel are rejected
			uci -q batch <<-EOF >/dev/null
				set firewall.omrvxlan=rule
				set firewall.omrvxlan.enabled='1'
				set firewall.omrvxlan.target='ACCEPT'
				set firewall.omrvxlan.name='Allow-VXLAN'
				set firewall.omrvxlan.proto='udp'
				set firewall.omrvxlan.src='vpn'
				set firewall.omrvxlan.dest_port='${vxlan_port:-4789}'
				commit firewall
			EOF
			logger -t "OMR-VPS" "VXLAN tunnel configured over the VPN (mode: $vxlan_mode)"
			ifup omrvxlan >/dev/null 2>&1
			ubus call network reload >/dev/null 2>&1
			"$FIREWALL_INIT" reload >/dev/null 2>&1
			if [ "$vxlan_mode" = "l2" ]; then
				# The target interface's device now points at the bridge. A
				# bridge can report kernel-level "up" even when the actual L3
				# path is broken (e.g. tunnel not really passing traffic), so
				# also require a live IPv4 address -- that's what an interface
				# doubling as the management/LAN path actually needs to keep
				# working. Unwind rather than strand the router.
				sleep 2
				vxlan_bridge_status="$(ubus call network.interface.${vxlan_bridge_if} status 2>/dev/null)"
				if [ -n "$vxlan_bridge_status" ] && { [ "$(echo "$vxlan_bridge_status" | jsonfilter -q -e '@.up')" != "true" ] || \
				   [ -z "$(echo "$vxlan_bridge_status" | jsonfilter -q -e '@["ipv4-address"][0].address')" ]; }; then
					logger -t "OMR-VPS" "VXLAN L2 bridge on ${vxlan_bridge_if} did not come up cleanly -- rolling back to avoid stranding the router"
					_vxlan_leave_lan_bridge
					uci -q commit network
					ubus call network reload >/dev/null 2>&1
				fi
			fi
		fi
	elif [ -n "$(uci -q get network.omrvxlan)" ]; then
		[ -n "$(uci -q get network.omrvxlanip)" ] && _vxlan_teardown_l3
		[ -n "$(_vxlan_active_bridge_section)" ] && _vxlan_leave_lan_bridge
		ifdown omrvxlan >/dev/null 2>&1
		uci -q batch <<-EOF >/dev/null
			delete network.omrvxlan
			commit network
		EOF
		uci -q delete firewall.omrvxlan
		uci -q commit firewall
		ubus call network reload >/dev/null 2>&1
		"$FIREWALL_INIT" reload >/dev/null 2>&1
		logger -t "OMR-VPS" "VXLAN tunnel removed"
	fi
}

# omr-schedule runs set_vxlan every minute while openmptcprouter.settings.vxlan
# and the omrvxlan interface disagree. When the tunnel cannot be set up (no
# VPN, a VPS without VXLAN, a read only user, a refused login) that was a
# login and a GET /config every minute. Tried again after 5 minutes, then
# 10, 20... up to an hour; a change of the setting tries at once.
_set_vxlan_service() {
	local marker="/tmp/.omr-vps-vxlan-$1" want now last delay wanted stuck=0
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	# VXLAN rides on top of the VPN of the master/current server only
	_is_config_server "$servername" || return
	want="$(uci -q get openmptcprouter.settings.vxlan)"
	[ "$want" = "1" ] || want=0
	now="$(date +%s)"
	if [ -f "$marker" ]; then
		read -r last delay wanted < "$marker"
		case "$last$delay" in
			''|*[!0-9]*) ;;
			*) [ "$wanted" = "$want" ] && [ "$((now - last))" -lt "$delay" ] && return ;;
		esac
	fi
	_set_vxlan_sync
	if [ -n "$(uci -q get network.omrvxlan)" ]; then
		[ "$want" = "1" ] || stuck=1
	else
		[ "$want" = "1" ] && stuck=1
	fi
	if [ "$stuck" = "1" ]; then
		if [ "$wanted" = "$want" ] && [ -n "$delay" ]; then
			delay=$((delay * 2))
			[ "$delay" -gt 3600 ] && delay=3600
		else
			delay=300
			logger -t "OMR-VPS" "VXLAN can't be set up with server ${servername} yet, next try in ${delay}s"
		fi
		echo "$now $delay $want" > "$marker"
	else
		rm -f "$marker"
	fi
}

_set_vxlan_sync() {
	local tokenserver user_permission
	vps_config=""
	tokenserver=$(_get_token $servername)
	_split_tokenserver "$tokenserver"
	[ -z "$token" ] && return
	vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	user_permission="$(echo "$vps_config" | jsonfilter -q -e '@.user.permission')"
	[ "$user_permission" != "ro" ] && _set_vxlan_vps
	_get_vxlan
}

set_vxlan() {
	config_load openmptcprouter
	config_foreach _set_vxlan_service server
}


# dnsmasq's server list holds <entry> as a whole word
_dnsmasq_has_server() {
	case " $(uci -q get dhcp.@dnsmasq[0].server) " in
		*" $1 "*) return 0 ;;
	esac
	return 1
}

# The Pi-hole of a VPS that turned it off: dnsmasq back on the local
# resolver, as omr-tracker's disable_pihole does while the VPN is down. Only
# set_pihole pointed dnsmasq at it, and it is never asked again once it did:
# the tunnel address stayed its DNS server.
_pihole_dnsmasq_off() {
	local _s _found=""
	[ "$(uci -q get openmptcprouter.settings.pihole_auto_conf)" = "0" ] && return
	for _s in $(uci -q get dhcp.@dnsmasq[0].server); do
		case "$_s" in
			10.255.*#53) _found="$_found $_s" ;;
		esac
	done
	[ -n "$_found" ] || return 0
	logger -t "OMR-VPS" "Pi-hole disabled on server ${servername}, DNS back to the local resolver"
	for _s in $_found; do
		uci -q del_list dhcp.@dnsmasq[0].server="$_s"
	done
	_dnsmasq_has_server "127.0.0.1#5353" || uci -q add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'
	uci -q commit dhcp
	/etc/init.d/dnsmasq restart >/dev/null 2>&1
}

_get_pihole() {
	[ "$(uci -q get openmptcprouter.settings.pihole_auto_conf)" = "0" ] && return
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	vpn="$(uci -q get openmptcprouter.settings.vpn)"
	piholeomr="$(uci -q get openmptcprouter.${servername}.pihole)"
	pihole="$(echo "$vps_config" | jsonfilter -q -e '@.pihole.state')"
	if [ "$pihole" = "true" ] && [ "$piholeomr" != "1" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.${servername}.pihole='1'
			commit openmptcprouter
		EOF
	elif [ "$pihole" = "false" ] && [ "$piholeomr" != "0" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.${servername}.pihole='0'
			commit openmptcprouter
		EOF
	fi
	# dnsmasq has one Pi-hole: the one behind the tunnel, not a backup
	# server's, whose tunnel address may not be reachable (set_pihole
	# picks it)
	[ -z "$pihole_dns_server" ] || [ "$servername" = "$pihole_dns_server" ] || return
	if [ "$pihole" != "true" ]; then
		_vps_config_valid && _pihole_dnsmasq_off
		return
	fi

	# The VPS address of the VPN, where its Pi-hole answers. The list is the
	# one of _set_pihole_server and omr-tracker's 003-up.
	case "$vpn" in
		glorytun_tcp) host_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.tcp.host_ip')" ;;
		glorytun_udp) host_ip="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.udp.host_ip')" ;;
		openvpn|mlvpn|dsvpn|mqvpn) host_ip="$(echo "$vps_config" | jsonfilter -q -e "@.${vpn}.host_ip")" ;;
		*) return ;;
	esac
	# An empty answer made grep match everything and put "#53" in dnsmasq
	case "$host_ip" in
		''|*[!0-9.]*) return ;;
	esac
	if [ "$pihole" = "true" ] && ! _dnsmasq_has_server "${host_ip}#53"; then
		# The Pi-hole of a VPN used before: only OMR's tunnel addresses, a
		# user's own 10.2x server stays
		for _s in $(uci -q get dhcp.@dnsmasq[0].server); do
			case "$_s" in
				10.255.*#53) uci -q del_list dhcp.@dnsmasq[0].server="$_s" ;;
			esac
		done
		uci -q batch <<-EOF >/dev/null
			del_list dhcp.@dnsmasq[0].server="127.0.0.1#5353"
			add_list dhcp.@dnsmasq[0].server="$host_ip#53"
			set dhcp.@dnsmasq[0].rebind_protection='0'
			commit dhcp
		EOF
		logger -t "OMR-VPS" "Restart dnsmasq..."
		/etc/init.d/dnsmasq restart >/dev/null 2>&1
	fi
}

_set_redirect_ports_from_vps() {
	redirect_ports=$1
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	redirect_ports_current="$(echo "$vps_config" | jsonfilter -q -e '@.shorewall.redirect_ports')"
	[ "$redirect_ports" = "1" ] && redirect_ports_request="enable"
	[ "$redirect_ports" = "0" ] && redirect_ports_request="disable"
	if [ "$redirect_ports_request" != "$redirect_ports_current" ]; then
		settings='{"redirect_ports": "'$redirect_ports_request'"}'
		echo $(_set_json "shorewall" "$settings")
	else
		echo 1
	fi

}

_set_mptcp_vps() {
	local settings path_manager_changed
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	mptcp_enabled_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.enabled')"
	checksum_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.checksum')"
	path_manager_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.path_manager')"
	scheduler_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.scheduler')"
	syn_retries_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.syn_retries')"
	congestion_control_current="$(echo "$vps_config" | jsonfilter -q -e '@.network.congestion_control')"
	mptcp_version_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.version')"
	close_timeout_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.close_timeout')"
	pm_type_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.pm_type')"
	stale_loss_cnt_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.stale_loss_cnt')"
	syn_retrans_current="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.syn_retrans_before_tcp_fallback')"
	mptcp_enabled="$(uci -q get network.globals.multipath)"
	if [ "$mptcp_enabled" = "disable" ]; then
		mptcp_enabled="0"
	else
		mptcp_enabled="1"
	fi
	checksum="$(uci -q get network.globals.mptcp_checksum)"
	path_manager="$(uci -q get network.globals.mptcp_path_manager)"
	scheduler="$(uci -q get network.globals.mptcp_scheduler)"
	syn_retries="$(uci -q get network.globals.mptcp_syn_retries)"
	congestion="$(uci -q get network.globals.congestion)"
	mptcp_version="$(uci -q get network.globals.mptcp_version)"
	close_timeout="$(uci -q get network.globals.mptcp_close_timeout)"
	pm_type="$(uci -q get network.globals.mptcp_pm_type)"
	stale_loss_cnt="$(uci -q get network.globals.mptcp_stale_loss_cnt)"
	syn_retrans_before_tcp_fallback="$(uci -q get network.globals.mptcp_syn_retrans_before_tcp_fallback)"
	[ -z "$mptcp_version" ] && mptcp_version="0"
	[ ! -f /proc/sys/net/mptcp/mptcp_enabled ] && mptcp_version="1"
	[ -z "$congestion" ] && congestion="bbr"
	# These four v1-only knobs are recent additions: on a router that
	# predates them, or where the user never touched them, their uci option
	# is simply absent and `uci -q get` returns "". Left as "", the JSON
	# below would post e.g. "close_timeout": "" for an int field the VPS
	# side declares as `int = 0`, which the VPS's admin API rejects with a
	# bare 422 before it ever reaches its own error handling -- see
	# https://github.com/Ysurac/openmptcprouter/issues/4350.
	[ -z "$close_timeout" ] && close_timeout="0"
	[ -z "$pm_type" ] && pm_type="0"
	[ -z "$stale_loss_cnt" ] && stale_loss_cnt="0"
	[ -z "$syn_retrans_before_tcp_fallback" ] && syn_retrans_before_tcp_fallback="0"
	# The VPS reports the path manager its own kernel really registered, and
	# maps the out-of-tree v0.9x vocabulary (default/fullmesh/ndiffports/
	# binder/netlink, all of them in-kernel path managers) onto mainline's
	# 'kernel' before applying it. So 'kernel' there against one of those
	# names here is not a difference worth pushing -- re-sending 'fullmesh'
	# at every sync only makes the VPS warn and renormalize it again -- while
	# 'kernel' there against 'userspace' here is a real change and used to be
	# swallowed by a blanket "ignore 'kernel'" test, leaving the switch to
	# mptcpd's userspace PM stuck on the router until some *other* mptcp
	# setting happened to change too.
	path_manager_changed=0
	if [ -n "$path_manager_current" ] && [ "$path_manager_current" != "$path_manager" ]; then
		path_manager_changed=1
		if [ "$path_manager_current" = "kernel" ]; then
			case "$path_manager" in
				default|fullmesh|ndiffports|binder|netlink) path_manager_changed=0 ;;
			esac
		fi
	fi
	if [ "$mptcp_enabled_current" != "$mptcp_enabled" ] || [ "$checksum_current" != "$checksum" ] || [ "$path_manager_changed" = "1" ] || ([ "$scheduler_current" != "" ] && [ "$scheduler_current" != "$scheduler" ]) || ([ "$syn_retries_current" != "" ] && [ "$syn_retries_current" != "$syn_retries" ]) || [ "$congestion_control_current" != "$congestion" ] || [ "$mptcp_version_current" != "$mptcp_version" ] || ([ -n "$close_timeout_current" ] && [ "$close_timeout_current" != "0" ] && [ "$close_timeout_current" != "$close_timeout" ]) || ([ -n "$pm_type_current" ] && [ "$pm_type_current" != "0" ] && [ "$pm_type_current" != "$pm_type" ]) || ([ -n "$stale_loss_cnt_current" ] && [ "$stale_loss_cnt_current" != "0" ] && [ "$stale_loss_cnt_current" != "$stale_loss_cnt" ]) || ([ -n "$syn_retrans_current" ] && [ "$syn_retrans_current" != "0" ] && [ "$syn_retrans_current" != "$syn_retrans_before_tcp_fallback" ]); then
		settings='{"enabled" : "'$mptcp_enabled'", "checksum": "'$checksum'","path_manager": "'$path_manager'","scheduler": "'$scheduler'","syn_retries": "'$syn_retries'","congestion_control": "'$congestion'","version": "'$mptcp_version'","close_timeout": "'$close_timeout'","pm_type": "'$pm_type'","stale_loss_cnt": "'$stale_loss_cnt'","syn_retrans_before_tcp_fallback": "'$syn_retrans_before_tcp_fallback'"}'
		echo $(_set_json "mptcp" "$settings")
	else
		echo 1
	fi
}

# The MPTCP endpoint ids of a WAN whose IPv4 id is $1 (its ip4table, the
# metric multipath gets): IPv4 has that id, the next addresses of a
# dual-stack WAN id+128 and id+192 (see multipath's add_endpoints), so the
# VPS pins hold for its IPv6 subflows too. None above 255: multipath gives
# no explicit id then, and the VPS refuses the whole list for one of them.
_wan_remote_ids() {
	local id="$1" extra ids="$1"
	[ "$id" -le 255 ] 2>/dev/null || return 0
	for extra in 128 192; do
		[ $((id + extra)) -le 255 ] && ids="$ids $((id + extra))"
	done
	echo "$ids"
}

# Sets first_subflow_wan to the WAN that opens the connections to this VPS.
# The VPS knows the first subflow of a connection by remote id 0, not by the
# endpoint id of its WAN, so a pin or a weight by endpoint id never matched
# the master WAN: measured, a download pinned to it stayed spread over every
# WAN while {0} alone put 99% of it on the master. This is the WAN the route
# to the VPS sends a new connection by: its only nexthop, or in balancing
# mode the one with the highest weight (the master, 100 against 1). Read
# from the route rather than from the master option, so that it follows a
# failover; the sync's cache makes the change one push.
_get_first_subflow_wan() {
	local vpsip dev
	first_subflow_wan=""
	vpsip="$(uci -q get openmptcprouter.${servername}.ip | awk '{print $1}')"
	# a VPS given by name: "ip route show <name>" is an error, and remote
	# id 0 was never pinned nor weighted
	case "$vpsip" in
		*[!0-9.]*) vpsip="$(resolveip -t 5 -4 "$vpsip" | head -n 1)" ;;
	esac
	[ -n "$vpsip" ] || return 0
	dev="$(ip -4 route show "$vpsip" 2>/dev/null | awk '
		NR > 1 && /^[^ \t]/ { exit }
		{
			d = ""; w = 1
			for (i = 1; i < NF; i++) {
				if ($i == "dev") d = $(i + 1)
				if ($i == "weight") w = $(i + 1)
			}
			if (d != "" && w + 0 > best + 0) { best = w; bd = d }
		}
		END { print bd }')"
	[ -n "$dev" ] || return 0
	. /lib/functions/network.sh
	config_foreach _match_first_subflow_wan interface "$dev"
}

_match_first_subflow_wan() {
	local l3dev
	[ -n "$first_subflow_wan" ] && return
	network_get_device l3dev "$1"
	[ -z "$l3dev" ] && l3dev="$(uci -q get network.$1.device)"
	[ "$l3dev" = "$2" ] && first_subflow_wan="$1"
}

# Only a WAN that MPTCP uses has subflows to pin to. The pushed sets took
# every interface with an ip4table: docker, or a WAN taken out of
# multipath, kept its ids in the VPS's maps.
_vps_pin_wan() {
	case "$(uci -q get network.$1.multipath)" in
		on|master|backup|handover) return 0 ;;
	esac
	return 1
}

_get_dscp_vps_pin() {
	local section="$1" dscp remote_id remote_ids download_iface
	# Reads network's anonymous `config dscp_pin` sections (added by
	# luci-app-mptcp's DSCP/Weight Routing page), one row per DSCP class
	# with independent upload_interface/download_interface. Only
	# download_interface matters here -- the download side is deliberately
	# free to point at a different WAN than the upload pin
	# (041-multipath-dscp/dscp_iface handles upload, locally, and never
	# looks at this field at all).
	download_iface="$(uci -q get network.${section}.download_interface)"
	if [ -z "$download_iface" ]; then
		# No explicit download pin for this class -- always fall back to
		# the row's own upload_interface instead of pushing nothing. This
		# mirrors mqvpn's own downlink, which always follows upload with
		# no separate field at all.
		download_iface="$(uci -q get network.${section}.upload_interface)"
		[ -z "$download_iface" ] && return
	fi
	_vps_pin_wan "$download_iface" || return
	dscp="$(uci -q get network.${section}.dscp)"
	[ -z "$dscp" ] && return
	remote_id="$(uci -q get network.${download_iface}.ip4table)"
	case "$remote_id" in
		''|*[!0-9]*) return ;;
	esac
	remote_ids="$(_wan_remote_ids "$remote_id" | tr ' ' ',')"
	[ -z "$remote_ids" ] && return
	[ "$download_iface" = "$first_subflow_wan" ] && remote_ids="${remote_ids},0"
	# remote_id alone for an omradmin that only knows one id per class
	[ -n "$dscp_pins" ] && dscp_pins="${dscp_pins},"
	dscp_pins="${dscp_pins}{\"dscp\":\"${dscp}\",\"remote_id\":${remote_id},\"remote_ids\":[${remote_ids}]}"
}

# _vps_push_done <cache> <desired> <answer>: only what the VPS applied is
# cached as in sync. Any answer was before, a refusal too (a 422, the 404 of
# an omr-admin without the route): it was never sent again. What it did not
# apply is tried again, the same set at most every 10 minutes
# (_vps_push_wait): the tracker hooks ask on every cycle, a login and a POST
# each time. A refusal that stays (a read only user, the manager not
# installed on the VPS) hourly only, as _set_json_once does.
_vps_push_done() {
	local wait=600
	case "$(_parse_result "$3")" in
		done)
			echo "$2" > "$1"
			rm -f "$1.retry"
			return
			;;
		permission|warning) wait=3600 ;;
	esac
	echo "$(date +%s) $wait $2" > "$1.retry"
}

_vps_push_wait() { # <cache> <desired>
	local last wait pushed
	[ -f "$1.retry" ] || return 1
	read -r last wait pushed < "$1.retry"
	case "$last" in
		''|*[!0-9]*) return 1 ;;
	esac
	case "$wait" in
		''|*[!0-9]*) return 1 ;;
	esac
	[ "$pushed" = "$2" ] && [ "$(($(date +%s) - last))" -lt "$wait" ]
}

# Pushes network's dscp_pin rows' download_interface (router-side pick of
# which WAN should carry each DSCP class on the way *down*, see
# mptcp-dscp-manager) to the VPS as {dscp -> MPTCP remote endpoint id}
# pairs, so the VPS's own bpf_dscp scheduler can pin its send-side subflow
# choice for the same DSCP classes via dscp_remote_ids -- see
# mptcp-bpf-dscp/debian/README.Debian for why that map is keyed by
# remote_id instead of local IP on the VPS. Closes the "no automated
# sync... copy the number over manually" gap that doc used to describe.
# download_interface is independent of the same row's upload_interface
# (applied locally, separately, by 041-multipath-dscp) -- a class's
# upload and download sides can legitimately be pinned to different WANs.
# When a row has no download_interface at all, _get_dscp_vps_pin always
# falls back to that row's upload_interface instead, so a class only
# pinned on Upload still gets a gateway-side pin rather than none.
#
# Skips the round trip to the VPS entirely (not even a GET) when the
# locally-desired pin set matches the last one we successfully pushed,
# cached in $dscp_vps_pins_cache -- cheap enough to call unconditionally
# from a tracker hook every cycle. The cache is process-ephemeral (/tmp),
# which is intentional: it forces one real push after every reboot,
# matching the BPF map itself also resetting on reboot.
_set_mptcp_dscp_vps() {
	local dscp_pins="" desired dscp_vps_pins_cache="/tmp/.mptcp_dscp_vps_pins-${servername}" first_subflow_wan
	config_load network
	_get_first_subflow_wan
	config_foreach _get_dscp_vps_pin dscp_pin
	desired="[${dscp_pins}]"
	if [ "$(cat "$dscp_vps_pins_cache" 2>/dev/null)" = "$desired" ]; then
		echo 1
		return
	fi
	_vps_push_wait "$dscp_vps_pins_cache" "$desired" && return
	result=$(_set_json "mptcp_dscp" "{\"pins\":${desired}}")
	_vps_push_done "$dscp_vps_pins_cache" "$desired" "$result"
	[ -n "$result" ] && echo "$result"
}

# Standalone entry point (see EXTRA_COMMANDS) so the mptcp-dscp-manager
# tracker hook can trigger a sync without going through the full
# _config_service()/start_service() cycle. Bootstraps servername/token
# itself, same pattern as _set_vps_firewall's standalone use.
set_mptcp_dscp_vps() {
	local dscpservername=$1
	if [ -z "$dscpservername" ]; then
		config_load openmptcprouter
		config_foreach set_mptcp_dscp_vps server
		return
	fi
	servername=$dscpservername
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token=""
	_set_mptcp_dscp_vps
}

_get_weight_vps_pin() {
	local iface="$1" weight remote_id id ids _mw
	# Unset is 100, as in the router's map (the MPTCP page leaves it unset
	# when its Weight field is emptied). The WAN was left out of the pins,
	# which is 100 only on a VPS that drops the ids it no longer gets.
	# Normalised as mptcp-weight-manager's 040-multipath-weight puts it in
	# the map: 1 to 256, no leading zero. LuCI's uinteger takes "050", and
	# "weight":050 isn't JSON: the VPS refused the pins of every WAN.
	weight="$(uci -q get network.${iface}.multipath_weight)"
	case "$weight" in
		''|*[!0-9]*) weight="" ;;
		*)
			weight="${weight#"${weight%%[!0]*}"}"
			if [ -z "$weight" ]; then
				weight=1
			elif [ "${#weight}" -gt 3 ] || [ "$weight" -gt 256 ]; then
				weight=256
			fi
			;;
	esac
	weight="${weight:-100}"
	# The VPS model's weight, kept in /var/state by omr-weight-sync while it
	# runs with decision weights on, is the one in the router's map: send that
	_mw="$(uci -q -P /var/state get openmptcprouter.${iface}.decision_weight)"
	case "$_mw" in
		''|*[!0-9]*) _mw="" ;;
		*) _mw="${_mw#"${_mw%%[!0]*}"}" ;;
	esac
	if [ -n "$_mw" ] && [ -n "$(uci -q get omr-metrics.settings)" ] && \
	   [ "$(uci -q get omr-metrics.settings.enable_weight_sync)" != "0" ] && \
	   [ "$(uci -q get omr-metrics.settings.enable_decision_weights)" != "0" ]; then
		weight="$_mw"
	fi
	case "$weight" in
		''|*[!0-9]*) return ;;
	esac
	_vps_pin_wan "$iface" || return
	remote_id="$(uci -q get network.${iface}.ip4table)"
	case "$remote_id" in
		''|*[!0-9]*) return ;;
	esac
	ids="$(_wan_remote_ids "$remote_id")"
	[ -n "$ids" ] && [ "$iface" = "$first_subflow_wan" ] && ids="$ids 0"
	for id in $ids; do
		[ -n "$weight_pins" ] && weight_pins="${weight_pins},"
		weight_pins="${weight_pins}{\"remote_id\":${id},\"weight\":${weight}}"
	done
}

# VPS-side counterpart of _set_mptcp_dscp_vps, same reasoning: pushes
# network.<iface>.multipath_weight (router-side bpf_weight/bpf_weight_rr
# weights, see mptcp-weight-manager) to the VPS as
# {MPTCP remote endpoint id -> weight} pairs, so the VPS's own scheduler
# can weight its send-side subflow choice for the same WANs via
# weight_remote_id instead of defaulting to neutral 100 for everything.
_set_mptcp_weight_vps() {
	local weight_pins="" desired weight_vps_pins_cache="/tmp/.mptcp_weight_vps_pins-${servername}" first_subflow_wan
	config_load network
	_get_first_subflow_wan
	config_foreach _get_weight_vps_pin interface
	desired="[${weight_pins}]"
	if [ "$(cat "$weight_vps_pins_cache" 2>/dev/null)" = "$desired" ]; then
		echo 1
		return
	fi
	_vps_push_wait "$weight_vps_pins_cache" "$desired" && return
	result=$(_set_json "mptcp_weight" "{\"weights\":${desired}}")
	_vps_push_done "$weight_vps_pins_cache" "$desired" "$result"
	[ -n "$result" ] && echo "$result"
}

# Standalone entry point (see EXTRA_COMMANDS), same bootstrap pattern as
# set_mptcp_dscp_vps.
set_mptcp_weight_vps() {
	local wservername=$1
	if [ -z "$wservername" ]; then
		config_load openmptcprouter
		config_foreach set_mptcp_weight_vps server
		return
	fi
	servername=$wservername
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token=""
	_set_mptcp_weight_vps
}

# The pins of the scheduler in use, with the same gates as the tracker hooks.
# 041-multipath-dscp only syncs when a pin row names the tracked WAN or its
# own prune removed a class: a deleted row with a download interface only, a
# deletion the VPS did not apply, or the last row deleted before a reboot,
# were never pushed, and the VPS kept pinning the class. Run by
# _config_service (boot, then at least hourly) and on network changes
# (service_triggers); the caches make it a no-op once the VPS has the set.
_set_mptcp_pins_vps() {
	[ "$(uci -q get network.globals.mptcp_dscp_weight_vps_sync)" = "0" ] && return
	case "$(uci -q get network.globals.mptcp_scheduler)" in
		mptcp_bpf_dscp.o|bpf_dscp)
			_set_mptcp_dscp_vps
			;;
		mptcp_bpf_weight.o|mptcp_bpf_burstweight.o|mptcp_bpf_weight_rr.o|mptcp_bpf_overflow.o|bpf_weight|bpf_burstweight|bpf_weight_rr|bpf_overflow)
			_set_mptcp_weight_vps
			;;
	esac
}

set_mptcp_pins_vps() {
	local pservername=$1
	if [ -z "$pservername" ]; then
		config_load openmptcprouter
		config_foreach set_mptcp_pins_vps server
		return
	fi
	servername=$pservername
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token=""
	_set_mptcp_pins_vps
}

# Reads one DSCP class's current ipset/nftables-set members on this router
# ($2 = "4" or "6"), trying both the legacy iptables+ipset naming (omr-dscp:
# omr_dscp-<class> / omr_dscp6-<class>) and the fw4/nftables naming
# (omr-dscp-nft: omr_dscp_<class>_4 / omr_dscp_<class>_6) -- exactly one of
# the two variants is installed, the other command is just a silent no-op.
# `nft list set`'s plain text output (not -j/JSON) is used deliberately: its
# "elements = { a, b, c }" line has been a stable format for years and is a
# single sed+tr away from a token list, whereas the JSON element shape
# differs (bare string vs {"prefix":...} object) depending on whether an
# entry was inserted as a single IP or a CIDR, which would need two
# different jsonfilter paths to cover reliably.
#
# omr-bypass's DSCP rules fill sets of their own since they stopped sharing
# omr-dscp's names (omr_dst_bypass_dscp_<class>_4 / _6): they are read too,
# merged with omr-dscp's and deduplicated, or the VPS never learned about
# them. Either set may be missing. The elements of a long set are spread over
# several lines, and one may carry a timeout; a range (adjacent addresses
# merged in an interval set) is no CIDR, and the VPS refuses the whole list
# for a single one, so it is left out.
_get_dscp_class_members() {
	local class="$1" fam="$2" legacy_name nset

	[ "$fam" = "6" ] && legacy_name="omr_dscp6-${class}" || legacy_name="omr_dscp-${class}"

	{
		ipset -q -o save list "$legacy_name" 2>/dev/null | awk '/^add/ {print $3}'
		for nset in "omr_dscp_${class}_${fam}" "omr_dst_bypass_dscp_${class}_${fam}"; do
			nft list set inet fw4 "$nset" 2>/dev/null
		done | awk '
			/elements = \{/ { e = 1; sub(/.*elements = \{/, "") }
			e {
				done = sub(/\}.*/, "")
				n = split($0, parts, ",")
				for (i = 1; i <= n; i++) {
					split(parts[i], w, " ")
					if (w[1] != "" && w[1] !~ /-/) print w[1]
				}
				if (done) e = 0
			}'
	} | awk '!seen[$0]++'
}

_get_dscp_classify_entries() {
	local class="$1" cidr
	for cidr in $(_get_dscp_class_members "$class" 4) $(_get_dscp_class_members "$class" 6); do
		[ -n "$dscp_classify_entries" ] && dscp_classify_entries="${dscp_classify_entries},"
		dscp_classify_entries="${dscp_classify_entries}{\"dscp\":\"${class}\",\"cidr\":\"${cidr}\"}"
	done
}

# Pushes the router's live omr-dscp/omr-dscp-nft classification (which
# destination IPs are currently resolved into which DSCP class, e.g. by
# domain lists such as config/dscp's `config domains` and
# videochatipv4.list/videochatipv6.list) to the VPS, so the VPS's own
# outbound traffic to those same destinations -- e.g. ssserver's connection
# on behalf of a proxied session, see shadowsocks-rust's
# 002-add-dynamic-per-connection-dscp-reflection.patch for the router-side
# half of this -- can be marked with the same DSCP via a mirrored
# ipset+mangle rule on the VPS, without ever needing the DSCP bits
# themselves to survive the router->VPS hop on the wire.
#
# Unlike _set_mptcp_dscp_vps/_set_mptcp_weight_vps (a handful of pins that
# rarely change), this table can run into hundreds of resolved IPs and
# grows continuously as LAN devices browse to newly-resolved destinations,
# so on top of the same "skip if unchanged" cache this also self-throttles
# to at most one real attempt (enumerating up to 9 classes x 2 families of
# ipset/nft lookups, then an HTTPS round trip) per
# DSCP_CLASSIFY_MIN_INTERVAL seconds, independent of how often the calling
# hook itself fires.
_set_dscp_classify_vps() {
	local dscp_classify_entries="" desired class now last \
		dscp_classify_cache="/tmp/.dscp_classify_vps-${servername}" \
		dscp_classify_ts="/tmp/.dscp_classify_vps_ts-${servername}"

	now=$(date +%s)
	last=$(cat "$dscp_classify_ts" 2>/dev/null)
	if [ -n "$last" ] && [ $((now - last)) -lt "${DSCP_CLASSIFY_MIN_INTERVAL:-300}" ]; then
		echo 1
		return
	fi
	echo "$now" > "$dscp_classify_ts"

	for class in cs0 cs1 cs2 cs3 cs4 cs5 cs6 cs7 ef; do
		_get_dscp_classify_entries "$class"
	done
	desired="[${dscp_classify_entries}]"
	if [ "$(cat "$dscp_classify_cache" 2>/dev/null)" = "$desired" ]; then
		echo 1
		return
	fi
	result=$(_set_json "dscp_classify" "{\"entries\":${desired}}")
	[ "$(_parse_result "$result")" = "done" ] && echo "$desired" > "$dscp_classify_cache"
	[ -n "$result" ] && echo "$result"
}

# Standalone entry point (see EXTRA_COMMANDS), same bootstrap pattern as
# set_mptcp_dscp_vps/set_mptcp_weight_vps.
set_dscp_classify_vps() {
	local dcservername=$1
	if [ -z "$dcservername" ]; then
		config_load openmptcprouter
		config_foreach set_dscp_classify_vps server
		return
	fi
	servername=$dcservername
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token=""
	_set_dscp_classify_vps
}

_set_vpn_vps() {
	local settings
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	vpn_current="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.current')"
	vpn="$(uci -q get openmptcprouter.settings.vpn)"
	if [ "$vpn_current" != "$vpn" ]; then
		settings='{"vpn" : "'$vpn'"}'
		echo $(_set_json "vpn" "$settings")
	else
		echo 1
	fi
}

_set_proxy_vps() {
	local settings
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	proxy_current="$(echo "$vps_config" | jsonfilter -q -e '@.proxy.current')"
	proxy="$(uci -q get openmptcprouter.settings.proxy)"
	if [ "$proxy_current" != "$proxy" ]; then
		settings='{"proxy" : "'$proxy'"}'
		echo $(_set_json "proxy" "$settings")
	else
		echo 1
	fi
}

_get_local_wan_ip() {
	local wanip
	# publicip is the IPv4 one again since #4375, so the IPv6 address has to
	# be asked for by name: shadowsocks' local.acl takes either family, and
	# an IPv6 only WAN would otherwise contribute nothing at all
	for wanip in "$(uci -q -P /var/state get openmptcprouter.$1.publicip)" "$(uci -q -P /var/state get openmptcprouter.$1.publicip6)"; do
		[ -n "$wanip" ] && {
			[ -z "$wanips" ] && wanips=$wanip || wanips="$wanips"'\n'"$wanip"
		}
	done
}

# _set_json_once <route> <json>: for the settings /config doesn't give back
# (the WAN IPs, SIP ALG), POSTed on every sync before. Sent again only when
# they, or the server address, differ from the last ones the server answered
# (applied, or refused for good: read only user, not installed). /tmp: once
# more after a reboot.
_set_json_once() {
	local marker="/tmp/.omr-vps-sent-${servername}-$1" result
	# Resent at least hourly: a VPS reinstalled at the same address must
	# get the values again without waiting for a change or a reboot
	[ -n "$(find "$marker" -mmin -60 2>/dev/null)" ] && [ "$(cat "$marker")" = "${server}|$2" ] && return 0
	result=$(_set_json "$1" "$2")
	case "$(_parse_result "$result")" in
		done|warning|permission) printf '%s' "${server}|$2" > "$marker" ;;
	esac
}

_set_wan_ip() {
	local settings
	if [ -n "$wanips" ] && [ "$(echo "$vps_config" | jsonfilter -q -e '@.wan.ips')" != "$wanips" ]; then
		settings='{"ips" : "'$wanips'"}'
		_set_json_once "wan" "$settings"
	fi
}

_get_lan_ip() {
	local intf=$1 addr mask lanip
	# a whole word: grep took lan for lan2
	case " $(uci -q get firewall.zone_lan.network) " in
		*" $intf "*) ;;
		*) return ;;
	esac
	# ipaddr can be a list, and an address can carry its prefix with no
	# netmask option: 192.168.1.1/24 was sent as "192.168.1.1/24/"
	mask="$(uci -q get network.${intf}.netmask)"
	for addr in $(uci -q get network.${intf}.ipaddr); do
		case "$addr" in
			*/*) lanip="$addr" ;;
			*) lanip="${addr}${mask:+/$mask}" ;;
		esac
		if [ -z "$lanips" ]; then
			lanips='"'${lanip}'"'
		else
			# comma-separated so the /lan JSON array stays valid with
			# more than one lan-zone interface, and so the comparison in
			# _set_lan_ip matches jsonfilter's '"a", "b"' rendering of
			# the stored list (issue 4361: the old '" "' concatenation
			# made every POST /lan fail with 422 on multi-LAN setups)
			lanips=$lanips', "'${lanip}'"'
		fi
	done
}

_set_lan_ip() {
	local settings
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	lanip_current="$(echo "$vps_config" | jsonfilter -q -e '@.lan.ips' | sed -e 's:\\::g' -e 's:\[::g' -e 's:\]::g' -e 's/^[[:blank:]]*//;s/[[:blank:]]*$//')"
	if [ "$lanips" != "" ] && [ "$lanip_current" != "$lanips" ]; then
	#if [ "$lanips" != "" ]; then
		settings='{"lanips" : ['$lanips']}'
		result=$(_set_json "lan" "$settings")
	fi
}

# The elements of one of omr-bypass's fw4 sets, quoted and comma-separated.
# From nft's text output: the set is an interval set, so an element can be
# an address, a prefix or a range (the JSON output has a different form for
# each), and it may carry a timeout.
_bypass_set_elements() {
	nft list set inet fw4 "$1" 2>/dev/null | awk '
		/elements = \{/ { e = 1; sub(/.*elements = \{/, "") }
		e {
			done = sub(/\}.*/, "")
			n = split($0, parts, ",")
			for (i = 1; i <= n; i++) {
				split(parts[i], w, " ")
				if (w[1] != "") printf "%s\"%s\"", (c++ ? "," : ""), w[1]
			}
			if (done) exit
		}'
}

_set_bypass_ips() {
	local settings result bypass_sent
	bypassipv4s=$(_bypass_set_elements omr_dst_bypass_srv_vpn1_4)
	bypassipv6s=$(_bypass_set_elements omr_dst_bypass_srv_vpn1_6)
	# An empty list is sent only to a VPS that was given one before (the last
	# address was removed), or it would keep bypassing it. One that never had
	# one isn't sent anything.
	bypass_sent="$(uci -q get openmptcprouter.${servername}.bypass_sent)"
	[ -z "$bypassipv4s$bypassipv6s" ] && [ "$bypass_sent" != "1" ] && return
	settings='{"ipv4s" : ['$bypassipv4s'],"ipv6s" : ['$bypassipv6s'],"intf" : "vpn1"}'
	result=$(_set_json "bypass" "$settings")
	# No answer with the stored token: its address may be the one down, a
	# login goes through the others
	[ -z "$result" ] && [ "$bypass_stored_token" = "1" ] && \
		result=$(token=""; _set_json "bypass" "$settings")
	[ "$(_parse_result "$result")" = "done" ] || return
	# That this push went through, for omr-schedule's 030-bypass: runtime,
	# in /var/state. bypass_sent is committed on a change only (none from
	# a list to another one): it went to flash on every push.
	uci -q -P /var/state batch <<-EOF >/dev/null
		revert openmptcprouter.${servername}.bypass_pushed
		set openmptcprouter.${servername}.bypass_pushed=1
	EOF
	if [ -n "$bypassipv4s$bypassipv6s" ]; then
		[ "$bypass_sent" = "1" ] && return
		uci -q set openmptcprouter.${servername}.bypass_sent=1
	else
		uci -q delete openmptcprouter.${servername}.bypass_sent
	fi
	uci -q commit openmptcprouter
}

# Each server with its own token: _set_json logs in to $servername's VPS only
# when $token is empty, or when the VPS refuses it. The stored one is used,
# with the server's first address: a login on each push saved a new token,
# a flash write every time the list changed (030-bypass, every 10 minutes).
_set_bypass_ips_server() {
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token="$(uci -q get openmptcprouter.${servername}.token)"
	server="$(uci -q get openmptcprouter.${servername}.ip | awk '{print $1}')"
	serverport="$(uci -q get openmptcprouter.${servername}.port)"
	[ -z "$server" ] && token=""
	bypass_stored_token=""
	[ -n "$token" ] && bypass_stored_token=1
	_set_bypass_ips
	bypass_stored_token=""
}

set_bypass_ips() {
	config_load openmptcprouter
	config_foreach _set_bypass_ips_server server
}

# The tunnel's two ends as the VPS stores them: vpnip_local is the router's
# address on omrvpn (the VPS's remoteip), vpnip_remote the VPS's own (its
# localip). Either is empty while the tunnel is still coming up.
_get_vpn_ips() {
	vpnifname="$(uci -q get network.omrvpn.device)"
	vpnip_local=$(ip -4 -br addr ls dev ${vpnifname} 2>/dev/null | head -n 1 | awk -F'[ /]+' '{print $3}')
	[ -z "$vpnip_local" ]  && vpnip_local=$(ip -4 addr show dev "$OMR_TRACKER_DEVICE" 2>/dev/null | grep -m 1 inet | awk '{print $2}' | cut -d'/' -s -f1)
	vpnip_remote=$(ip -4 r show default dev ${vpnifname} 2>/dev/null | head -n 1 | awk '{print $3}' | tr -d "\n")
	# Only a point-to-point peer: openvpn's subnet topology has a kernel
	# route to the whole tunnel network (10.255.252.0/24), which is no address
	# and which the VPS refuses. Its default route is only added after
	# route_delay, so until then the peer stays unknown and the caller retries.
	[ -z "$vpnip_remote" ] && [ -n "$vpnifname" ] && vpnip_remote=$(ip -4 r list dev ${vpnifname} 2>/dev/null | awk '/ proto kernel / && $1 !~ /\// {print $1; exit}')
	[ -z "$vpnip_remote" ] && [ -n "$vpnifname" ] && vpnip_remote=$(ip -4 r list dev ${vpnifname} 2>/dev/null | grep -m 1 "proto static src" | awk '{print $3}' | tr -d "\n")
	[ -z "$vpnip_remote" ] && vpnip_remote=$(ifstatus omrvpn 2>/dev/null | jsonfilter -q -e '@.route[0].nexthop')
	[ -z "$vpnip_remote" ] && [ "$vpnifname" = "bonding-omrvpn" ] && vpnip_remote="10.255.248.1"
}

# Succeeds only once the VPS holds the tunnel's current addresses: they
# already matched, or POST /vpnips applied them. Fails when the config
# couldn't be fetched, the tunnel has no address or peer yet, or the POST
# was refused or got no answer, so that the wizard's poll tries again.
_set_vpn_ip() {
	local settings
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return 1
	[ "$(uci -q get openmptcprouter.settings.vpn)" = "none" ] && return 0
	_get_vpn_ips
	[ -n "$vpnip_local" ] && [ -n "$vpnip_remote" ] || return 1
	vpnip_local_current="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.remoteip')"
	vpnip_remote_current="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.localip')"
	ula="$(uci -q get network.globals.ula_prefix)"
	ula_current="$(echo "$vps_config" | jsonfilter -q -e '@.ip6in4.ula')"
	[ "$vpnip_remote" = "$vpnip_remote_current" ] && [ "$vpnip_local" = "$vpnip_local_current" ] && [ "$ula" = "$ula_current" ] && return 0
	settings='{"remoteip" : "'$vpnip_local'","localip" : "'$vpnip_remote'","ula" : "'$ula'"}'
	result=$(_set_json "vpnips" "$settings")
	[ "$(_parse_result "$result")" = "done" ] && return 0
	# The VPS answers this error when it already has all three values. The
	# /config of an older VPS reports no ULA for any user but userid 0, so
	# for the others the ULA always looks changed and every run POSTs: in
	# sync all the same.
	[ "$(echo "$result" | jsonfilter -q -e '@.reason')" = "Invalid parameters" ]
}

# GET /config from ${servername} with its stored token, for the entry points
# called in bursts: a POST /token per call is a login storm, and each login
# rewrites the token in uci. _login, skipped then, is what sets server and
# serverport, so try the token on each address in turn; when none answers
# (all unreachable) or there is no token, fall back to a real login.
#
# Both have to leave server on the address that answered: _set_json, which
# follows, has the token and so skips _login, and POSTs to $server. The login
# used to run inside $(_get_json ...): it found a working address in the
# subshell, only the token came back (through uci), and the POST went to the
# first, dead, address. It timed out, and every omrvpn hotplug and 30s wizard
# poll did it all again, with a fresh POST /token each time.
_get_config_stored_token() {
	local stored
	token="$(uci -q get openmptcprouter.${servername}.token)"
	serverport="$(uci -q get openmptcprouter.${servername}.port)"
	server=""
	vps_config=""
	stored="$token"
	_get_config_on_server() {
		[ -n "$vps_config" ] && return
		# A refused token was renewed in the subshell (a POST /token):
		# that address answered, don't do it once more per address
		[ "$(uci -q get openmptcprouter.${servername}.token)" = "$stored" ] || return
		server="$1"
		vps_config=$(_get_json "config")
	}
	if [ -n "$token" ]; then
		config_load openmptcprouter
		config_list_foreach "${servername}" ip _get_config_on_server
	fi
	# _get_json ran in a subshell: a login it did (refused token) only
	# reached uci, and the next _set_json would log in again
	token="$(uci -q get openmptcprouter.${servername}.token)"
	if [ -z "$vps_config" ] && [ "$token" = "$stored" ]; then
		# Here, not in a subshell: _login leaves server on the address
		# that answered
		token=""
		_login
		[ -z "$token" ] || vps_config=$(_get_json "config")
	fi
}

# rc.common hands an EXTRA_COMMAND only what follows its name, so nothing set
# servername here: _login looked up openmptcprouter..username, logged "No
# credentials for server  yet" and gave up, and the VPN IPs the omrvpn hotplug
# and the wizard's 30s poll push never reached the VPS. Do it per server, as
# _config_service does.
_set_vpn_ip_server() {
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	_get_config_stored_token
	_set_vpn_ip || vpn_ip_failed=1
}

# Fails until every server holds the tunnel's current addresses, so that the
# wizard's poll stops at the first full sync and retries a run that couldn't
# reach a server. While the tunnel has no address or peer yet, no server is
# asked anything: the poll would fetch each one's config every 2 s for
# nothing.
#
# Runs one at a time. The omrvpn hotplug starts it in the background (an
# ifup and an ifupdate can come back to back) while the poll runs it too:
# two runs at once raced on the stored token, and the one that read the
# older address could POST it last. A run that waited reads the addresses
# and the VPS config again, so it only sends what is still missing.
set_vpn_ip() {
	vpn_ip_failed=0
	[ "$(uci -q get openmptcprouter.settings.vpn)" = "none" ] && return 0
	# a failed exec redirection ends a POSIX shell: check first
	if command -v flock >/dev/null 2>&1 && [ -d /var/lock ] && [ -w /var/lock ]; then
		exec 9>/var/lock/omr-set-vpn-ip.lock
		flock 9
	fi
	_get_vpn_ips
	[ -n "$vpnip_local" ] && [ -n "$vpnip_remote" ] || return 1
	config_load openmptcprouter
	config_foreach _set_vpn_ip_server server
	# omr-tracker's 003-up sends it again until every server has it
	if [ "$vpn_ip_failed" = "0" ]; then
		uci -q -P /var/state get openmptcprouter.omr >/dev/null || uci -q -P /var/state set openmptcprouter.omr=router
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.omr.vpnip_sent
			set openmptcprouter.omr.vpnip_sent=$vpnip_local
		EOF
	fi
	return $vpn_ip_failed
}


_set_sipalg() {
	local settings
	sipalg="$(uci -q get openmptcprouter.settings.sipalg)"
	if [ "$sipalg" = "1" ]; then
		sipalg="true"
	else
		sipalg="false"
	fi
	settings='{"enable" : "'$sipalg'"}'
	_set_json_once "sipalg" "$settings"
}


# Removes the routes numbered past $c2cid, LANs the VPS no longer lists. All
# deleted then recreated, unchanged routes still made a change: network was
# committed on every sync.
_delete_client2client() {
	case "$1" in
		omr_client2client_*)
			[ "${1#omr_client2client_}" -le "$c2cid" ] 2>/dev/null || uci -q delete network.$1
			;;
	esac
}

# <enabled>: one route per LAN of another user of the VPS (omradmin lists one
# per user), none when client2client is off
_set_client2client() {
	local c2cid=0 c2cips=""
	[ "$1" = "1" ] && c2cips="$(echo "$vps_config" | jsonfilter -q -e '@.client2client.lanips[*]')"
	[ -n "$c2cips" ] && {
		vpnifname="$(uci -q get network.omrvpn.device)"
		vpnip_local=$(ip -4 -br addr ls dev ${vpnifname} | awk -F'[ /]+' '{print $3}' | tr -d "\n")
		vpnip_remote=$(ip -4 r list dev ${vpnifname} | grep via | grep -v default | grep -v / | grep -v metric | awk '{print $1}' | tr -d "\n")
	}
	for lanip in $c2cips; do
		c2cid=$((c2cid+1))
		targetip=$(echo $lanip | awk -F '/' '{print $1}' | tr -d "\n")
		netmask=$(echo $lanip | awk -F '/' '{print $2}' | tr -d "\n")
		target=$(ipcalc.sh $targetip/$netmask | grep NETWORK | awk -F '=' '{print $2}' | tr -d "\n")
		uci -q batch <<-EOF >/dev/null
			set network.omr_client2client_${c2cid}=route
			set network.omr_client2client_${c2cid}.interface=omrvpn
			set network.omr_client2client_${c2cid}.target="${target}"
			set network.omr_client2client_${c2cid}.netmask="${netmask}"
			set network.omr_client2client_${c2cid}.gateway="${vpnip_remote}"
		EOF
	done
	config_load network
	config_foreach _delete_client2client route
	[ -n "$(uci -q changes network)" ] && uci -q commit network
}

# A forward to a public IP of the VPS (src_dip) reaches the router through
# the tunnel, to its tunnel address: fw4's "ip daddr <src_dip>" in dstnat_vpn
# never matches it, so it is dropped there. Only there: dstnat_lan holds the
# reflection rule of the same forward (vpn zone masq), and the first rule of
# the table with that address was rewritten whatever its chain, the
# reflection rule first. Without its address, every connection from the LAN
# to that port, to any destination, went to the forward's target.
_fw4_vpn_dnat_any_daddr() {
	local rule handle
	nft -a list chain inet fw4 dstnat_vpn 2>/dev/null | grep -F "ip daddr $1 " | grep -vF '(reflection)' |
	while IFS= read -r rule; do
		handle=$(echo $rule | awk '{ print $NF }')
		nft replace rule inet fw4 dstnat_vpn handle $handle $(echo $rule | awk '{ sub(" comment.*$",""); print }' | sed "s/ip daddr $1 /meta nfproto ipv4 /") >/dev/null 2>&1
	done
}

_vps_firewall_redirect_port() {
	local src proto src_dport section sectype family_override
	section=$1
	family_override=$2
	# The rules for what reaches the router itself through the tunnel (the
	# VXLAN encapsulation, DHCP) are not forwards: opened on the VPS, they
	# accept the port from the whole internet, the VXLAN one into the vpn
	# zone and its L2 bridge (Allow-VXLAN was). By section name, which LuCI
	# doesn't change (an option of our own would be an "unknown option"
	# warning of fw4's on every reload). Not listed as kept, an opening an
	# older release made for them is closed with the other stale ones.
	case "$section" in
		omrvxlan|allow_dhcp_request_vpn) return ;;
	esac
	config_get src $1 src
	config_get proto $1 proto "tcp udp"
	config_get src_dport $1 src_dport
	config_get family $1 family "ipv4"
	# fw4 lets one forward cover both families: "Restrict to address family =
	# IPv4 and IPv6" writes family='any'. The VPS API takes ONE family per call
	# and its ipproto is a strict ipv4/ipv6 enum, so passing 'any' straight
	# through made every POST /shorewallopen fail 422 and the forward was
	# silently never created on the server -- while the checks below read
	# anything that is not "ipv4" as IPv6, so it also consulted the wrong
	# "already open?" list (issue #4369). Handle both families by re-entering
	# once per family; anything unrecognised is normalised to ipv4 rather than
	# sent to the API verbatim.
	[ -n "$family_override" ] && family="$family_override"
	case "$family" in
		any|all|\*)
			_vps_firewall_redirect_port "$section" ipv4
			_vps_firewall_redirect_port "$section" ipv6
			return
			;;
		ipv6) ;;
		*) family="ipv4" ;;
	esac
	config_get enabled $1 enabled "1"
	config_get src_dip $1 src_dip
	config_get dest_ip $1 dest_ip
	config_get dest_port $1 dest_port
	config_get src_ip $1 src_ip
	config_get v2ray $1 v2ray "0"
	config_get name $1 name
	config_get dmz $1 dmz "0"
	config_get sectype $1 TYPE
	config_get target $1 target ""
	if [ -z "$target" ]; then
		# Mirror fw4: a redirect without an explicit target is a DNAT, while
		# a rule without target has no verdict at all (fw4 renders a bare
		# counter, issue #4365) -- nothing to open on the VPS, and pushing it
		# as fwtype "REDIRECT" every pass only spammed the API with entries
		# the server ignores.
		[ "$sectype" = "rule" ] && return
		target="DNAT"
	fi
	# The VPS acts on ACCEPT and DNAT only and ignores any other fwtype: the
	# stock ftphelpervpn rule (target HELPER) was POSTed to /shorewallopen on
	# every pass, never listed back, so never seen as already open. The
	# v2ray/xray branch below even sends it as an ACCEPT.
	case "$target" in
		ACCEPT|DNAT) ;;
		*) return ;;
	esac
	if [ -z "$src_dport" ] && [ -n "$dest_port" ]; then
		src_dport=$dest_port
	fi
	if [ "$dmz" = "1" ] && [ "$src_dport" != "2-64999" ]; then
		uci -q batch <<-EOF >/dev/null
			set firewall.${section}.src_dport='2-64999'
			commit firewall
		EOF
		src_dport='2-64999'
	fi
	[ -n "$src_dport" ] && src_dport=$(echo $src_dport | sed 's/:/-/')
	if [ -n "$src_dport" ] && [ "$(echo $src_dport | cut -d'-' -f2)" -ge "65000" ]; then
		logger -t "OMR-VPS" "You can't redirect ports >= 65000, they are needed by OpenMPTCProuter Server part"
		enabled="0"
		#uci -q delete firewall.$1
		#return
	fi
	xray="$v2ray"
	([ "$(uci -q get v2ray.main.enabled)" != "1" ] || [ "$(uci -q -P /var/state get openmptcprouter.omr.v2ray)" = "down" ]) && v2ray="0"
	([ "$(uci -q get xray.main.enabled)" != "1" ] || [ "$(uci -q -P /var/state get openmptcprouter.omr.xray)" = "down" ]) && xray="0"
	[ "$proto" = "all" ] && proto="tcp udp"
	[ "$proto" = "" ] && proto="tcp udp"
	# Only forwards whose source zone is the VPN zone are pushed to the VPS
	# below. That is correct -- inbound connections arrive through the tunnel
	# -- but LuCI's own default source zone for a new forward is "wan", so a
	# user who takes the default gets a forward that is silently never opened
	# on the server: its user_dnat chain stays empty and nothing arrives
	# (issue #4369). Say so once per section per boot instead of skipping
	# mutely; /tmp is tmpfs, so the marker clears on reboot.
	[ "$target" = "DNAT" ] && [ -n "$src_dport" ] && [ "$enabled" != "0" ] && [ "$src" != "vpn" ] && {
		[ -f "/tmp/.omr-fwsrc-warned-$section" ] || {
			logger -t "OMR-VPS" "Port forward '${name:-$section}' source zone is '${src:-unset}', not 'vpn': it is NOT opened on the VPS (only vpn-zone forwards are)"
			touch "/tmp/.omr-fwsrc-warned-$section"
		}
	}
	[ "$src" = "vpn" ] && [ -n "$proto" ] && [ -n "$src_dport" ] && [ "$enabled" != "0" ] && [ "$name" != "Allow-DHCP-Request-VPN" ] && {
		for protoi in $proto; do
			if [ "$v2ray" = "0" ] && [ "$xray" = "0" ]; then
				checkfw=""
				if [ "$family" = "ipv4" ]; then
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						# ACCEPT rules are listed by the VPS as "open router" lines;
						# grepping for a "redirect router" line here (as before) never
						# matched, so every pass re-issued shorewallopen for them.
						if [ "$target" = "ACCEPT" ]; then
							checkfw=$(echo "$vpsfwlist" | grep "$src_dport	# OMR $username open router $src_dport port $protoi")
						else
							checkfw=$(echo "$vpsfwlist" | grep "$src_dport	# OMR $username redirect router $src_dport port $protoi")
						fi
					else
						comment=""
						[ -n "$src_dip" ] && {
							comment=" to $src_dip"
							if [ ! -f /usr/sbin/nft ]; then
								$IPTABLESSAVE --counters | sed "s:-d $src_dip/32::g" | $IPTABLESRESTORE -w
							else
								_fw4_vpn_dnat_any_daddr "$src_dip"
							fi
						}
						[ -n "$src_ip" ] && comment=" from $src_ip"
						if [ "$target" = "ACCEPT" ]; then
							checkfw=$(echo "$vpsfwlist" | grep "# OMR $username open router $src_dport port ${protoi}${comment}")
						else
							checkfw=$(echo "$vpsfwlist" | grep "# OMR $username redirect router $src_dport port ${protoi}${comment}")
						fi
					fi
				else
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						if [ "$target" = "ACCEPT" ]; then
							checkfw=$(echo "$vpsfw6list" | grep "$src_dport	# OMR $username open router $src_dport port $protoi")
						else
							checkfw=$(echo "$vpsfw6list" | grep "$src_dport	# OMR $username redirect router $src_dport port $protoi")
						fi
					else
						comment=""
						[ -n "$src_dip" ] && comment=" to $src_dip"
						[ -n "$src_ip" ] && comment=" from $src_ip"
						if [ "$target" = "ACCEPT" ]; then
							checkfw=$(echo "$vpsfw6list" | grep "# OMR $username open router $src_dport port ${protoi}${comment}")
						else
							checkfw=$(echo "$vpsfw6list" | grep "# OMR $username redirect router $src_dport port ${protoi}${comment}")
						fi
					fi
				fi
				if [ "$checkfw" = "" ]; then
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","source_dip" : "'$src_dip'","source_ip" : "'$src_ip'","proto" : "'$protoi'","fwtype" : "'$target'","ipproto" : "'$family'"}'
					result=$(_set_json "shorewallopen" "$settings")
				fi
				if [ "$family" = "ipv4" ]; then
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						if [ "$target" = "ACCEPT" ]; then
							vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR $username open router $src_dport port $protoi$")
							[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR open router $src_dport port $protoi$")
						else
							vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR $username redirect router $src_dport port $protoi$")
							[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR redirect router $src_dport port $protoi$")
						fi
					else
						comment=""
						[ -n "$src_dip" ] && comment=" to $src_dip"
						[ -n "$src_ip" ] && comment=" from $src_ip"
						if [ "$target" = "ACCEPT" ]; then
							vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment}$")
							[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR open router $src_dport port ${protoi}${comment}$")
						else
							vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR $username redirect router $src_dport port ${protoi}${comment}")
							[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR redirect router $src_dport port ${protoi}${comment}$")
						fi
					fi
				else
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						if [ "$target" = "ACCEPT" ]; then
							vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR $username open router $src_dport port $protoi$")
							[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR open router $src_dport port $protoi$")
						else
							vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR $username redirect router $src_dport port $protoi$")
							[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR redirect router $src_dport port $protoi$")
						fi
					else
						[ -n "$src_dip" ] && comment=" to $src_dip"
						[ -n "$src_ip" ] && comment=" from $src_ip"
						if [ "$target" = "ACCEPT" ]; then
							vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment}$")
							[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR open router $src_dport port ${protoi}${comment}$")
						else
							vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR $username redirect router $src_dport port ${protoi}${comment}$")
							[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR redirect router $src_dport port ${protoi}${comment}$")
						fi
					fi
				fi
			else
				# Always (re)issue shorewallopen + v2rayredirect/xrayredirect for
				# the active family instead of gating on a "does a shorewall
				# line already mention this family" text match: v2ray_add_port/
				# xray_add_port on the VPS are already idempotent (they key off
				# the inbound tag and no-op if it exists), and the previous
				# text-match gate went stale as soon as a proxy-family switch
				# left old comment text behind (see the resulting nft errors
				# below this loop and the cross-family unredirect a few lines
				# down) -- a stale match then wrongly skipped re-adding the
				# redirect for the now-active family, silently breaking the
				# forward instead of healing it.
				if [ "$family" = "ipv4" ] && [ -n "$src_dip" ]; then
					if [ ! -f /usr/sbin/nft ]; then
						$IPTABLESSAVE --counters | sed "s:-d $src_dip/32::g" | $IPTABLESRESTORE -w
					else
						_fw4_vpn_dnat_any_daddr "$src_dip"
					fi
				fi
				[ "$v2ray" = "1" ] && settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","source_dip" : "'$src_dip'","source_ip" : "'$src_ip'","proto" : "'${protoi}'","fwtype" : "ACCEPT","ipproto" : "'$family'","comment" : "V2Ray to '${dest_ip}':'${dest_port}'"}'
				[ "$xray" = "1" ] && settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","source_dip" : "'$src_dip'","source_ip" : "'$src_ip'","proto" : "'${protoi}'","fwtype" : "ACCEPT","ipproto" : "'$family'","comment" : "XRay to '${dest_ip}':'${dest_port}'"}'
				result=$(_set_json "shorewallopen" "$settings")
				[ -z "$dest_port" ] && dest_port="$src_dport"
				settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","destip" : "'$dest_ip'","destport" : "'$dest_port'","proto" : "'${protoi}'"}'
				[ "$v2ray" = "1" ] && result=$(_set_json "v2rayredirect" "$settings")
				[ "$xray" = "1" ] && result=$(_set_json "xrayredirect" "$settings")
				# A forwarded port can only be safely bound to one proxy
				# family's dokodemo-door inbound at a time: the VPS runs
				# v2ray and xray simultaneously, so a stale redirect left on
				# the other family after a proxy switch competes for the same
				# port (SO_REUSEPORT) and randomly blackholes connections
				# that land on the family with no live reverse bridge.
				# Explicitly tear down the other family's redirect for this
				# exact port+destination every pass, not just when (re)adding
				# our own -- otherwise a stale entry left from before this
				# script gained cross-family cleanup (or from a run that hit
				# this same race) never gets closed.
				settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","destip" : "'$dest_ip'","destport" : "'$dest_port'","proto" : "'${protoi}'"}'
				[ "$v2ray" = "1" ] && result=$(_set_json "xrayunredirect" "$settings")
				[ "$xray" = "1" ] && result=$(_set_json "v2rayunredirect" "$settings")
				# Strip both the V2Ray- and XRay-tagged variant of this rule from
				# the stale-entry list unconditionally (not just the currently
				# active family's). A proxy-family switch rewrites the shorewall
				# comment to the new family without going through this cleanup
				# for the old one, so the old family's line otherwise survives
				# into _vps_firewall_close_port below. Since v2ray_del_port/
				# xray_del_port on the VPS key their delete purely off
				# proto+port+destip:destport (not family), that stale close call
				# then deletes the redirect this same pass just (re)created —
				# the port-forward would appear to "randomly" stop working after
				# switching proxy family on the same forwarded port (issue #4315
				# follow-up).
				if [ "$family" = "ipv4" ]; then
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR $username open router $src_dport port ${protoi} --- V2Ray to ${dest_ip}:${dest_port}$")
						vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR $username open router $src_dport port ${protoi} --- XRay to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR open router $src_dport port ${protoi} --- V2Ray to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "$src_dport	# OMR open router $src_dport port ${protoi} --- XRay to ${dest_ip}:${dest_port}$")
					else
						comment=""
						[ -n "$src_dip" ] && comment=" to $src_dip"
						[ -n "$src_ip" ] && comment=" from $src_ip"
						vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment} --- V2Ray to ${dest_ip}:${dest_port}$")
						vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment} --- XRay to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR open router $src_dport port ${protoi}${comment} --- V2Ray to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfwlist=$(echo "$vpsfwlist" | grep -v "# OMR open router $src_dport port ${protoi}${comment} --- XRay to ${dest_ip}:${dest_port}$")
					fi
				else
					if [ "$src_dip" = "" ] && [ "$src_ip" = "" ]; then
						vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR $username open router $src_dport port ${protoi} --- V2Ray to ${dest_ip}:${dest_port}$")
						vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR $username open router $src_dport port ${protoi} --- XRay to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR open router $src_dport port ${protoi} --- V2Ray to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "$src_dport	# OMR open router $src_dport port ${protoi} --- XRay to ${dest_ip}:${dest_port}$")
					else
						[ -n "$src_dip" ] && comment=" to $src_dip"
						[ -n "$src_ip" ] && comment=" from $src_ip"
						vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment} --- V2Ray to ${dest_ip}:${dest_port}$")
						vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR $username open router $src_dport port ${protoi}${comment} --- XRay to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR open router $src_dport port ${protoi}${comment} --- V2Ray to ${dest_ip}:${dest_port}$")
						[ "$username" = "openmptcprouter" ] && vpsfw6list=$(echo "$vpsfw6list" | grep -v "# OMR open router $src_dport port ${protoi}${comment} --- XRay to ${dest_ip}:${dest_port}$")
					fi
				fi
			fi
		done
	}
}

_vps_firewall_close_port() {
	[ -n "$vpsfwlist" ] && {
		echo "$vpsfwlist" | while read -r line; do
			[ -n "$line" ] && {
				type=$(echo $line | awk '{print $1}' | tr -d "\n")
				proto=$(echo $line | awk '{print $4}' | tr -d "\n")
				src_dport=$(echo $line | awk '{print $5}' | tr -d "\n")
				source_port=$(echo $line | awk '{print $6}' | tr -d "\n")
				source_dip=$(echo $line | awk '{print $7}' | tr -d "\n")
				comment=$(echo $line | awk -F' --- ' '{print $2}' | tr -d "\n")
				[ "$source_dip" = "#" ] && source_dip=""
				source_ip=$(echo $line | awk '{print $2}' | awk -F ":" '{print $2}' | tr -d "\n")
				if [ "$source_port" = "-" ]; then
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","source_dip": "'$source_dip'","source_ip": "'$source_ip'","comment" : "'$comment'"}'
				elif [ "$source_ip" != "" ]; then
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","source_ip": "'$source_ip'","comment" : "'$comment'"}'
				else
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","comment" : "'$comment'"}'
				fi
				result=$(_set_json "shorewallclose" "$settings")
				[ "$type" = "ACCEPT" ] && {
					destip=$(echo $comment | awk '{print $3}' | awk -F: '{print $1}')
					destport=$(echo $comment | awk '{print $3}' | awk -F: '{print $2}')
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","destip" : "'$destip'","destport": "'$destport'"}'
					result=$(_set_json "v2rayunredirect" "$settings")
					result=$(_set_json "xrayunredirect" "$settings")
				}
			}
		done
	}
	[ -n "$vpsfw6list" ] && {
		echo "$vpsfw6list" | while read -r line; do
			[ -n "$line" ] && {
				type=$(echo $line | awk '{print $1}' | tr -d "\n")
				proto=$(echo $line | awk '{print $4}' | tr -d "\n")
				src_dport=$(echo $line | awk '{print $5}' | tr -d "\n")
				source_port=$(echo $line | awk '{print $6}' | tr -d "\n")
				source_dip=$(echo $line | awk '{print $7}' | tr -d "\n")
				comment=$(echo $line | awk -F' --- ' '{print $2}' | tr -d "\n")
				[ "$source_dip" = "#" ] && source_dip=""
				source_ip=$(echo $line | awk '{print $2}' | awk -F ":" '{print $2}' | tr -d "\n")
				if [ "$source_port" = "-" ]; then
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","ipproto" : "ipv6","source_dip": "'$source_dip'","source_ip": "'$source_ip'","comment" : "'$comment'"}'
				elif [ "$source_ip" != "" ]; then
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","ipproto" : "ipv6","source_dip": "'$source_dip'","source_ip": "'$source_ip'","comment" : "'$comment'"}'
				else
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","fwtype" : "DNAT","ipproto" : "ipv6","comment" : "'$comment'"}'
				fi
				result=$(_set_json "shorewallclose" "$settings")
				[ "$type" = "ACCEPT" ] && {
					settings='{"name" : "router '$src_dport'","port" : "'$src_dport'","proto" : "'$proto'","comment" : "'$comment'"}'
					result=$(_set_json "v2rayunredirect" "$settings")
					result=$(_set_json "xrayunredirect" "$settings")
				}
			}
		done
	}
}

_set_vps_firewall() {
	# iptables (fw3) only: on fw4, fw3 is fw4 and printed the whole nftables
	# ruleset for nothing, _vps_firewall_redirect_port handles it there
	[ -e /sbin/fw4 ] || fw3 -q print 2>/dev/null | grep 'vpn.* -d' |
	while IFS= read -r c; do
		eval $(echo $c | sed 's/iptables/iptables -w/' | sed 's/-A/-D/') 2>&1 >/dev/null
		publicip=$(echo $c | awk -F'-d' '{print $2}' | cut -d '/' -f1 | sed 's/ //g')
		[ -n "$publicip" ] && {
			greintf=$(uci show network | grep "Tunnel for $publicip" | cut -d '.' -f2 | tr -d "\n")
			[ -n "$greintf" ] && greip=$(uci -q get network.${greintf}.ipaddr)
		}
		if [ -z "$greip" ]; then
			newrule=$(echo $c | sed 's/iptables/iptables -w/' | sed -E -e 's/ -d ([^ ])*//' -e 's/ -s ([^ ])*//')
		else
			newrule=$(echo $c | sed 's/iptables/iptables -w/' | sed -E -e "s/ -d ([^ ])*/ -d ${greip}\/255.255.255.252/" -e 's/ -s ([^ ])*//')
		fi
		eval $(echo $newrule | sed 's/-A/-D/') || true
		eval $newrule
	done
	#'

	fwservername=$1
	[ -n "$fwservername" ] && servername=$fwservername
	[ -z "$fwservername" ] && fwservername=$servername
	# Nothing to push, but the flag the firewall reload raised has to go:
	# omr-schedule asked for this push, ran blocklanfw and logged every
	# minute otherwise
	if [ "$(uci -q get openmptcprouter.${fwservername}.disabled)" = "1" ] || \
	   [ "$(uci -q get openmptcprouter.${fwservername}.nofwredirect)" = "1" ]; then
		_vps_firewall_pushed "$fwservername"
		return
	fi
	[ -z "$(uci -q get openmptcprouter.${fwservername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${fwservername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${fwservername}.port)" ] && return
	# Called by _config_service (no argument), the token and the config of
	# the server it syncs are kept: emptied, its next calls logged in and
	# fetched /config again, each of them
	[ -n "$1" ] && {
		token=""
		vps_config=""
	}
	_login
	[ -z "$token" ] && {
		_token_failed "$fwservername" "Can't get ${fwservername} token, try later"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${fwservername}.admin_error
			set openmptcprouter.${fwservername}.admin_error=1
		EOF
		return
	}
	username="$(uci -q get openmptcprouter.${fwservername}.username)"
	settings='{"name" : "redirect router"}'
	fw_list=$(_set_json "shorewalllist" "$settings")
	[ -z "$fw_list" ] && return
	vpsfwlist=$(echo $fw_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')
	settings='{"name" : "open router"}'
	fw_list=$(_set_json "shorewalllist" "$settings")
	[ -z "$fw_list" ] && return
	if [ -n "$vpsfwlist" ]; then
		vpsfwlist="${vpsfwlist}"$'\n'"$(echo $fw_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')"
	else
		vpsfwlist="$(echo $fw_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')"
	fi
	settings='{"name" : "redirect router","ipproto" : "ipv6"}'
	fw6_list=$(_set_json "shorewalllist" "$settings")
	[ -z "$fw6_list" ] && return
	vpsfw6list=$(echo $fw6_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')
	settings='{"name" : "open router","ipproto" : "ipv6"}'
	fw6_list=$(_set_json "shorewalllist" "$settings")
	[ -z "$fw6_list" ] && return
	if [ -n "$vpsfw6list" ]; then
		# "\n" is a backslash and an n here: the last "redirect" rule and the
		# first "open" one made one line no rule could match
		vpsfw6list="${vpsfw6list}"$'\n'"$(echo $fw6_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')"
	else
		vpsfw6list="$(echo $fw6_list | jsonfilter -q -e '@.list[*]' | sed '/^[[:space:]]*$/d')"
	fi
	config_load firewall
	config_foreach _vps_firewall_redirect_port redirect
	config_foreach _vps_firewall_redirect_port rule
	[ -n "$vpsfwlist" ] || [ -n "$vpsfw6list" ] && {
		logger -t "OMR-VPS" "Remove old firewall rules"
		_vps_firewall_close_port
	}
	_vps_firewall_pushed "$fwservername"
}

_vps_firewall_pushed() {
	uci -q -P /var/state batch <<-EOF >/dev/null
		revert openmptcprouter.${1}.set_firewall
		set openmptcprouter.${1}.set_firewall=0
	EOF
}

set_vps_firewall() {
	# rc.common passes the server as $1: reading $2 refreshed every server
	# each time a single one was asked for
	local server_opt=$1
	if [ -n "$server_opt" ]; then
		_set_vps_firewall $server_opt
	else
		config_load openmptcprouter
		config_foreach _set_vps_firewall server
	fi
}

_set_ss_redir() {
	local option=$2
	local value=$3
	if [ "$value" = "true" ]; then
		value=1
	elif [ "$value" = "false" ]; then
		value=0
	fi
	uci -q set shadowsocks-libev.$1.$option=$value
}

_set_ss_server() {
	local option=$2
	local value=$3
	if [ "$value" = "true" ]; then
		value=1
	elif [ "$value" = "false" ]; then
		value=0
	fi
	if [ "$(echo $1 | grep omr)" != "" ] || [ "$(echo $1 | grep sss)" ]; then 
		uci -q set shadowsocks-libev.$1.$option=$value
	fi
}

_set_ssrust_redir() {
	local option=$2
	local value=$3
	if [ "$value" = "true" ]; then
		value=1
	elif [ "$value" = "false" ]; then
		value=0
	fi
	uci -q set shadowsocks-rust.$1.$option=$value
}

_set_ssrust_server() {
	local option=$2
	local value=$3
	if [ "$value" = "true" ]; then
		value=1
	elif [ "$value" = "false" ]; then
		value=0
	fi
	# sss1 is the dedicated IPv6 proxy slot (see the wizard's dual-stack
	# setup): its "server" address comes from the wizard's IPv6 entry, not
	# from this v4-preferred $vpsip sync, so leave that one option alone
	# here -- otherwise every VPS-config refresh would clobber it back to
	# the IPv4 address. Other options (password, method, ...) still apply
	# to it uniformly like every other server section.
	if [ "$1" = "sss1" ] && [ "$option" = "server" ]; then
		return
	fi
	if [ "$(echo $1 | grep omr)" != "" ] || [ "$(echo $1 | grep sss)" ]; then
		uci -q set shadowsocks-rust.$1.$option=$value
	fi
}

_set_openvpn_server() {
	local option=$2
	local value=$3
	if [ "$value" = "true" ]; then
		value=1
	elif [ "$value" = "false" ]; then
		value=0
	fi
	if [ "$(echo $1 | grep omr)" != "" ]; then 
		if [ "$option" = "remote" ]; then
			# Rewritten as it was, the list still counted as changed:
			# OpenVPN restarted on every config pull
			[ "$(uci -q get openvpn.$1.$option)" = "$value" ] && return
			uci -q delete openvpn.$1.$option
			uci -q add_list openvpn.$1.$option=$value
		else
			uci -q set openvpn.$1.$option=$value
		fi
	fi
}

# OpenVPN refuses to start a tls_client config that has no CA
_openvpn_client_ready() {
	local section="$1" ca secret
	if [ "$(uci -q get openvpn.${section}.tls_client)" = "1" ]; then
		ca="$(uci -q get openvpn.${section}.ca)"
		[ -n "$ca" ] && [ -s "$ca" ]
		return $?
	fi
	secret="$(uci -q get openvpn.${section}.secret)"
	[ -n "$secret" ] && [ -s "$secret" ]
}

# config_foreach-compatible option remover. $1 = section, $2 = option.
_del_openvpn_option() {
	[ "$(echo $1 | grep omr)" = "" ] && return
	uci -q delete openvpn.$1.$2
}

# config_foreach-compatible "enabled" setter that never turns on a section
# which cannot possibly start. $1 = section, $2 = wanted state.
_set_openvpn_enabled() {
	local section="$1" wanted="$2"
	[ "$(echo $section | grep omr)" = "" ] && return
	if [ "$wanted" = "1" ] && ! _openvpn_client_ready "$section"; then
		[ "$(uci -q get openvpn.${section}.enabled)" = "1" ] && \
			logger -t "OMR-VPS" "OpenVPN ${section}: server sent no usable credentials, keeping it off instead of looping on a start error"
		_set_openvpn_server "$section" "enabled" "0"
		return
	fi
	_set_openvpn_server "$section" "enabled" "$wanted"
}

# config_foreach-compatible: every omr* section but $2 off. omr turned off
# here and back on right after left two deltas for the same value, and the
# "uci changes" test restarted OpenVPN on every config pull with
# openvpn_lb=0, the default on a multi-core router.
_set_openvpn_other_off() {
	[ "$1" = "$2" ] && return
	_set_openvpn_server "$1" "enabled" "0"
}

# Which proxy engine is switched on. omr-tracker picks the proxy watchdog it
# launches (omr-tracker-ss, -v2ray, -xray) from these flags, not from
# openmptcprouter.settings.proxy, so a change here needs it to reconcile even
# when the proxy name stays the same.
_proxy_engines() {
	echo "$(uci -q get shadowsocks-rust.sss0.disabled)/$(uci -q get shadowsocks-libev.sss0.disabled)/$(uci -q get v2ray.main.enabled)/$(uci -q get xray.main.enabled)"
}


_set_config_from_vps() {
	local shadowsocks_disabled vpn glorytun_state redirect shorewall_redirect mlvpn_key openvpn_key dsvpn_key mqvpn_key mqvpn_port mqvpn_user mqvpn_cc mqvpn_scheduler mqvpn_fec_enable_raw mqvpn_fec_enable mqvpn_fec_scheme mqvpn_reinjection_control_raw mqvpn_reinjection_control mqvpn_reinjection_mode mqvpn_reorder_enabled_raw mqvpn_reorder_enabled mqvpn_reorder_max_wait_ms mqvpn_reorder_cap_packets mqvpn_reorder_rules_json mqvpn_pinned_pubkey mqvpn_rule_idx mqvpn_rule_proto mqvpn_rule_port mqvpn_rule_profile proxy_before proxy_engines_before
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return

	logger -t "OMR-VPS" "Get config from server ${servername}..."
	# get VPS ip
	_get_vpsip
	vps_lastchange="$(echo "$vps_config" | jsonfilter -q -e '@.vps.lastchange')"
	lastchange="$(uci -q get openmptcprouter.${servername}.lastchange)"
	[ -z "$lastchange" ] && lastchange=0
	#[ -n "$lastchange" ] && [ -n "$vps_lastchange" ] && [ "$lastchange" -gt "$vps_lastchange" ] && return
	user_permission="$(echo "$vps_config" | jsonfilter -q -e '@.user.permission')"
	[ -z "$user_permission" ] && user_permission="rw"

	vpsinternet="$(echo "$vps_config" | jsonfilter -q -e '@.network.internet')"
	if [ "$vpsinternet" = "false" ] && [ "$(uci -q get openmptcprouter.settings.external_check)" != "0" ]; then
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.settings.external_check='0'
		EOF
		logger -t "OMR-VPS" "OMR-Tracker restart..."
		/etc/init.d/omr-tracker restart
	fi

	ula="$(echo "$vps_config" | jsonfilter -q -e '@.ip6in4.ula')"
	if [ -n "$ula" ]; then
		case "$ula" in
			*" "*) ;;
			*)
				uci -q batch <<-EOF >/dev/null
				set network.globals.ula_prefix=$ula
				EOF
			;;
		esac
	fi

	# Set current VPN
	current_vpn="$(uci -q get openmptcprouter.settings.vpn)"
	if [ -z "$current_vpn" ] || ([ -n "$vps_lastchange" ] && [ "$vps_lastchange" -gt "$lastchange" ]) || [ "$user_permission" = "ro" ]; then
		current_vpn="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.current')"
		if [ -n "$current_vpn" ]; then
			uci -q batch <<-EOF >/dev/null
			set openmptcprouter.settings.vpn=${current_vpn}
			commit openmptcprouter
			EOF
		fi
	fi

	# Set current Proxy
	current_proxy="$(uci -q get openmptcprouter.settings.proxy)"
	if [ -z "$current_proxy" ] || ([ -n "$vps_lastchange" ] && [ "$vps_lastchange" -gt "$lastchange" ]) || [ "$user_permission" = "ro" ]; then
		proxy_before="$current_proxy"
		proxy_engines_before="$(_proxy_engines)"
		current_proxy="$(echo "$vps_config" | jsonfilter -q -e '@.proxy.current')"
		if [ -n "$current_proxy" ]; then
			uci -q batch <<-EOF >/dev/null
			set openmptcprouter.settings.proxy=${current_proxy}
			commit openmptcprouter
			EOF
			if [ "$current_proxy" = "shadowsocks" ]; then
				uci -q batch <<-EOF >/dev/null
					set shadowsocks-rust.sss0.disabled=1
					commit shadowsocks-rust
					set shadowsocks-libev.sss0.disabled=0
					commit shadowsocks-libev
					set v2ray.main.enabled=0
					commit v2ray
					set xray.main.enabled=0
					commit xray
					set openmptcprouter.settings.shadowsocks_disable=0
					commit openmptcprouter
				EOF
			elif [ "$current_proxy" = "shadowsocks-rust" ]; then
				uci -q batch <<-EOF >/dev/null
					set shadowsocks-rust.sss0.disabled=0
					commit shadowsocks-rust
					set shadowsocks-libev.sss0.disabled=1
					commit shadowsocks-libev
					set v2ray.main.enabled=0
					commit v2ray
					set xray.main.enabled=0
					commit xray
					set openmptcprouter.settings.shadowsocks_disable=0
					commit openmptcprouter
				EOF
			elif [ "$current_proxy" = "v2ray" ]; then
				uci -q batch <<-EOF >/dev/null
					set shadowsocks-rust.sss0.disabled=1
					commit shadowsocks-rust
					set shadowsocks-libev.sss0.disabled=1
					commit shadowsocks-libev
					set v2ray.main.enabled=1
					commit v2ray
					set xray.main.enabled=0
					commit xray
					set openmptcprouter.settings.shadowsocks_disable=1
					commit openmptcprouter
				EOF
			elif [ "$current_proxy" = "xray" ]; then
				uci -q batch <<-EOF >/dev/null
					set shadowsocks-rust.sss0.disabled=1
					commit shadowsocks-rust
					set shadowsocks-libev.sss0.disabled=1
					commit shadowsocks-libev
					set v2ray.main.enabled=0
					commit v2ray
					set xray.main.enabled=1
					commit xray
					set openmptcprouter.settings.shadowsocks_disable=1
					commit openmptcprouter
				EOF
			elif [ "$current_proxy" = "none" ]; then
				uci -q batch <<-EOF >/dev/null
					set shadowsocks-rust.sss0.disabled=1
					commit shadowsocks-rust
					set shadowsocks-libev.sss0.disabled=1
					commit shadowsocks-libev
					set v2ray.main.enabled=0
					commit v2ray
					set xray.main.enabled=0
					commit xray
					set openmptcprouter.settings.shadowsocks_disable=1
					commit openmptcprouter
				EOF
			fi
			# omr-tracker only reconciles which per-proxy watchdog instances
			# are running when it (re)starts. Without this, switching the
			# active proxy here (the VPS-poll path, as opposed to the local
			# wizard Save & Apply path which already restarts omr-tracker)
			# leaves the old proxy's tracker process orphaned, endlessly
			# trying to restart a proxy that was intentionally disabled.
			if [ "$proxy_before" != "$current_proxy" ]; then
				/etc/init.d/omr-tracker restart >/dev/null 2>&1
			elif [ "$proxy_engines_before" != "$(_proxy_engines)" ]; then
				# Same proxy, but its engine was just switched on: the first
				# config pulled after a fresh install enables sss0 (shipped
				# disabled) while settings.proxy already names
				# shadowsocks-rust. omr-tracker started at boot with no proxy
				# watchdog and no server tracker, so openmptcprouter.omr.proxy
				# stayed unset (the proxy reported down) until omr-schedule's
				# 010-services noticed, minutes later. start, not restart:
				# procd adds the missing trackers and leaves the WAN ones
				# running.
				/etc/init.d/omr-tracker start >/dev/null 2>&1
			fi
		fi
	fi

	# Shadowsocks settings
	#shadowsocks_disabled="$(uci -q get openmptcprouter.settings.shadowsocks_disable)"
	#[ -z "$shadowsocks_disabled" ] && shadowsocks_disabled=0
	ss_key="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.key')"
	ss_key="$(echo $ss_key | sed 's/-/+/g; s/_/\//g;')"
	if [ -n "$ss_key" ] && [ "$ss_key" != "$(uci -q get shadowsocks-libev.sss0.key)" ] && [ -n "$(uci -q get shadowsocks-libev.sss0)" ]; then
		ss_method="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.method')"
		ss_port="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.port')"
		ss_ebpf="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.ebpf')"
		if [ "$ss_ebpf" = "true" ] && [ "$ss_method" = "none" ]; then
			ss_ebpf=1
		else
			ss_ebpf=0
		fi
		[ -z "$ss_port" ] && ss_port=65101
		ss_no_delay="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.no_delay')"
		ss_fast_open="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.fast_open')"
		ss_obfs="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs')"
		if [ "$ss_obfs" = "true" ]; then
			ss_obfs=1
		else
			ss_obfs=0
		fi
		ss_obfs_plugin="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs_plugin')"
		ss_obfs_type="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.obfs_type')"
		ss_obfs_host="$(echo "$vps_config" | jsonfilter -q -e '@.network.domain')"
		#ss_reuse_port="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks.reuse_port')"
		config_load shadowsocks-libev
		config_foreach _set_ss_redir ss_redir "no_delay" $ss_no_delay
		config_foreach _set_ss_redir ss_redir "fast_open" $ss_fast_open
		config_foreach _set_ss_redir ss_redir "ebpf" $ss_ebpf
		config_foreach _set_ss_redir ss_local "no_delay" $ss_no_delay
		config_foreach _set_ss_redir ss_local "fast_open" $ss_fast_open
		config_foreach _set_ss_redir ss_local "ebpf" $ss_ebpf
		uci -q batch <<-EOF >/dev/null
			set shadowsocks-libev.sss0.key=$ss_key
			set shadowsocks-libev.sss0.server_port=$ss_port
			set shadowsocks-libev.sss0.method=$ss_method
			set shadowsocks-libev.sss0.obfs=$ss_obfs
			set shadowsocks-libev.sss0.obfs_plugin=$ss_obfs_plugin
			set shadowsocks-libev.sss0.obfs_type=$ss_obfs_type
			set shadowsocks-libev.sss0.obfs_host=$ss_obfs_host
		EOF
		config_foreach _set_ss_server server "key" $ss_key
		config_foreach _set_ss_server server "method" $ss_method
		config_foreach _set_ss_server server "obfs" $ss_obfs
		config_foreach _set_ss_server server "obfs_plugin" $ss_obfs_plugin
		config_foreach _set_ss_server server "obfs_type" $ss_obfs_type
		config_foreach _set_ss_server server "obfs_host" $ss_obfs_host
		config_foreach _set_ss_server server "server" $vpsip
		if [ -n "$(uci -q changes shadowsocks-libev)" ]; then
			uci -q commit shadowsocks-libev
			if [ "$(uci -q get shadowsocks-libev.sss0.disabled)" != "1" ]; then
				logger -t "OMR-VPS" "Shadowsocks restart..."
				/etc/init.d/shadowsocks-libev restart >/dev/null 2>&1
			else
				logger -t "OMR-VPS" "Shadowsocks stop..."
				/etc/init.d/shadowsocks-libev stop >/dev/null 2>&1
			fi
		fi
	fi

	# Shadowsocks Rust settings
	#shadowsocks_disabled="$(uci -q get openmptcprouter.settings.shadowsocks_disable)"
	#[ -z "$shadowsocks_disabled" ] && shadowsocks_disabled=0
	ssgo_sskey="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.password')"
	if [ -n "$ssgo_sskey" ] && [ "$ssgo_sskey" != "$(uci -q get shadowsocks-rust.sss0.password)" ] && [ -n "$(uci -q get shadowsocks-rust.sss0)" ]; then
		ss_method="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks_go.config.protocol')"
		ss_port="65280"
		#ss_fast_open="$(echo "$vps_config" | jsonfilter -q -e '@.shadowsocks-rust.fast_open')"
		config_load shadowsocks-rust
		#config_foreach _set_ssrust_redir ss_redir "fast_open" $ss_fast_open
		#config_foreach _set_ssrust_redir ss_local "fast_open" $ss_fast_open
		uci -q batch <<-EOF >/dev/null
			set shadowsocks-rust.sss0.password=$ssgo_sskey
			set shadowsocks-rust.sss0.server_port=$ss_port
			set shadowsocks-rust.sss0.method=$ss_method
		EOF
		# sss1 (the IPv6 slot) is the same VPS on the same port; keep it in
		# sync too, but only once it actually has an address of its own --
		# otherwise this would be the only place a bare "server_port" ever
		# gets set on a still-unconfigured sss1.
		if [ -n "$(uci -q get shadowsocks-rust.sss1.server)" ]; then
			uci -q set shadowsocks-rust.sss1.server_port=$ss_port
		fi
		config_foreach _set_ssrust_server server "password" $ssgo_sskey
		config_foreach _set_ssrust_server server "method" $ss_method
		config_foreach _set_ssrust_server server "server" $vpsip
		if [ -n "$(uci -q changes shadowsocks-rust)" ]; then
			uci -q commit shadowsocks-rust
			if [ "$(uci -q get shadowsocks-rust.sss0.disabled)" != "1" ]; then
				logger -t "OMR-VPS" "Shadowsocks Rust restart..."
				/etc/init.d/shadowsocks-rust restart >/dev/null 2>&1
			else
				logger -t "OMR-VPS" "Shadowsocks Rust stop..."
				/etc/init.d/shadowsocks-rust stop >/dev/null 2>&1
			fi
		fi
	fi

	# V2Ray settings
	v2ray_key="$(echo "$vps_config" | jsonfilter -q -e '@.v2ray.config.key')"
	#v2ray_port="$(echo "$vps_config" | jsonfilter -q -e '@.v2ray.config.port')"
	v2ray_port="65228"
	if [ -n "$(uci -q get v2ray.omr)" ] && ( ([ -n "$v2ray_key" ] && [ "$v2ray_key" != "$(uci -q get v2ray.omrout.s_vmess_user_id)" ]) || ([ -n "$v2ray_port" ] && [ "$v2ray_port" != "$(uci -q get v2ray.omrout.s_vless_port)" ]) ); then
		uci -q batch <<-EOF >/dev/null
			set v2ray.omrout.s_socks_user_id="$v2ray_key"
			set v2ray.omrout.s_socks_port="$((v2ray_port+3))"
			set v2ray.omrout.s_trojan_user_id="$v2ray_key"
			set v2ray.omrout.s_trojan_port="$((v2ray_port+1))"
			set v2ray.omrout.s_vmess_user_id="$v2ray_key"
			set v2ray.omrout.s_vmess_port="$((v2ray_port+2))"
			set v2ray.omrout.s_vless_user_id="$v2ray_key"
			set v2ray.omrout.s_vless_port="$v2ray_port"
		EOF
		uci -q set v2ray.omrout.s_vmess_address="$vpsip"
		uci -q set v2ray.omrout.s_vless_address="$vpsip"
		uci -q set v2ray.omrout.s_trojan_address="$vpsip"
		uci -q set v2ray.omrout.s_socks_address="$vpsip"
		if [ -n "$(uci -q changes v2ray)" ]; then
			uci -q commit v2ray
			if [ "$(uci -q get v2ray.main.enabled)" = "1" ]; then
				logger -t "OMR-VPS" "V2ray restart..."
				/etc/init.d/v2ray restart >/dev/null 2>&1
			else
				logger -t "OMR-VPS" "Stop restart..."
				/etc/init.d/v2ray stop >/dev/null 2>&1
			fi
		fi
	fi

	# XRay settings
	xray_key="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.key')"
	xray_sskey="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.sskey')"
	xray_vless_reality_key="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.vless_reality_key')"
	xray_reverse_key="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.reverse_key')"
	xray_transport="$(echo "$vps_config" | jsonfilter -q -e '@.xray.config.transport')"
	#v2ray_port="$(echo "$vps_config" | jsonfilter -q -e '@.v2ray.config.port')"
	xray_port="65248"
	if [ -n "$(uci -q get xray.omr)" ] && ( ([ -n "$xray_key" ] && [ "$xray_key" != "$(uci -q get xray.omrout.s_vmess_user_id)" ]) || ([ -n "$xray_port" ] && [ "$xray_port" != "$(uci -q get xray.omrout.s_vless_port)" ]) || ([ -n "$xray_transport" ] && [ "$xray_transport" != "$(uci -q get xray.omrout.ss_network)" ]) || ([ -n "$xray_vless_reality_key" ] && [ "$xray_vless_reality_key" != "$(uci -q get xray.omrout.s_vless_reality_public_key)" ]) || ([ -n "$xray_reverse_key" ] && [ "$xray_reverse_key" != "$(uci -q get xray.omrout.s_vless_reverse_user_id)" ]) || ([ -n "$xray_sskey" ] && [ "$xray_sskey" != "$(uci -q get xray.omrout.s_shadowsocks_password)" ])); then
		uci -q batch <<-EOF >/dev/null
			set xray.omrout.s_shadowsocks_port="$((xray_port+4))"
			set xray.omrout.s_shadowsocks_password="$xray_sskey"
			set xray.omrout.s_socks_user_id="$xray_key"
			set xray.omrout.s_socks_port="$((xray_port+3))"
			set xray.omrout.s_trojan_user_id="$xray_key"
			set xray.omrout.s_trojan_port="$((xray_port+1))"
			set xray.omrout.s_vmess_user_id="$xray_key"
			set xray.omrout.s_vmess_port="$((xray_port+2))"
			set xray.omrout.s_vless_user_id="$xray_key"
			set xray.omrout.s_vless_port="$xray_port"
			set xray.omrout.s_vless_reality_user_id="$xray_key"
			set xray.omrout.s_vless_reality_public_key="$xray_vless_reality_key"
			set xray.omrout.ss_network="$xray_transport"
		EOF
		[ -n "$xray_reverse_key" ] && uci -q set xray.omrout.s_vless_reverse_user_id="$xray_reverse_key"
		#uci -q set xray.omrout.s_shadowsocks_password=$xray_sskey
		uci -q set xray.omrout.s_vmess_address="$vpsip"
		uci -q set xray.omrout.s_vless_address="$vpsip"
		uci -q set xray.omrout.s_vless_reality_address="$vpsip"
		uci -q set xray.omrout.s_trojan_address="$vpsip"
		uci -q set xray.omrout.s_socks_address="$vpsip"
		uci -q set xray.omrout.s_shadowsocks_address="$vpsip"
		if [ -n "$(uci -q changes xray)" ]; then
			uci -q commit xray
			if [ "$(uci -q get xray.main.enabled)" = "1" ]; then
				logger -t "OMR-VPS" "Xray restart..."
				/etc/init.d/xray restart >/dev/null 2>&1
			else
				logger -t "OMR-VPS" "Xray stop..."
				/etc/init.d/xray stop >/dev/null 2>&1
			fi
		fi
	fi

	# Glorytun settings
	glorytun_key="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.key')"
	glorytun_port="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.port')"
	if [ -n "$(uci -q get glorytun.vpn)" ] && ( ([ -n "$glorytun_key" ] && [ "$glorytun_key" != "$(uci -q get glorytun.vpn.key)" ]) || ([ -n "$glorytun_port" ] && [ "$glorytun_port" != "$(uci -q get glorytun.vpn.port)" ]) ); then
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		glorytun_state=0
		if [ "$vpn" = "glorytun_tcp" ]; then
			glorytun_state=1
		fi
		[ -z "$glorytun_port" ] && glorytun_port="65001"
		glorytun_chacha="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.chacha')"
		{ [ -z "$glorytun_chacha" ] || [ "$glorytun_chacha" = "true" ]; } && glorytun_chacha=1
		[ "$glorytun_chacha" = "false" ] && glorytun_chacha=0
		uci -q batch <<-EOF >/dev/null
			set glorytun.vpn.port=$glorytun_port
			set glorytun.vpn.key=$glorytun_key
			set glorytun.vpn.enable=$glorytun_state
			set glorytun.vpn.chacha20=$glorytun_chacha
		EOF
		if [ -z "$(uci -q get openmptcprouter.settings.mptcpovervpn)" ]; then
			uci -q set glorytun.vpn.host="$vpsip"
		fi
		if [ -n "$(uci -q changes glorytun)" ]; then
			uci -q commit glorytun
			logger -t "OMR-VPS" "Glorytun restart..."
			/etc/init.d/glorytun restart >/dev/null 2>&1
		fi
	fi
	if [ -n "$(uci -q get glorytun-udp.vpn)" ] && ( ([ -n "$glorytun_key" ] && [ "$glorytun_key" != "$(uci -q get glorytun-udp.vpn.key)" ]) || ([ -n "$glorytun_port" ] && [ "$glorytun_port" != "$(uci -q get glorytun-udp.vpn.port)" ]) ); then
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		glorytun_state=0
		if [ "$vpn" = "glorytun_udp" ]; then
			glorytun_state=1
		fi
		[ -z "$glorytun_port" ] && glorytun_port="65001"
		glorytun_chacha="$(echo "$vps_config" | jsonfilter -q -e '@.glorytun.chacha')"
		{ [ -z "$glorytun_chacha" ] || [ "$glorytun_chacha" = "true" ]; } && glorytun_chacha=1
		[ "$glorytun_chacha" = "false" ] && glorytun_chacha=0
		uci -q batch <<-EOF >/dev/null
			set glorytun-udp.vpn.port=$glorytun_port
			set glorytun-udp.vpn.key=$glorytun_key
			set glorytun-udp.vpn.enable=$glorytun_state
			set glorytun-udp.vpn.chacha=$glorytun_chacha
		EOF
		if [ -z "$(uci -q get openmptcprouter.settings.mptcpovervpn)" ]; then
			uci -q set glorytun-udp.vpn.host="$vpsip"
		fi
		if [ -n "$(uci -q changes glorytun-udp)" ]; then
			uci -q commit glorytun-udp
			logger -t "OMR-VPS" "Glorytun UDP restart..."
			/etc/init.d/glorytun-udp restart >/dev/null 2>&1
		fi
	fi

	# OpenVPN settings
	openvpn_key="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.key')"
	[ -n "$openvpn_key" ] && {
		# 0600 as client.key, also for a key written 0644 by an older version
		( umask 077; echo $openvpn_key | base64 -d > /etc/luci-uploads/openvpn.key )
		chmod 0600 /etc/luci-uploads/openvpn.key
		openvpn_port="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.port')"
		[ -z "$openvpn_port" ] && openvpn_port="65001"
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		openvpn_state=0
		if [ "$vpn" = "openvpn" ]; then
			openvpn_state=1
		fi
		config_load openvpn
		config_foreach _set_openvpn_server openvpn "port" "$openvpn_port"
		# The static key is the legacy setup. The current client config is
		# tls_client, and openvpn rejects --secret together with --tls-client,
		# so only wire it up where it is actually the mode in use -- the
		# certificate block below is what enables a tls_client section.
		# --remote-cert-tls is TLS-mode only, openvpn refuses it with --secret.
		[ "$(uci -q get openvpn.omr.tls_client)" != "1" ] && {
			config_foreach _set_openvpn_server openvpn "secret" "/etc/luci-uploads/openvpn.key"
			config_foreach _del_openvpn_option openvpn "remote_cert_tls"
		}

		#uci -q batch <<-EOF >/dev/null
		#	set openvpn.omr.port=$openvpn_port
		#	set openvpn.omr.secret="/etc/luci-uploads/openvpn.key"
		#EOF
		if [ "$(uci -q get openmptcprouter.settings.openvpn_lb)" = "0" ]; then
			config_foreach _set_openvpn_other_off openvpn omr
			_set_openvpn_enabled omr "$openvpn_state"
		else
			config_foreach _set_openvpn_enabled openvpn "$openvpn_state"
		fi
		#if [ "$openvpn_state" = "1" ]; then
		#	uci -q set openvpn.omr.enabled=$openvpn_state
		#else
		#	uci -q del openvpn.omr.enabled
		#fi
		config_foreach _set_openvpn_server openvpn "remote" "$vpsip"
		#uci -q set openvpn.omr.remote="$vpsip"
		if [ -n "$(uci -q changes openvpn)" ]; then
			uci -q commit openvpn
			if [ "$(uci -q get openvpn.omr.enabled)" = "1" ]; then
				logger -t "OMR-VPS" "OpenVPN restart..."
				/etc/init.d/openvpn restart
			fi
		fi
	}
	openvpn_client_key="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_key')"
	[ -n "$openvpn_client_key" ] && {
		# 0600, also for a key written 0644 by an older version
		( umask 077; echo $openvpn_client_key | base64 -d > /etc/luci-uploads/client.key )
		chmod 0600 /etc/luci-uploads/client.key
	}
	openvpn_client_crt="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_crt')"
	[ -n "$openvpn_client_crt" ] && {
		echo $openvpn_client_crt | base64 -d > /etc/luci-uploads/client.crt
	}
	openvpn_client_ca="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.client_ca')"
	[ -n "$openvpn_client_ca" ] && {
		echo $openvpn_client_ca | base64 -d > /etc/luci-uploads/ca.crt
	}
	[ -n "$openvpn_client_key" ] && [ -n "$openvpn_client_ca" ] && {
		openvpn_port="$(echo "$vps_config" | jsonfilter -q -e '@.openvpn.port')"
		[ -z "$openvpn_port" ] && openvpn_port="65001"
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		openvpn_state=0
		if [ "$vpn" = "openvpn" ]; then
			openvpn_state=1
		fi

		NBCPU=$(grep -c '^processor' /proc/cpuinfo | tr -d "\n")
		if [ "$NBCPU" -gt 1 ] && [ -z "$(uci -q get openvpn.omr2)" ]; then
			[ -f /rom/etc/uci-defaults/2020-omr-vpn ] && /rom/etc/uci-defaults/2020-omr-vpn
			[ -f /usr/share/omr-update/2020-omr-vpn ] && /usr/share/omr-update/2020-omr-vpn
		fi

		config_load openvpn
		config_foreach _set_openvpn_server openvpn "key" "/etc/luci-uploads/client.key"
		[ -n "$openvpn_client_crt" ] && config_foreach _set_openvpn_server openvpn "cert" "/etc/luci-uploads/client.crt"
		config_foreach _set_openvpn_server openvpn "ca" "/etc/luci-uploads/ca.crt"
		config_foreach _set_openvpn_server openvpn "tls_client" "1"
		# omradmin signs every user's client certificate with this same CA, so
		# the CA alone would also accept another user's client cert presented
		# by a man in the middle. Only the VPS's own cert has the serverAuth EKU.
		config_foreach _set_openvpn_server openvpn "remote_cert_tls" "server"
		config_foreach _set_openvpn_server openvpn "client" "1"
		config_foreach _set_openvpn_server openvpn "allow_recursive_routing" "1"
		# A tls_client section and a static key are mutually exclusive for
		# openvpn, and the block above just made this one tls_client.
		config_foreach _del_openvpn_option openvpn "secret"
		# "enabled" comes last: the readiness check reads the options set above.
		if [ "$(uci -q get openmptcprouter.settings.openvpn_lb)" = "0" ]; then
			config_foreach _set_openvpn_other_off openvpn omr
			_set_openvpn_enabled omr "$openvpn_state"
		else
			config_foreach _set_openvpn_enabled openvpn "$openvpn_state"
		fi

		#uci -q batch <<-EOF >/dev/null
		#	set openvpn.omr.port=$openvpn_port
		#	set openvpn.omr.key="/etc/luci-uploads/client.key"
		#	set openvpn.omr.cert="/etc/luci-uploads/client.crt"
		#	set openvpn.omr.ca="/etc/luci-uploads/ca.crt"
		#	set openvpn.omr.enabled=$openvpn_state
		#	set openvpn.omr.tls_client=1
		#	set openvpn.omr.client=1
		#	set openvpn.omr.allow_recursive_routing=1
		#EOF
		config_foreach _set_openvpn_server openvpn "remote" "$vpsip"
		#uci -q set openvpn.omr.remote="$vpsip"
		if [ -n "$(uci -q changes openvpn)" ]; then
			uci -q commit openvpn
			if [ "$(uci -q get openvpn.omr.enabled)" = "1" ]; then
				logger -t "OMR-VPS" "OpenVPN restart..."
				/etc/init.d/openvpn restart
			fi
		fi
	}

	# MLVPN settings
	mlvpn_key="$(echo "$vps_config" | jsonfilter -q -e '@.mlvpn.key')"
	if [ -n "$(uci -q get mlvpn.general)" ] && [ -n "$mlvpn_key" ] && [ "$mlvpn_key" != "$(uci -q get mlvpn.general.password)" ] && [ -f /etc/init.d/mlvpn ]; then
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		mlvpn_state=0
		if [ "$vpn" = "mlvpn" ]; then
			mlvpn_state=1
		fi
		uci -q batch <<-EOF >/dev/null
			set mlvpn.general.password=$mlvpn_key
			set mlvpn.general.enable=$mlvpn_state
		EOF
		uci -q set mlvpn.general.host="$vpsip"
		if [ -n "$(uci -q changes mlvpn)" ]; then
			uci -q commit mlvpn
			logger -t "OMR-VPS" "MLVPN restart..."
			/etc/init.d/mlvpn restart
		fi
	fi

	# DSVPN settings
	dsvpn_key="$(echo "$vps_config" | jsonfilter -q -e '@.dsvpn.key')"
	dsvpn_port="$(echo "$vps_config" | jsonfilter -q -e '@.dsvpn.port')"
	if [ -f /etc/init.d/dsvpn ] && [ -n "$(uci -q get dsvpn.vpn)" ] && ( ([ -n "$dsvpn_key" ] && [ "$dsvpn_key" != "$(uci -q get dsvpn.vpn.key)" ]) || ([ -n "$dsvpn_port" ] && [ "$dsvpn_port" != "$(uci -q get dsvpn.vpn.port)" ]) ); then
		dsvpn_state=0
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		if [ "$vpn" = "dsvpn" ]; then
			dsvpn_state=1
		fi
		uci -q batch <<-EOF >/dev/null
			$([ -n "$dsvpn_key" ] && echo "set dsvpn.vpn.key=$dsvpn_key")
			set dsvpn.vpn.enable=$dsvpn_state
			$([ -n "$dsvpn_port" ] && echo "set dsvpn.vpn.port=$dsvpn_port")
		EOF
		uci -q set dsvpn.vpn.host="$vpsip"
		if [ -n "$(uci -q changes dsvpn)" ]; then
			uci -q commit dsvpn
			logger -t "OMR-VPS" "DSVPN restart..."
			/etc/init.d/dsvpn restart
		fi
	fi

	# MQVPN settings
	mqvpn_key="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.key')"
	mqvpn_port="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.port')"
	[ -z "$mqvpn_port" ] && mqvpn_port="65443"
	# mqvpn.server.port is user intent (pushed to the VPS by _set_mqvpn_vps):
	# only default it from the VPS when not set locally
	[ -n "$(uci -q get mqvpn.server.port)" ] && mqvpn_port="$(uci -q get mqvpn.server.port)"
	mqvpn_cc="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.cc')"
	mqvpn_scheduler="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.scheduler')"
	mqvpn_fec_enable_raw="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.fec_enable')"
	mqvpn_fec_scheme="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.fec_scheme')"
	mqvpn_reinjection_control_raw="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reinjection_control')"
	mqvpn_reinjection_mode="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reinjection_mode')"
	mqvpn_reorder_enabled_raw="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.enabled')"
	mqvpn_reorder_max_wait_ms="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.max_wait_ms')"
	mqvpn_reorder_cap_packets="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder.cap_packets')"
	mqvpn_reorder_rules_json="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.reorder_rules')"
	# mqvpn 0.18.0, both ends must agree (omr-admin 0.18+20261026)
	mqvpn_path_mtu="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.path_mtu')"
	case "$mqvpn_path_mtu" in
		0) ;;
		''|*[!0-9]*|?????*) mqvpn_path_mtu="" ;;
		*) [ "$mqvpn_path_mtu" -ge 1280 ] && [ "$mqvpn_path_mtu" -le 9000 ] || mqvpn_path_mtu="" ;;
	esac
	mqvpn_pmtud_raw="$(echo "$vps_config" | jsonfilter -q -e '@.mqvpn.pmtud')"
	case "$mqvpn_pmtud_raw" in true|false) ;; *) mqvpn_pmtud_raw="" ;; esac
	mqvpn_pinned_pubkey="$(_mqvpn_vps_pin "$vps_config")"
	[ "$mqvpn_fec_enable_raw" = "true" ] && mqvpn_fec_enable="1" || mqvpn_fec_enable="0"
	[ "$mqvpn_reinjection_control_raw" = "true" ] && mqvpn_reinjection_control="1" || mqvpn_reinjection_control="0"
	[ "$mqvpn_pmtud_raw" = "true" ] && mqvpn_pmtud="1" || mqvpn_pmtud="0"
	case "$mqvpn_reorder_enabled_raw" in
		on|auto) mqvpn_reorder_enabled="1" ;;
		off) mqvpn_reorder_enabled="0" ;;
		*) mqvpn_reorder_enabled="" ;;
	esac
	# Same rule mqvpn.server.port above already follows: the multipath and
	# reorder tuning is user intent, set on the MQVPN page and pushed to the
	# server by _set_mqvpn_vps. Adopting the server's copy here overwrote what
	# the user had just chosen -- picking "Redundant" applied to the client
	# until the next config sync, which put the server's "minrtt" back and
	# restarted mqvpn with it, so a WAN failure still cut real-time traffic
	# (issue #4379). Blanking the value drops the option from both the
	# comparison below and the uci batch that follows it.
	mqvpn_cc="$(_mqvpn_local_wins mqvpn.multipath.cc "$mqvpn_cc")"
	mqvpn_scheduler="$(_mqvpn_local_wins mqvpn.multipath.scheduler "$mqvpn_scheduler")"
	mqvpn_fec_enable_raw="$(_mqvpn_local_wins mqvpn.multipath.fec_enable "$mqvpn_fec_enable_raw")"
	mqvpn_fec_scheme="$(_mqvpn_local_wins mqvpn.multipath.fec_scheme "$mqvpn_fec_scheme")"
	mqvpn_reinjection_control_raw="$(_mqvpn_local_wins mqvpn.multipath.reinjection_control "$mqvpn_reinjection_control_raw")"
	mqvpn_reinjection_mode="$(_mqvpn_local_wins mqvpn.multipath.reinjection_mode "$mqvpn_reinjection_mode")"
	mqvpn_reorder_enabled="$(_mqvpn_local_wins mqvpn.reorder.enabled "$mqvpn_reorder_enabled")"
	mqvpn_reorder_max_wait_ms="$(_mqvpn_local_wins mqvpn.reorder.max_wait_ms "$mqvpn_reorder_max_wait_ms")"
	mqvpn_reorder_cap_packets="$(_mqvpn_local_wins mqvpn.reorder.cap_packets "$mqvpn_reorder_cap_packets")"
	mqvpn_reorder_rules_json="$(_mqvpn_local_wins "mqvpn.@reorder_rule[0]" "$mqvpn_reorder_rules_json")"
	mqvpn_path_mtu="$(_mqvpn_local_wins mqvpn.interface.path_mtu "$mqvpn_path_mtu")"
	mqvpn_pmtud_raw="$(_mqvpn_local_wins mqvpn.interface.pmtud "$mqvpn_pmtud_raw")"
	mqvpn_user="$(uci -q get openmptcprouter.${servername}.username)"
	if [ -f /etc/init.d/mqvpn ] && [ -n "$(uci -q get mqvpn.auth)" ] && [ -n "$mqvpn_key" ] && \
	   ( [ "$mqvpn_key" != "$(uci -q get mqvpn.auth.key)" ] || \
	     [ "$mqvpn_user" != "$(uci -q get mqvpn.auth.user)" ] || \
	     [ "$(uci -q get mqvpn.server.ip)" != "$vpsip" ] || \
	     [ "$(uci -q get mqvpn.server.port)" != "$mqvpn_port" ] || \
	     [ "$(uci -q get mqvpn.server.insecure)" != "1" ] || \
	     [ "$(uci -q get mqvpn.server.pinned_pubkey)" != "$mqvpn_pinned_pubkey" ] || \
	     ( [ -n "$mqvpn_cc" ] && [ "$mqvpn_cc" != "$(uci -q get mqvpn.multipath.cc)" ] ) || \
	     ( [ -n "$mqvpn_scheduler" ] && [ "$mqvpn_scheduler" != "$(uci -q get mqvpn.multipath.scheduler)" ] ) || \
	     ( [ -n "$mqvpn_fec_enable_raw" ] && [ "$mqvpn_fec_enable" != "$(uci -q get mqvpn.multipath.fec_enable)" ] ) || \
	     ( [ -n "$mqvpn_fec_scheme" ] && [ "$mqvpn_fec_scheme" != "$(uci -q get mqvpn.multipath.fec_scheme)" ] ) || \
	     ( [ -n "$mqvpn_reinjection_control_raw" ] && [ "$mqvpn_reinjection_control" != "$(uci -q get mqvpn.multipath.reinjection_control)" ] ) || \
	     ( [ -n "$mqvpn_reinjection_mode" ] && [ "$mqvpn_reinjection_mode" != "$(uci -q get mqvpn.multipath.reinjection_mode)" ] ) || \
	     ( [ -n "$mqvpn_reorder_enabled" ] && [ "$mqvpn_reorder_enabled" != "$(uci -q get mqvpn.reorder.enabled)" ] ) || \
	     ( [ -n "$mqvpn_path_mtu" ] && [ "$mqvpn_path_mtu" != "$(uci -q get mqvpn.interface.path_mtu)" ] ) || \
	     ( [ -n "$mqvpn_pmtud_raw" ] && [ "$mqvpn_pmtud" != "$(uci -q get mqvpn.interface.pmtud)" ] ) || \
	     ( [ "$(uci -q get openmptcprouter.settings.vpn)" = "mqvpn" ] && [ "$(uci -q get mqvpn.settings.enable)" != "1" ] ) ); then
		mqvpn_state=0
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		if [ "$vpn" = "mqvpn" ]; then
			mqvpn_state=1
		fi
		uci -q batch <<-EOF >/dev/null
			set mqvpn.auth.key=$mqvpn_key
			set mqvpn.auth.user=$mqvpn_user
			set mqvpn.settings.enable=$mqvpn_state
			set mqvpn.server.ip="$vpsip"
			set mqvpn.server.port="$mqvpn_port"
			set mqvpn.server.insecure=1
			$([ -n "$mqvpn_pinned_pubkey" ] && echo "set mqvpn.server.pinned_pubkey=$mqvpn_pinned_pubkey")
			$([ -z "$mqvpn_pinned_pubkey" ] && [ -n "$(uci -q get mqvpn.server.pinned_pubkey)" ] && echo "delete mqvpn.server.pinned_pubkey")
			$([ -n "$mqvpn_cc" ] && echo "set mqvpn.multipath.cc=$mqvpn_cc")
			$([ -n "$mqvpn_scheduler" ] && echo "set mqvpn.multipath.scheduler=$mqvpn_scheduler")
			$([ -n "$mqvpn_fec_enable_raw" ] && echo "set mqvpn.multipath.fec_enable=$mqvpn_fec_enable")
			$([ -n "$mqvpn_fec_scheme" ] && echo "set mqvpn.multipath.fec_scheme=$mqvpn_fec_scheme")
			$([ -n "$mqvpn_reinjection_control_raw" ] && echo "set mqvpn.multipath.reinjection_control=$mqvpn_reinjection_control")
			$([ -n "$mqvpn_reinjection_mode" ] && echo "set mqvpn.multipath.reinjection_mode=$mqvpn_reinjection_mode")
			$([ -n "$mqvpn_reorder_enabled" ] && echo "set mqvpn.reorder.enabled=$mqvpn_reorder_enabled")
			$([ -n "$mqvpn_reorder_max_wait_ms" ] && echo "set mqvpn.reorder.max_wait_ms=$mqvpn_reorder_max_wait_ms")
			$([ -n "$mqvpn_reorder_cap_packets" ] && echo "set mqvpn.reorder.cap_packets=$mqvpn_reorder_cap_packets")
			$([ -n "$mqvpn_path_mtu" ] && echo "set mqvpn.interface.path_mtu=$mqvpn_path_mtu")
			$([ -n "$mqvpn_pmtud_raw" ] && echo "set mqvpn.interface.pmtud=$mqvpn_pmtud")
		EOF
		if [ -n "$mqvpn_reorder_rules_json" ]; then
			while uci -q delete mqvpn.@reorder_rule[0] 2>/dev/null; do :; done
			mqvpn_rule_idx=0
			while true; do
				mqvpn_rule_proto="$(echo "$vps_config" | jsonfilter -q -e "@.mqvpn.reorder_rules[$mqvpn_rule_idx].proto")"
				[ -z "$mqvpn_rule_proto" ] && break
				mqvpn_rule_port="$(echo "$vps_config" | jsonfilter -q -e "@.mqvpn.reorder_rules[$mqvpn_rule_idx].port")"
				mqvpn_rule_profile="$(echo "$vps_config" | jsonfilter -q -e "@.mqvpn.reorder_rules[$mqvpn_rule_idx].profile")"
				uci -q add mqvpn reorder_rule >/dev/null
				uci -q set "mqvpn.@reorder_rule[-1].proto=$mqvpn_rule_proto"
				[ -n "$mqvpn_rule_port" ] && uci -q set "mqvpn.@reorder_rule[-1].port=$mqvpn_rule_port"
				[ -n "$mqvpn_rule_profile" ] && uci -q set "mqvpn.@reorder_rule[-1].profile=$mqvpn_rule_profile"
				mqvpn_rule_idx=$((mqvpn_rule_idx + 1))
			done
		fi
		if [ -n "$(uci -q changes mqvpn)" ]; then
			uci -q commit mqvpn
			logger -t "OMR-VPS" "MQVPN restart..."
			/etc/init.d/mqvpn restart
		fi
	fi

	# SoftEtherVPN settings
	softethervpn_password="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.password')"
	softethervpn_port="$(echo "$vps_config" | jsonfilter -q -e '@.softethervpn.port')"
	if [ -f /etc/init.d/softethervpnclient ] && [ -n "$(uci -q get softethervpn.openmptcprouter)" ] && ( ([ -n "$softethervpn_password" ] && [ "$softethervpn_password" != "$(uci -q get softethervpn.openmptcprouter.password)" ]) || ([ -n "$softethervpn_port" ] && [ "$softethervpn_port" != "$(uci -q get softethervpn.openmptcprouter.port)" ]) ); then
		softethervpn_state=0
		vpn="$(uci -q get openmptcprouter.settings.vpn)"
		# The wizard stores "softether" (the VPS API's name), older
		# configs "softethervpn"
		if [ "$vpn" = "softethervpn" ] || [ "$vpn" = "softether" ]; then
			softethervpn_state=1
		fi
		uci -q batch <<-EOF >/dev/null
			$([ -n "$softethervpn_password" ] && echo "set softethervpn.openmptcprouter.password=$softethervpn_password")
			set softethervpn.openmptcprouter.enable=$softethervpn_state
			$([ -n "$softethervpn_port" ] && echo "set softethervpn.openmptcprouter.port=$softethervpn_port")
		EOF
		uci -q set softethervpn.openmptcprouter.host="$vpsip"
		if [ -n "$(uci -q changes softethervpn)" ]; then
			uci -q commit softethervpn
			logger -t "OMR-VPS" "SoftEther VPN restart..."
			/etc/init.d/softethervpnclient restart
		fi
	fi

	# Shorewall settings
	# An empty value is no setting: "uci set" with one deletes the option
	shorewall_redirect="$(echo "$vps_config" | jsonfilter -q -e '@.shorewall.redirect_ports')"
	redirect=""
	[ "$shorewall_redirect" = "enable" ] && redirect="1"
	[ "$shorewall_redirect" = "disable" ] && redirect="0"
	[ -n "$redirect" ] && uci -q set openmptcprouter.${servername}.redirect_ports=$redirect

	# Wireguard settings
	wireguard_key="$(echo "$vps_config" | jsonfilter -q -e '@.wireguard.key')"
	[ -n "$wireguard_key" ] && uci -q set openmptcprouter.${servername}.wgkey=$wireguard_key

	# MPTCP settings
	mptcp_path_manager="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.path_manager')"
	mptcp_scheduler="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.scheduler')"
	mptcp_checksum="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.checksum')"
	mptcp_syn_retries="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.syn_retries')"
	mptcp_enabled="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.enabled')"
	if [ "$mptcp_enabled" = "0" ]; then
		mptcp_enabled="disable"
	else
		mptcp_enabled="enable"
	fi
	congestion="$(echo "$vps_config" | jsonfilter -q -e '@.network.congestion_control')"
	uci -q batch <<-EOF >/dev/null
		set network.globals.multipath=$mptcp_enabled
		$([ -n "$mptcp_checksum" ] && echo "set network.globals.mptcp_checksum=$mptcp_checksum")
		$([ -n "$congestion" ] && echo "set network.globals.congestion=$congestion")
		commit network
	EOF
	if [ "$mptcp_scheduler" != "" ] && [ "$mptcp_syn_retries" != "" ]; then
		uci -q batch <<-EOF >/dev/null
			set network.globals.mptcp_scheduler=$mptcp_scheduler
			set network.globals.mptcp_syn_retries=$mptcp_syn_retries
			commit network
		EOF
	fi
	if [ "$mptcp_path_manager" != "" ] && [ "$mptcp_path_manager" != "kernel" ]; then
		uci -q set network.globals.mptcp_path_manager="$mptcp_path_manager"
	fi
	mptcp_close_timeout="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.close_timeout')"
	mptcp_pm_type="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.pm_type')"
	mptcp_stale_loss_cnt="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.stale_loss_cnt')"
	mptcp_syn_retrans_before_tcp_fallback="$(echo "$vps_config" | jsonfilter -q -e '@.mptcp.syn_retrans_before_tcp_fallback')"
	[ -n "$mptcp_close_timeout" ] && [ "$mptcp_close_timeout" != "0" ] && uci -q set network.globals.mptcp_close_timeout="$mptcp_close_timeout"
	[ -n "$mptcp_pm_type" ] && [ "$mptcp_pm_type" != "0" ] && uci -q set network.globals.mptcp_pm_type="$mptcp_pm_type"
	[ -n "$mptcp_stale_loss_cnt" ] && [ "$mptcp_stale_loss_cnt" != "0" ] && uci -q set network.globals.mptcp_stale_loss_cnt="$mptcp_stale_loss_cnt"
	[ -n "$mptcp_syn_retrans_before_tcp_fallback" ] && [ "$mptcp_syn_retrans_before_tcp_fallback" != "0" ] && uci -q set network.globals.mptcp_syn_retrans_before_tcp_fallback="$mptcp_syn_retrans_before_tcp_fallback"
	if [ -n "$(uci -q changes network)" ]; then
		uci -q commit network
	fi

	# Check if server get an IPv6, if not disable IPv6 on OMR
	vps_ipv6_addr="$(echo "$vps_config" | jsonfilter -q -e '@.network.ipv6')"
	if [ -z "$vps_ipv6_addr" ]; then
		if [ "$(uci -q get openmptcprouter.settings.disable_ipv6)" = "0" ]; then
			logger -t "OMR-VPS" "No IPv6 support on VPS side ?"
		fi
		#uci -q batch <<-EOF >/dev/null
		#	set openmptcprouter.settings.disable_ipv6=1
		#EOF
		#sysctl -qw net.ipv6.conf.all.disable_ipv6=1
	else
		uci -q batch <<-EOF >/dev/null
			set openmptcprouter.${servername}.ipv6=$vps_ipv6_addr
		EOF
	fi

	omr6in4_vps_localip="$(echo "$vps_config" | jsonfilter -q -e '@.ip6in4.localip')"
	omr6in4_vps_remoteip="$(echo "$vps_config" | jsonfilter -q -e '@.ip6in4.remoteip')"
	if ([ -n "$omr6in4_vps_localip" ] && [ "$omr6in4_vps_localip" != "$(uci -q get network.omr6in4.gateway)" ]) || ([ -n "$omr6in4_vps_remoteip" ] && [ "$omr6in4_vps_remoteip" != "$(uci -q get network.omr6in4.ip6addr)" ]); then
		logger -t "OMR-VPS" "Set omr6in4 ip address and gateway"
		uci -q batch <<-EOF >/dev/null
			set network.omr6in4.ip6addr="$omr6in4_vps_remoteip"
			set network.omr6in4.gateway="$omr6in4_vps_localip"
		EOF
		if [ -n "$(uci -q changes network)" ]; then
			uci -q commit network
			/etc/init.d/network reload
			sleep 6
		fi
	fi

	# Get VPS iperf config
	iperf_user="$(echo "$vps_config" | jsonfilter -q -e '@.iperf.user')"
	iperf_pass="$(echo "$vps_config" | jsonfilter -q -e '@.iperf.password')"
	iperf_key="$(echo "$vps_config" | jsonfilter -q -e '@.iperf.key')"
	uci -q batch <<-EOF >/dev/null
		del iperf.${servername}
		set iperf.${servername}=server
		set iperf.${servername}.host=$vpsip
		set iperf.${servername}.ports=65400
		set iperf.${servername}.ipv4=1
		set iperf.${servername}.ipv6=0
		set iperf.${servername}.speed=1000
		set iperf.${servername}.tcp=1
		set iperf.${servername}.udp=1
		set iperf.${servername}.user=$iperf_user
		set iperf.${servername}.password=$iperf_pass
		set iperf.${servername}.key="$iperf_key"
		commit iperf
	EOF

	# Get available server
	available_vpn="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.available' | sed -e 's/\[ //' -e 's/ \]//' -e 's/,//g')"
	available_proxy="$(echo "$vps_config" | jsonfilter -q -e '@.proxy.available' | sed -e 's/\[ //' -e 's/ \]//' -e 's/,//g')"
	uci -q batch <<-EOF >/dev/null
		del openmptcprouter.${servername}.available_vpn
		del openmptcprouter.${servername}.available_proxy
	EOF

	if [ "$user_permission" = "ro" ]; then
		current_vpn="$(echo "$vps_config" | jsonfilter -q -e '@.vpn.current')"
		if [ -n "$current_vpn" ]; then
			uci -q batch <<-EOF >/dev/null
				set openmptcprouter.${servername}.available_vpn=${current_vpn}
			EOF
		fi
		current_proxy="$(echo "$vps_config" | jsonfilter -q -e '@.proxy.current')"
		if [ -n "$current_proxy" ]; then
			uci -q batch <<-EOF >/dev/null
				set openmptcprouter.${servername}.available_proxy=${current_proxy}
			EOF
		fi
	
	else
		if [ -n "$available_vpn" ]; then
			for vpn in $available_vpn; do
				uci -q batch <<-EOF >/dev/null
					add_list openmptcprouter.${servername}.available_vpn=$vpn
				EOF
			done
		fi
		if [ -n "$available_proxy" ]; then
			for proxy in $available_proxy; do
				uci -q batch <<-EOF >/dev/null
					add_list openmptcprouter.${servername}.available_proxy=$proxy
				EOF
			done
		fi
	fi
	uci -q batch <<-EOF >/dev/null
		set openmptcprouter.${servername}.get_config=0
		delete openmptcprouter.settings.forceretrieve
		commit openmptcprouter
	EOF
}

_backup_send() {
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	token=""
	vps_config=""
	_login
	[ -z "$token" ] && {
		_token_failed "$servername" "Can't get ${servername} token, try later"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${servername}.admin_error
			set openmptcprouter.${servername}.admin_error=1
		EOF
		backup_failed=1
		return
	}
	# The archive holds /etc/shadow and every key: private, and gone once sent
	local backup_file backupjson
	backup_file="$(mktemp /tmp/omr-backup.XXXXXX)" || return
	backupjson="$(mktemp /tmp/omr-backupjson.XXXXXX)" || { rm -f "$backup_file"; return; }
	sysupgrade -k -b "$backup_file"
	backup_data="$(base64 "$backup_file" | tr -d '\n')"
	backup_sha256sum="$(sha256sum "$backup_file" | awk '{print $1}')"
	[ -n "$backup_data" ] && {
		logger -t "OMR-VPS" "Send backup file to server $servername"
		echo '{"data": "'$backup_data'","sha256sum": "'$backup_sha256sum'"}' > "$backupjson"
		result=$(_set_json "backuppost" "" "$backupjson")
		if [ "$(_parse_result "$result")" = "done" ]; then
			_backup_state_set "$servername" lastbackup "$(date +%s)"
			backup_sent=1
		else
			logger -t "OMR-VPS" "Server $servername didn't take the backup: $(echo "$result" | cut -c1-200)"
			backup_failed=1
		fi
	}
	rm -f "$backup_file" "$backupjson"
}

backup_send() {
	backup_failed=""
	backup_sent=""
	config_load openmptcprouter
	config_foreach _backup_send server
	[ -n "$backup_sent" ] && [ -z "$backup_failed" ]
}


_backup_get() {
	servername=$1
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	local srvlastbackup="$(uci -q -P /var/state get openmptcprouter.$servername.lastbackup)"
	# the VPS gives the time with its fraction of a second, which -lt refuses
	srvlastbackup="${srvlastbackup%%.*}"
	[ -n "$srvlastbackup" ] || return
	[ -z "$lastbackup" ] || [ "$lastbackup" -lt "$srvlastbackup" ] && {
		lastbackup="$srvlastbackup"
		serverbackup="$servername"
	}
}

_backup_get_and_apply() {
	servername=$1
	backupfile=$2
	token=""
	vps_config=""
	_login
	[ -z "$token" ] && {
		_token_failed "$servername" "Can't get ${servername} token, try later"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${servername}.admin_error
			set openmptcprouter.${servername}.admin_error=1
		EOF
		return 1
	}
	if [ -n "$backupfile" ]; then
		echo "get $backupfile"
		vps_backup=$(_get_json "backupget?filename=${backupfile}")
	else
		vps_backup=$(_get_json "backupget")
	fi
	[ -z "$vps_backup" ] && return 1
	backup_data="$(echo "$vps_backup" | jsonfilter -q -e '@.data')"
	backup_sha256sum="$(echo "$vps_backup" | jsonfilter -q -e '@.sha256sum' | tr 'A-F' 'a-f')"
	[ -n "$backup_data" ] || return
	local backup_file
	backup_file="$(mktemp /tmp/omr-backup.XXXXXX)" || return
	echo $backup_data | base64 -d > "$backup_file" 2>/dev/null
	# sysupgrade -r unpacks over / whatever it is handed: a truncated or
	# damaged download must never get there
	if [ -n "$backup_sha256sum" ]; then
		if [ "$(sha256sum "$backup_file" | awk '{print $1}')" != "$backup_sha256sum" ]; then
			logger -t "OMR-VPS" -p daemon.warn "Backup from server $servername doesn't match its sha256sum, not restored"
			rm -f "$backup_file"
			return 1
		fi
	else
		# Servers older than the checksum in backupget: check the archive
		logger -t "OMR-VPS" "Server $servername sent no backup sha256sum, checking the archive only"
	fi
	if ! tar -tzf "$backup_file" >/dev/null 2>&1; then
		logger -t "OMR-VPS" -p daemon.warn "Backup from server $servername is not a valid archive, not restored"
		rm -f "$backup_file"
		return 1
	fi
	# sysupgrade's status, not rm's: a failed restore reported a success
	local rc=0
	sysupgrade -r "$backup_file" || rc=$?
	rm -f "$backup_file"
	return $rc
}

backup_get() {
	backuptoapply=$2
	if [ -n "$backuptoapply" ]; then
		_backup_get_and_apply $1 $2
	else
		lastbackup=""
		serverbackup=""
		config_load openmptcprouter
		config_foreach _backup_get server
		[ -n "$serverbackup" ] && _backup_get_and_apply $serverbackup
	fi
}

# The backup list is what the VPS holds right now, asked again each time the
# Backup page opens: kept in /var/state, not committed to flash
_backup_state_set() {
	uci -q -P /var/state revert "openmptcprouter.$1.$2"
	uci -q -P /var/state set "openmptcprouter.$1.$2=$3"
}

# _backup_list [<server>]: _config_service runs it for the server it syncs
_backup_list() {
	[ "$(uci -q get openmptcprouter.${1:-$servername}.disabled)" = "1" ] && return
	[ -z "$(uci -q get openmptcprouter.${1:-$servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${1:-$servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${1:-$servername}.port)" ] && return
	# _config_service has the token of the server it syncs, and its config:
	# emptied, each of its next calls fetched /config again
	[ -n "$1" ] && {
		servername=$1
		token=""
		vps_config=""
	}
	_login
	[ -z "$token" ] && {
		_token_failed "$servername" "Can't get ${servername} token, try later"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${servername}.admin_error
			set openmptcprouter.${servername}.admin_error=1
		EOF
		return
	}
	vps_backup=$(_get_json "backuplist")
	[ -z "$vps_backup" ] && return
	backup_lastmodif="$(echo "$vps_backup" | jsonfilter -q -e '@.modif')"
	[ -n "$backup_lastmodif" ] && {
		_backup_state_set "$servername" lastbackup "$backup_lastmodif"
		uci -q -P /var/state revert "openmptcprouter.$servername.allbackup"
		# hides a list an older version committed to the config
		uci -q -P /var/state delete "openmptcprouter.$servername.allbackup"
		echo "$vps_backup" | jsonfilter -q -e '@.sorted[*]' | while read -r backup; do
			name=$(echo "$backup" | jsonfilter -q -e '@[0]' | tr -d '\n')
			modiftime=$(echo "$backup" | jsonfilter -q -e '@[1]' | tr -d '\n')
			uci -q -P /var/state add_list "openmptcprouter.$servername.allbackup=${name}|${modiftime}"
		done
	}
}



backup_list() {
	config_load openmptcprouter
	config_foreach _backup_list server
}


_count_server() {
	local servername=$1
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	serversnb=$((serversnb+1))
}

_get_token() {
	servername=$1
	[ -z "$(uci -q get openmptcprouter.${servername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.port)" ] && return
	[ -z "$(uci -q get openmptcprouter.${servername}.ip)" ] && return
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	token=""
	_login
	if [ -z "$token" ]; then
		reason=""
		test_ping() {
			_ping_server $1
			status=$?
			if $(exit $status); then
				reason="can ping server ${servername} on $1"
			else
				reason="can't ping server ${servername} on $1"
			fi
		}
		config_list_foreach "${servername}" ip test_ping
		port="$(uci -q get openmptcprouter.${servername}.port)"
		api_test() {
			server="$1"
			resolve="$(resolveip -t 5 $server)"
			valid_ip6=$(valid_subnet6 $server)
			if [ "$resolve" != "$server" ] || [ "$valid_ip6" != "ok" ]; then
				if [ "$(omr_vps_curl -s -k -m 3 https://${server}:${port}/)" = "" ]; then
					reason="$reason, no server API answer on $1"
				else
					reason="$reason, server API answer on $1"
				fi
			else
				if [ "$(omr_vps_curl -s -k -m 3 https://[${server}]:${port}/)" = "" ]; then
					reason="$reason, no server API answer on $1"
				else
					reason="$reason, server API answer on $1"
				fi
			fi
		}
		config_list_foreach "${servername}" ip api_test
		[ -e "${OMR_VPS_PIN_STATE}/${servername}.mismatch" ] && reason="$reason, server API certificate doesn't match the pinned public key"
		if [ "$(uci -q get openmptcprouter.${servername}.username)" != "openmptcprouter" ]; then
			reason="$reason, custom username"
		fi
		_token_failed "$servername" "Can't get ${servername} token, try later ($reason)"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${servername}.admin_error
			set openmptcprouter.${servername}.admin_error=1
		EOF
	else
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${servername}.token_error
			set openmptcprouter.${servername}.token_error=0
		EOF
	fi
	echo "$server:$serverport:$token"
}

# _get_token answers "<server>:<port>:<token>". An IPv6 server has colons
# of its own: cut -d: made server "2001", the port "db8" and the token
# empty, so nothing was synced with a VPS reached over IPv6. The port and
# the token have none, so read it from the right.
_split_tokenserver() {
	token="${1##*:}"
	set -- "${1%:*}"
	serverport="${1##*:}"
	server="${1%:*}"
}

# A rebooted VPS comes back with empty DSCP/weight maps (and classify sets),
# while the caches here still say they were sent: nothing was pushed again
# until the set changed. Its boot time (now - uptime) is kept per server; on
# a change, the "already sent" caches of that server are dropped.
_vps_reboot_check() {
	local uptime boot last marker="/tmp/.omr-vps-boot-${servername}"
	uptime="$(echo "$vps_config" | jsonfilter -q -e '@.vps.uptime')"
	uptime="${uptime%%.*}"
	case "$uptime" in
		""|*[!0-9]*) return 0 ;;
	esac
	boot=$(( $(date +%s) - uptime ))
	[ -f "$marker" ] && read -r last < "$marker"
	# a minute of slack: the API answer and this date are not the same instant.
	# Either way: a section pointed at another VPS that booted earlier kept
	# the caches of the first one
	if [ -n "$last" ] && { [ $((boot - last)) -gt 60 ] || [ $((last - boot)) -gt 60 ]; }; then
		logger -t "OMR-VPS" "Server $servername rebooted, its MPTCP pins, weights and classify sets are sent again"
		rm -f "/tmp/.mptcp_dscp_vps_pins-${servername}" "/tmp/.mptcp_dscp_vps_pins-${servername}.retry" \
			"/tmp/.mptcp_weight_vps_pins-${servername}" "/tmp/.mptcp_weight_vps_pins-${servername}.retry" \
			"/tmp/.dscp_classify_vps-${servername}" "/tmp/.dscp_classify_vps_ts-${servername}" \
			/tmp/.omr-vps-sent-"${servername}"-*
	fi
	{ [ -z "$last" ] || [ $((boot - last)) -gt 60 ] || [ $((last - boot)) -gt 60 ]; } && echo "$boot" > "$marker"
	return 0
}

_config_service() {
	servername=$1
	# left by older versions
	! _is_config_server "$servername" && [ "$(uci -q get openmptcprouter.${servername}.get_config)" = "1" ] && \
		uci -q set openmptcprouter.${servername}.get_config=0
	[ "$(uci -q get openmptcprouter.${servername}.disabled)" = "1" ] && return
	vps_config=""
	vpsip=""
	tokenserver=$(_get_token $servername)
	_split_tokenserver "$tokenserver"
	[ -z "$token" ] && return
	error=0
	if [ -n "$serial" ]; then
		[ -z "$vps_config" ] && vps_config=$(_get_json "config?serial=${serial}")
		if [ -n "$vps_config" ] && [ "$( echo "$vps_config" | jsonfilter -q -e '@.error')" = "False serial number" ]; then
			logger -t "OMR-VPS" "Invalid serial number"
			# the address as text and as a whole value, as uci writes it:
			# as a pattern its dots matched any character, it matched inside
			# longer addresses, and an IPv6 address ended the s command
			sed -i "s|'$(echo "$server" | sed 's/[][\.*^$]/\\&/g')'|''|g" /etc/config/*
			return
		fi
	fi
	# "Force retrieve settings" in the wizard (and the post-upgrade
	# zzzzz-omr-update uci-default) only raise a global flag: turn it into a
	# real get_config for the server we actually talk to. Restricted to
	# master/current because get_config is never cleared on the other servers,
	# and a lingering one keeps omr-tracker's "get_config='1'" check true and
	# reruns this service on every tracker cycle.
	[ "$(uci -q get openmptcprouter.settings.forceretrieve)" = "1" ] && _is_config_server "$servername" && uci -q set openmptcprouter.${servername}.get_config=1
	# Every config has the server's kernel. An error answer (a 500, or
	# FastAPI's {"detail": ...}) has none, and applying it would empty the
	# VPN addresses and keys and clear get_config: check it first, and leave
	# get_config set so the next run tries again.
	[ -z "$vps_config" ] && vps_config=$(_get_json "config")
	[ -z "$vps_config" ] && return
	kernel="$(echo "$vps_config" | jsonfilter -q -e '@.vps.kernel')"
	[ -z "$kernel" ] && return
	_vps_reboot_check
	# every server's, not only the config server's (start_service commits)
	_set_wgkey_from_vps && wgkey_changed=1
	user_permission="$(echo "$vps_config" | jsonfilter -q -e '@.user.permission')"
	vps_config_applied=""
	[ "$(uci -q get openmptcprouter.${servername}.get_config)" = "1" ] && _is_config_server "$servername" && {
		_set_config_from_vps
		vps_config_applied="$servername"
		_get_gre_tunnel
		_get_vps_config
	}

#	([ "$(uci -q get openmptcprouter.${servername}.master)" = "1" ] || [ "$(uci -q get openmptcprouter.${servername}.current)" = "1" ]) && {
#		_get_vps_config
#	}

	if [ "$(uci -q get openmptcprouter.settings.firstboot)" != "0" ]; then
		[ -n "$(cat /proc/cpuinfo | grep aes)" ] && {
			vps_aes="$(echo "$vps_config" | jsonfilter -q -e '@.vps.aes')"
			method="$(uci -q get openmptcprouter.settings.encryption)"
			if [ "$vps_aes" != "false" ] && [ "$user_permission" != "ro" ] && [ "$method" != "aes-256-gcm" ]; then
				logger -t "OMR-VPS" "CPU support AES, set it by default"
				uci -q batch <<-EOF >/dev/null
					set openmptcprouter.settings.encryption="aes-256-gcm"
					commit openmptcprouter
					set glorytun.vpn.chacha20="0"
					commit glorytun
					set glorytun-udp.vpn.chacha="0"
					commit glorytun-udp
				EOF
				config_load shadowsocks-libev
				config_foreach _set_ss_server server "method" "aes-256-gcm"
				[ -n "$(uci -q changes shadowsocks-libev)" ] && uci -q commit shadowsocks-libev
				config_load shadowsocks-rust
				config_foreach _set_ssrust_server server "method" "2022-blake3-aes-256-gcm"
				[ -n "$(uci -q changes shadowsocks-rust)" ] && uci -q commit shadowsocks-rust
				[ "$method" != "aes-256-gcm" ] && {
					logger -t "OMR-VPS" "Shadowsocks restart"
					/etc/init.d/shadowsocks-libev restart
					/etc/init.d/shadowsocks-rust restart
				}
			fi
		}
	fi

	if [ "$user_permission" != "ro" ]; then
		#config_load shadowsocks-libev
		#config_foreach _set_ss_server_vps server
		[ -n "$(uci -q get shadowsocks-libev.sss0)" ] && _set_ss_server_vps
		[ -n "$(uci -q get shadowsocks-rust.sss0)" ] && _set_ssgo_server_vps
		[ -n "$(uci -q get v2ray.omr)" ] && _set_v2ray_server_vps
		[ -n "$(uci -q get xray.omr)" ] && _set_xray_server_vps
		([ -n "$(uci -q get glorytun.vpn)" ] || [ -n "$(uci -q get glorytun-udp.vpn)" ]) && [ -z "$(_set_glorytun_vps)" ] && error=1
		[ -n "$(uci -q get openvpn.omr)" ] && [ -z "$(_set_openvpn_vps)" ] && error=1
		[ -n "$(uci -q get mlvpn.general)" ] && [ -z "$(_set_mlvpn_vps)" ] && error=1
		[ -n "$(uci -q get mqvpn.server)" ] && [ -z "$(_set_mqvpn_vps)" ] && error=1
		[ -n "$(uci -q get softethervpn.openmptcprouter)" ] && [ -z "$(_set_softethervpn_vps)" ] && error=1
		_set_vps_firewall
		[ -n "$(uci -q show network | grep wireguard)" ] && [ -z "$(_set_wireguard_vps)" ] && error=1
	fi
	_backup_list
	redirect_port="0"
	if [ "$(uci -q get openmptcprouter.${servername}.redirect_ports)" = "1" ] || [ "$(uci -q get upnpd.config.enabled)" = "1" ]; then
		redirect_port="1"
	fi
	if [ "$user_permission" != "ro" ]; then
		[ -z "$(_set_redirect_ports_from_vps $redirect_port)" ] && error=1
		[ -z "$(_set_mptcp_vps)" ] && error=1
		[ -z "$(_set_vpn_vps)" ] && error=1
		[ -z "$(_set_proxy_vps)" ] && error=1
		( _set_mptcp_pins_vps ) >/dev/null
	fi
	([ -n "$(uci -q get shadowsocks-libev.sss0)" ] || [ -n "$(uci -q get shadowsocks-rust.sss0)" ]) && [ "$(uci -q get shadowsocks-libev.sss0.key)" = "" ] && [ "$(uci -q get shadowsocks-rust.sss0.password)" = "" ] && _want_config
	
	#_set_pihole
	[ -n "$wanips" ] && _set_wan_ip
	_set_vpn_ip
	_is_config_server "$servername" && {
		[ "$user_permission" != "ro" ] && _set_vxlan_vps
		_get_vxlan
		_get_xray_reverse_key
		[ "$(echo "$vps_config" | jsonfilter -q -e '@.pihole.state')" != "true" ] && _pihole_dnsmasq_off
	}
	config_load network
	lanips=""
	config_foreach _get_lan_ip interface
	_set_lan_ip
	_set_sipalg
	_set_bypass_ips
	# The routes go through omrvpn, the tunnel to the config server: a
	# reachable backup's list replaced or deleted the master's on every sync
	_is_config_server "$servername" && {
		if [ "$(uci -q get openmptcprouter.settings.vpn)" != "openvpn" ] && [ "$(echo "$vps_config" | jsonfilter -q -e '@.client2client.enabled')" = "true" ]; then
			_set_client2client 1
		else
			_set_client2client 0
		fi
	}
	if [ "$error" = 0 ]; then
		#logger -t "OMR-VPS" "No errors"
		uci -q set openmptcprouter.${servername}.lastchange=$(date "+%s")
		[ -n "$vps_config" ] && uci -q set openmptcprouter.settings.firstboot=0
	else
		logger -t "OMR-VPS" "Setting server config error, try again"
	fi
	#[ "$error" != 0 ] && logger -t "OMR-VPS" "Error !"
	uci -q -P /var/state batch <<-EOF >/dev/null
		revert openmptcprouter.${servername}.admin_error
		set openmptcprouter.${servername}.admin_error=$error
	EOF
}

_set_pihole_server() {
	pservername=$1
	# _login logs in to $servername: kept from the first server, each
	# server's Pi-hole state was the first one's
	servername=$pservername
	[ -z "$(uci -q get openmptcprouter.${pservername}.username)" ] && return
	[ -z "$(uci -q get openmptcprouter.${pservername}.password)" ] && return
	[ -z "$(uci -q get openmptcprouter.${pservername}.port)" ] && return
	[ "$(uci -q get openmptcprouter.${pservername}.disabled)" = "1" ] && return
	# _get_pihole only knows the VPS address of these VPNs. With any other
	# one dnsmasq never leaves 127.0.0.1#5353, and 003-up asked again on
	# every tracker cycle: a login and a GET /config each time.
	case "$(uci -q get openmptcprouter.settings.vpn)" in
		glorytun_tcp|glorytun_udp|openvpn|mlvpn|dsvpn|mqvpn) ;;
		*) return ;;
	esac
	_pihole_backoff "$pservername" && return
	local dns_before
	token=""
	vps_config=""
	_login
	[ -z "$token" ] && {
		_token_failed "$pservername" "Can't get ${pservername} token, try later"
		uci -q -P /var/state batch <<-EOF >/dev/null
			revert openmptcprouter.${pservername}.admin_error
			set openmptcprouter.${pservername}.admin_error=1
		EOF
		return
	}
	dns_before="$(uci -q get dhcp.@dnsmasq[0].server)"
	_get_pihole $pservername
	if [ "$(uci -q get dhcp.@dnsmasq[0].server)" = "$dns_before" ]; then
		date +%s > "/tmp/.omr-vps-pihole-${pservername}"
	else
		rm -f "/tmp/.omr-vps-pihole-${pservername}"
	fi
}

# 003-up asks for set_pihole on every tracker cycle while dnsmasq is on the
# local resolver and the servers have Pi-hole. When that cannot clear (a
# refused login, no tunnel address in the answer) each cycle was a login
# and a GET /config, and the failed logins got the router banned by the VPS
# fail2ban. Waits 10 minutes after a failed login (until a sync logs in
# again) and 5 minutes after a pass that changed nothing.
_pihole_backoff() { # <server>
	local now first last count seen
	now="$(date +%s)"
	if [ -f "/tmp/.omr-vps-tokenfail-$1" ]; then
		read -r first last count seen < "/tmp/.omr-vps-tokenfail-$1"
		case "$seen" in
			''|*[!0-9]*) ;;
			*) [ "$((now - seen))" -lt 600 ] && return 0 ;;
		esac
	fi
	if [ -f "/tmp/.omr-vps-pihole-$1" ]; then
		read -r last < "/tmp/.omr-vps-pihole-$1"
		case "$last" in
			''|*[!0-9]*) ;;
			*) [ "$((now - last))" -lt 300 ] && return 0 ;;
		esac
	fi
	return 1
}

# _pihole_dns_server <server> [any]
_pihole_dns_server() {
	[ -n "$pihole_dns_server" ] && return
	[ -z "$(uci -q get openmptcprouter.$1.username)" ] && return
	[ "$(uci -q get openmptcprouter.$1.disabled)" = "1" ] && return
	{ [ "$2" = "any" ] || _is_config_server "$1"; } && pihole_dns_server=$1
}

set_pihole() {
	config_load openmptcprouter
	# the master or current server, or else the first one, as before
	pihole_dns_server=""
	config_foreach _pihole_dns_server server
	[ -z "$pihole_dns_server" ] && config_foreach _pihole_dns_server server any
	config_foreach _set_pihole_server server
}

token() {
	config_load openmptcprouter
	config_foreach _get_token server
}

_fix_server() {
	uci -q set openmptcprouter.$1=server
}

start_service() {
	serversnb=0
	wanips=""
	wgkey_changed=""
	serial=$(cat /proc/cpuinfo | grep Serial | awk '{print $3}')
	[ -z "$serial" ] && [ -f "/usr/sbin/dmidecode" ] && serial=$(/usr/sbin/dmidecode -t 1 | egrep 'Serial' | awk '{print $3}')
	uci -q set openmptcprouter.settings.serial=${serial}
	config_load openmptcprouter
	config_foreach _fix_server openmptcprouter
	config_foreach _count_server server
	config_foreach _get_local_wan_ip interface
	config_foreach _config_service server

	uci -q batch <<-EOF >/dev/null
		commit openmptcprouter
	EOF
	# A server key learned or changed here (the first sync, a reinstalled
	# VPS) only reaches the WireGuard peers of MPTCP over VPN when
	# mptcpovervpn runs: they had none until the next network change.
	# In the background: it reloads the network.
	[ -n "$wgkey_changed" ] && [ "$(uci -q get openmptcprouter.settings.mptcpovervpn)" = "wireguard" ] && \
		( /etc/init.d/mptcpovervpn restart >/dev/null 2>&1 </dev/null & )
	# [s]: the grep matched itself, so it was never restarted. Only with a
	# package list left to reinstall.
	if [ -f /etc/backup/installed_packages.txt ] && ! ps w | grep -q '[s]ysupgrade'; then
		/etc/init.d/sysupgrade restart
	fi
}

service_triggers() {
	#procd_add_reload_trigger openmptcprouter network shadowsocks-libev v2ray glorytun glorytun-udp mlvpn openvpn dsvpn
	procd_add_reload_trigger shadowsocks-libev shadowsocks-rust v2ray xray glorytun glorytun-udp mlvpn dsvpn mqvpn
	procd_add_config_trigger "config.change" "firewall" /etc/init.d/openmptcprouter-vps set_vps_firewall
	procd_add_config_trigger "config.change" "network" /etc/init.d/openmptcprouter-vps set_mptcp_pins_vps
	#procd_add_config_trigger "config.change" "network" /etc/init.d/openmptcprouter-vps set_gre_tunnel
	#procd_add_reload_trigger openmptcprouter shadowsocks-libev network upnpd
}
