#!/bin/sh /etc/rc.common
#
# Copyright (C) 2026 Ycarus (Yannick Chabanois) <ycarus@zugaina.org> for OpenMPTCProuter
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#

START=90
STOP=10

USE_PROCD=1

CONF=/var/run/mqvpn.conf

# The key /etc/config/mqvpn shipped until it shipped none: identical on every
# OMR router until openmptcprouter-vps overwrote it with the one the VPS API
# reports, and still there on an upgraded router whose sync never completed.
# Refusing to start on it is the counterpart of the 203.0.113.1 placeholder-IP
# check.
PLACEHOLDER_KEY='mPyVpoQWcp/5gr404xvS19aRC03o0XS2mrb2tZJ1Ii4='

_bool() { [ "$1" = "1" ] && echo "true" || echo "false"; }

generate_config() {
	local enable address ip port server_address server_name insecure pinned_pubkey key user
	local tun_name log_level mtu kill_switch reconnect reconnect_interval route_via_server no_routes
	local status_interval
	local path_mtu path_mtu_ok=1 pmtud
	local cipher
	local control_port control_addr
	local scheduler cc auto_wan reinjection_control reinjection_mode fec_enable fec_scheme sync_path_labels init_max_path_id
	local redundant redundant_ports
	local reorder_enabled reorder_max_wait_ms reorder_cap_packets
	local recv_rate_limit tun_offload tun_gro
	local dns_csv

	config_get_bool enable settings enable 0
	[ "$enable" = "0" ] && { logger -t mqvpn "mqvpn is disabled"; return 1; }

	config_get ip server ip
	config_get port server port
	config_get address server address
	config_get server_name server server_name ''
	config_get_bool insecure server insecure 1
	config_get pinned_pubkey server pinned_pubkey ''

	# Backward compatibility: support old single address option while migrating.
	if [ -z "$ip" ] && [ -n "$address" ]; then
		case "$address" in
			\[*\]:*)
				ip="${address%\]:*}"
				ip="${ip#[}"
				[ -z "$port" ] && port="${address##*\]:}"
				;;
			*:* )
				if echo "${address##*:}" | grep -Eq '^[0-9]+$'; then
					ip="${address%:*}"
					[ -z "$port" ] && port="${address##*:}"
				else
					ip="$address"
				fi
				;;
			*)
				ip="$address"
				;;
		esac
	fi

	[ -n "$ip" ] && ip="${ip#[}"
	[ -n "$ip" ] && ip="${ip%]}"
	[ -z "$port" ] && port='443'

	server_address="$ip:$port"
	case "$ip" in
		*:* ) server_address="[$ip]:$port" ;;
	esac

	config_get cipher tls cipher ''

	config_get key auth key
	config_get user auth user

	config_get tun_name interface tun_name 'mqvpn0'
	config_get log_level interface log_level 'info'
	config_get mtu interface mtu ''
	config_get_bool kill_switch interface kill_switch 0
	config_get_bool reconnect interface reconnect 1
	config_get reconnect_interval interface reconnect_interval 5
	# mqvpn logs a [STATUS] block (one line plus one per path) every 30s by
	# default, which crowds logread; LuCI and the control API have the same data
	config_get status_interval interface status_interval 300
	case "$status_interval" in
		''|*[!0-9]*) status_interval=300 ;;
	esac
	config_get_bool route_via_server interface route_via_server 0
	config_get_bool no_routes interface no_routes 1
	config_get path_mtu interface path_mtu ''
	config_get_bool pmtud interface pmtud 0

	config_get control_port control control_port ''
	config_get control_addr control control_addr '127.0.0.1'

	config_get scheduler multipath scheduler 'minrtt'
	config_get cc multipath cc ''
	config_get_bool auto_wan multipath auto_wan 1
	config_get_bool reinjection_control multipath reinjection_control 0
	config_get reinjection_mode multipath reinjection_mode ''
	config_get_bool fec_enable multipath fec_enable 0
	config_get fec_scheme multipath fec_scheme ''
	config_get_bool sync_path_labels multipath sync_path_labels 1
	config_get init_max_path_id multipath init_max_path_id ''
	config_get_bool redundant multipath redundant 0
	config_get redundant_ports multipath redundant_ports ''

	config_get_bool reorder_enabled reorder enabled 0
	config_get reorder_max_wait_ms reorder max_wait_ms ''
	config_get reorder_cap_packets reorder cap_packets ''

	config_get recv_rate_limit advanced recv_rate_limit ''
	config_get_bool tun_offload advanced tun_offload 1
	config_get_bool tun_gro advanced tun_gro 1

	dns_csv=''
	config_list_foreach interface dns _append_dns_csv

	[ -z "$ip" ] && { logger -t mqvpn "No server address configured"; return 1; }
	[ "$ip" = "203.0.113.1" ] && { logger -t mqvpn "Server IP is not configured (placeholder 203.0.113.1)"; return 1; }
	[ -z "$key" ]     && { logger -t mqvpn "No auth key configured"; return 1; }
	# A router whose VPS config sync never completed still holds the shipped
	# placeholder key, so mqvpn would connect, get a 403 on every CONNECT-IP and
	# never build its tun -- with nothing in the log at the default level to say
	# why (issue 4367). The same incomplete sync leaves shadowsocks-rust server
	# sections without a password, which crash-loops their instances (issue 3898),
	# so this is usually not the only symptom.
	[ "$key" = "$PLACEHOLDER_KEY" ] && { logger -t mqvpn "Auth key is still the placeholder shipped in /etc/config/mqvpn: the VPS configuration has not been retrieved yet, so the server would reject every tunnel with 403. Not starting. Re-run the setup wizard or check the VPS API (logread -e OMR-VPS)."; return 1; }
	# The pin goes verbatim into an INI line: anything outside base64, the
	# sha256// prefix and the ';' list separator (a newline above all) is
	# refused rather than written. mqvpn checks the pins themselves.
	case "$pinned_pubkey" in
		*[!A-Za-z0-9+/=\;\ ]*) logger -t mqvpn "pinned_pubkey '$pinned_pubkey' is not a base64 SHA-256 pin list. Not starting."; return 1 ;;
	esac
	# Same for the port list, written only with Redundant on; mqvpn checks
	# the ports and their number, and stops on a bad list rather than copy
	# all UDP.
	if [ "$redundant" = "1" ]; then
		case "$redundant_ports" in
			*[!0-9,\ ]*) logger -t mqvpn "redundant_ports '$redundant_ports' is not a comma-separated port list. Not starting."; return 1 ;;
		esac
	fi
	# mqvpn refuses the whole config on a PathMTU other than 0 or 1280-9000.
	# Leading zeros ("01500", "00", set by hand or by an older LuCI) are
	# dropped rather than refused: refusing kept the tunnel down.
	while :; do
		case "$path_mtu" in
			0?*) path_mtu="${path_mtu#0}" ;;
			*) break ;;
		esac
	done
	case "$path_mtu" in
		''|0) ;;
		*[!0-9]*|?????*) path_mtu_ok=0 ;;
		*) [ "$path_mtu" -ge 1280 ] && [ "$path_mtu" -le 9000 ] || path_mtu_ok=0 ;;
	esac
	[ "$path_mtu_ok" = "1" ] || { logger -t mqvpn "path_mtu '$path_mtu' is not 0 or 1280-9000. Not starting."; return 1; }

	mkdir -p /var/run
	# The config holds the auth key: created 0600, the redirection below
	# then keeps that mode
	rm -f "$CONF"
	( umask 077; : > "$CONF" )

	{
		echo '[Server]'
		echo "Address = $server_address"
		[ -n "$server_name" ] && echo "ServerName = $server_name"
		# A pin authenticates the server by its key, so a self-signed one
		# needs no Insecure, which lets any server get the auth key
		# (GHSA-qq6x-5r9f-2w3m). mqvpn would ignore Insecure next to a pin
		# anyway; it is left out so the generated config says what applies.
		if [ -n "$pinned_pubkey" ]; then
			echo "PinnedPubkey = $pinned_pubkey"
		elif [ "$insecure" = "1" ]; then
			echo "Insecure = true"
		fi
		if [ -n "$cipher" ]; then
			echo ''
			echo '[TLS]'
			echo "Cipher = $cipher"
		fi
		echo ''
		echo '[Auth]'
		echo "Key = $key"
		[ -n "$user" ] && echo "Username = $user"
		echo ''
		echo '[Interface]'
		echo "TunName = $tun_name"
		echo "LogLevel = $log_level"
		[ -n "$dns_csv" ] && echo "DNS = $dns_csv"
		[ -n "$mtu" ] && echo "MTU = $mtu"
		[ -n "$path_mtu" ] && [ "$path_mtu" != "0" ] && echo "PathMTU = $path_mtu"
		echo "PMTUD = $(_bool "$pmtud")"
		echo "KillSwitch = $(_bool "$kill_switch")"
		echo "Reconnect = $(_bool "$reconnect")"
		echo "ReconnectInterval = $reconnect_interval"
		echo "StatusInterval = $status_interval"
		echo "RouteViaServer = $(_bool "$route_via_server")"
		echo "NoRoutes = $(_bool "$no_routes")"

		if [ -n "$control_port" ]; then
			echo ''
			echo '[Control]'
			echo "Port = $control_port"
			echo "Addr = $control_addr"
		fi

		echo ''
		echo '[Multipath]'
		echo "Scheduler = $scheduler"
		[ -n "$cc" ] && echo "CC = $cc"
		[ "$reinjection_control" = "1" ] && echo "ReinjectionControl = true"
		[ -n "$reinjection_mode" ] && echo "ReinjectionMode = $reinjection_mode"
		[ "$fec_enable" = "1" ] && echo "FecEnable = true"
		[ -n "$fec_scheme" ] && echo "FecScheme = $fec_scheme"
		echo "SyncPathLabels = $(_bool "$sync_path_labels")"
		[ -n "$init_max_path_id" ] && echo "InitMaxPathId = $init_max_path_id"
		if [ "$redundant" = "1" ]; then
			echo "Redundant = on"
			[ -n "$redundant_ports" ] && echo "RedundantPorts = $redundant_ports"
		fi

		if [ "$auto_wan" = "1" ]; then
			config_load network
			config_foreach _write_iface_path interface
		else
			config_list_foreach multipath path _write_explicit_path ""
			config_list_foreach multipath backup_path _write_explicit_path "backup"
		fi

		if [ "$reorder_enabled" = "1" ]; then
			echo ''
			echo '[Reorder]'
			echo 'Enabled = on'
			[ -n "$reorder_max_wait_ms" ] && echo "MaxWaitMs = $reorder_max_wait_ms"
			[ -n "$reorder_cap_packets" ] && echo "CapPackets = $reorder_cap_packets"
		fi

		config_load mqvpn
		config_foreach _write_reorder_rule reorder_rule

		echo ''
		echo '[Advanced]'
		[ -n "$recv_rate_limit" ] && [ "$recv_rate_limit" != "0" ] && \
			echo "RecvRateLimit = $recv_rate_limit"
		echo "TunOffload = $(_bool "$tun_offload")"
		echo "TunGro = $(_bool "$tun_gro")"
	} > "$CONF"

	return 0
}

_append_dns_csv() {
	local dns="$1"
	[ -z "$dns" ] && return 0
	if [ -n "$dns_csv" ]; then
		dns_csv="$dns_csv, $dns"
	else
		dns_csv="$dns"
	fi
}

_write_iface_path() {
	local config="$1"
	local mode ifname

	config_get mode "$config" multipath ""
	[ "$mode" = "" ] && mode="$(uci -q get openmptcprouter."$config".multipath)"
	[ "$mode" = "on" ] || [ "$mode" = "master" ] || [ "$mode" = "backup" ] || return 0

	# Every multipath WAN, not only those a tracker marked up: at boot none
	# is yet, and with no Path line mqvpn opens one path bound to no device,
	# which follows the route to the VPS and keeps sending into a failed WAN
	# until that route moves. A WAN a tracker found down is left out: no
	# transition removes its path, 005-mqvpn-path adds it when it recovers.
	local state
	state="$(uci -q -P /var/state get openmptcprouter."$config".state)"
	[ "$state" = "down" ] && return 0

	ifname=$(ifstatus "$config" 2>/dev/null | jsonfilter -q -e '@["l3_device"]')
	[ -n "$(echo "$ifname" | grep '@')" ] && ifname=$(ifstatus "$config" 2>/dev/null | jsonfilter -q -e '@["device"]')
	# No l3_device before netifd brings the interface up. The device is the
	# L3 one only for these protos: a PPPoE or modem WAN runs on another.
	if [ -z "$ifname" ]; then
		case "$(uci -q get network."$config".proto)" in
			static|dhcp|dhcpv6)
				ifname=$(ifstatus "$config" 2>/dev/null | jsonfilter -q -e '@["device"]')
				[ -z "$ifname" ] && ifname="$(uci -q get network."$config".device)"
				;;
		esac
	fi
	# mqvpn does not start at all when a path device does not exist
	[ -n "$ifname" ] && [ -d "/sys/class/net/$ifname" ] || return 0

	if [ "$mode" = "backup" ]; then
		echo "BackupPath = $ifname"
	else
		echo "Path = $ifname"
	fi
}

_write_explicit_path() {
	local ifname="$1"
	local is_backup="$2"
	[ -z "$ifname" ] && return 0
	if [ "$is_backup" = "backup" ]; then
		echo "BackupPath = $ifname"
	else
		echo "Path = $ifname"
	fi
}

_write_reorder_rule() {
	local config="$1"
	local proto port profile
	config_get proto "$config" proto ''
	config_get port "$config" port ''
	config_get profile "$config" profile ''
	[ -z "$proto" ] && return 0
	echo ''
	echo '[ReorderRule]'
	echo "Proto = $proto"
	[ -n "$port" ] && echo "Port = $port"
	[ -n "$profile" ] && echo "Profile = $profile"
}

_drop_static_tun_mtu() {
	local dev="$1" sec mtu changed=0
	[ -n "$dev" ] || return 0
	for sec in $(uci -q show network 2>/dev/null | sed -n "s/^network\.\([^.]*\)\.name='\{0,1\}${dev}'\{0,1\}\$/\1/p"); do
		[ "$(uci -q get network.$sec)" = "device" ] || continue
		mtu="$(uci -q get network.$sec.mtu)"
		[ -n "$mtu" ] || continue
		logger -t mqvpn "Removing static MTU $mtu from netifd device $dev (network.$sec): mqvpn negotiates and sets the tunnel MTU itself"
		uci -q delete network.$sec.mtu
		changed=1
	done
	[ "$changed" = "1" ] || return 0
	uci -q commit network
	ubus -t 5 call network reload >/dev/null 2>&1
	return 0
}

start_service() {
	config_load mqvpn

	generate_config || return 1

	local tun_name
	config_get tun_name interface tun_name 'mqvpn0'
	_drop_static_tun_mtu "$tun_name"

	procd_open_instance
	procd_set_param command /usr/sbin/mqvpn --config "$CONF"
	# The command line never changes: without the file, a reload with new
	# settings leaves the running instance as it is
	procd_set_param file "$CONF"
	procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}"
	procd_set_param stderr 1
	procd_set_param stdout 1
	procd_close_instance
}

stop_service() {
	rm -f "$CONF"
}

service_triggers() {
	procd_add_reload_trigger mqvpn
}
