#!/bin/sh
#
# 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.
#
# Pushes this WAN's upload-side DSCP pins (network's anonymous
# `config dscp_pin` sections, upload_interface field -- see
# mptcp-dscp-manager's 041-multipath-dscp hook and luci-app-mptcp's
# DSCP/Weight Routing page) into mqvpn as a per-path DSCP mask via the
# control API's set_path_dscp_mask, mirroring how 006-mqvpn-weight pushes
# multipath_weight via set_path_weight.
#
# mqvpn has no equivalent of MPTCP's independent dscp_remote_id map: a
# dscp_pin row's download_interface is NOT applied here at all, only
# upload_interface. The mqvpn control API's own "sync_path_labels"
# setting (when enabled) announces this path's mask to the server so it
# adopts the same association for its own downlink scheduling -- i.e.
# mqvpn's download side always *mirrors* whichever WAN carries the class
# on upload, it cannot be pinned to a different WAN the way MPTCP's
# bpf_dscp scheduler can. This is a protocol limitation, not something
# this hook chooses -- if that ever changes, revisit before assuming
# download_interface can be wired up here too.

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

if [ "$(uci -q get mqvpn.settings.enable)" != "1" ]; then
	exit 0
fi

if [ -z "$OMR_TRACKER_DEVICE" ] || [ -z "$OMR_TRACKER_INTERFACE" ]; then
	exit 0
fi

# Don't manage VPN interfaces
if [ "$(uci -q get openmptcprouter.$OMR_TRACKER_INTERFACE.vpn)" = "1" ]; then
	exit 0
fi

if [ "$(pgrep -f mqvpn)" = "" ]; then
	exit 0
fi

multipath_config=$(uci -q get "openmptcprouter.$OMR_TRACKER_INTERFACE.multipath")
[ -z "$multipath_config" ] && multipath_config=$(uci -q get "network.$OMR_TRACKER_INTERFACE.multipath" || echo "off")

if [ "$multipath_config" != "on" ] && [ "$multipath_config" != "master" ] && [ "$multipath_config" != "backup" ]; then
	exit 0
fi

# Mirrors mptcp-scheduler-dscp.sh's dscp_to_val -- deliberately duplicated
# rather than shelling out to that script, so this hook doesn't gain a
# dependency on mptcp-dscp-manager being installed (mqvpn's DSCP masking
# is independent of the MPTCP bpf_dscp scheduler).
_dscp_to_val() {
	case "$1" in
		cs0) echo 0 ;;
		cs1) echo 8 ;;
		cs2) echo 16 ;;
		cs3) echo 24 ;;
		cs4) echo 32 ;;
		cs5) echo 40 ;;
		cs6) echo 48 ;;
		cs7) echo 56 ;;
		le) echo 1 ;;
		af11) echo 10 ;;
		af12) echo 12 ;;
		af13) echo 14 ;;
		af21) echo 18 ;;
		af22) echo 20 ;;
		af23) echo 22 ;;
		af31) echo 26 ;;
		af32) echo 28 ;;
		af33) echo 30 ;;
		af41) echo 34 ;;
		af42) echo 36 ;;
		af43) echo 38 ;;
		ef) echo 46 ;;
		''|*[!0-9]*) echo "" ;;
		*) [ "$1" -ge 0 ] 2>/dev/null && [ "$1" -le 63 ] 2>/dev/null && echo "$1" || echo "" ;;
	esac
}

# Bitmask over the 64 DSCP codepoints (bit N set == this path carries
# class N on upload): one bit per row in network's dscp_pin sections
# whose upload_interface names this interface.
mask=0
for section in $(uci -q show network 2>/dev/null | sed -n "s/^network\.\([^.=]*\)=dscp_pin\$/\1/p"); do
	[ "$(uci -q get network.${section}.upload_interface)" = "$OMR_TRACKER_INTERFACE" ] || continue
	dscp="$(uci -q get network.${section}.dscp)"
	val="$(_dscp_to_val "$dscp")"
	[ -z "$val" ] && continue
	mask=$((mask | (1 << val)))
done
# Bit 63 (DSCP 63) makes the shell's signed 64-bit value negative: mqvpn
# takes the mask as an unsigned JSON number
[ "$mask" -lt 0 ] && mask="$(printf '%u' "$mask")"

# openmptcprouter.<iface>.mqvpn_dscp_mask is the mask last applied to this
# path, as "<mqvpn pid>:<mask>", in /var/state -- same reasoning as
# 006-mqvpn-weight's mqvpn_weight cache: mqvpn holds the mask in RAM only,
# so a reboot, an mqvpn restart (new pid) or a path removal (005 clears
# it) must make the mask be pushed again. Without a cache the path has
# mqvpn's default mask, 0, so a 0 mask needs no push.
mqvpn_pid="$(pidof mqvpn 2>/dev/null)"
mqvpn_pid="${mqvpn_pid%% *}"
current_mask="$(omr_state_get "openmptcprouter.$OMR_TRACKER_INTERFACE.mqvpn_dscp_mask")"
[ -z "$current_mask" ] && current_mask="${mqvpn_pid}:0"

# A failure is logged once, as in 006-mqvpn-weight
if [ "${mqvpn_pid}:${mask}" != "$current_mask" ]; then
	failed_mask="$(omr_state_get "openmptcprouter.$OMR_TRACKER_INTERFACE.mqvpn_dscp_mask_failed")"
	[ "$failed_mask" = "${mqvpn_pid}:${mask}" ] || _log "mqvpn: set DSCP mask $OMR_TRACKER_DEVICE ($OMR_TRACKER_INTERFACE) -> $mask"
	if mqvpn-path dscp "$OMR_TRACKER_DEVICE" "$mask" >/dev/null 2>&1; then
		omr_state_set openmptcprouter "$OMR_TRACKER_INTERFACE" mqvpn_dscp_mask "${mqvpn_pid}:${mask}"
		[ -n "$failed_mask" ] && omr_state_del openmptcprouter "$OMR_TRACKER_INTERFACE" mqvpn_dscp_mask_failed
	elif [ "$failed_mask" != "${mqvpn_pid}:${mask}" ]; then
		_log "mqvpn: failed to set DSCP mask $OMR_TRACKER_DEVICE ($OMR_TRACKER_INTERFACE) -> $mask"
		omr_state_set openmptcprouter "$OMR_TRACKER_INTERFACE" mqvpn_dscp_mask_failed "${mqvpn_pid}:${mask}"
	fi
fi

exit 0
