#!/bin/sh
#
# Update the MPTCP flags with or without the pached iproute2
#
# Author: Mario Krueger <openwrt at xedp3x.de>
#	  Ycarus (Yannick Chabanois) for OpenMPTCProuter <contact@openmptcprouter.com>
# Released under GPL 3 or later

if [ -d "/proc/sys/net/mptcp" ]; then
        # read, not `cat` in two subshells: this runs on every call
        if { [ -f /proc/sys/net/mptcp/mptcp_enabled ] && read -r mptcp_enabled < /proc/sys/net/mptcp/mptcp_enabled && [ "$mptcp_enabled" = 0 ]; } || { [ -f /proc/sys/net/mptcp/enabled ] && read -r mptcp_enabled < /proc/sys/net/mptcp/enabled && [ "$mptcp_enabled" = 0 ]; }; then
                echo "MPTCP is disabled!"
                exit 1
        fi
else
        echo "Your device don't support multipath-TCP."
        exit 1
fi

# From an "ip mptcp endpoint show" listing ($1), set id_used when an endpoint
# has the id $2 and id_holder to its device, and dev_ids to the endpoint ids
# of device $3. Lines are "ADDR [port P] id ID [FLAGS...] [dev DEV]".
endpoint_ids() {
	local listing="$1" want="$2" dev="$3" ep_line ep_id ep_dev
	id_used=""
	id_holder=""
	dev_ids=""
	while IFS= read -r ep_line; do
		ep_id=""
		ep_dev=""
		set -- $ep_line
		while [ "$#" -gt 1 ]; do
			case "$1" in
				id) ep_id="$2" ;;
				dev) ep_dev="$2" ;;
			esac
			shift
		done
		[ -n "$ep_id" ] || continue
		[ "$ep_id" = "$want" ] && {
			id_used=1
			id_holder="$ep_dev"
		}
		[ -n "$dev" ] && [ "$ep_dev" = "$dev" ] && dev_ids="$dev_ids $ep_id"
	done <<EOF
$listing
EOF
}

# Add an endpoint with the flags $@ for each global address ($IP) of $DEVICE.
# The explicit id ($ENDPOINT_ID, the WAN's metric) goes to the first address,
# the IPv4 one on a dual-stack WAN, the one the VPS pins a WAN by. Each
# endpoint needs its own id: with the same one for all, the IPv6 add failed
# ("Resource busy") and IPv6 was left out of MPTCP. The next addresses get
# id+128 then id+192, above the WAN metrics so that they never take the id
# of another WAN, and an address left without one is not added at all: an
# id picked by the kernel would be one of those WAN ids.
# When another device still holds the id of the first address, it is added
# with an id picked by the kernel instead of failing on every tracker check,
# and "multipath -i" tells the callers to fix it once the id is free.
# The in-kernel path manager takes 8 endpoints at most (MPTCP_PM_ADDR_MAX;
# beyond: "Result not representable"). Added WAN by WAN as they come up, the
# extra IPv6 endpoints of the first WANs could take every slot and leave a
# later WAN out of MPTCP, with nothing logged. When full, the first endpoint
# of a WAN takes the slot of another WAN's extra one (id 128 and up), extra
# endpoints only use a free slot, and a refused add is logged.
MPTCP_ENDPOINTS_MAX=8

# The id of the endpoint whose address is $2 in the listing $1. The address
# is compared as the whole first field: "grep -F '1.2.3.4 '" also matched
# 11.2.3.4, the endpoint of another WAN, which was then deleted.
endpoint_id_of_addr() {
	printf '%s\n' "$1" | awk -v a="$2" '$1 == a {
		for (i = 2; i < NF; i++) if ($i == "id") { print $(i + 1); exit }
	}'
}

# Whether the device $2 has an endpoint with an id below $3 in the listing $1
_dev_has_lower_id() {
	local listing="$1" dev="$2" below="$3" ep_line ep_id ep_dev
	[ -n "$dev" ] || return 1
	while IFS= read -r ep_line; do
		ep_id=""
		ep_dev=""
		set -- $ep_line
		while [ "$#" -gt 1 ]; do
			case "$1" in
				id) ep_id="$2" ;;
				dev) ep_dev="$2" ;;
			esac
			shift
		done
		[ "$ep_dev" = "$dev" ] && [ -n "$ep_id" ] && [ "$ep_id" -lt "$below" ] && return 0
	done <<EOF
$listing
EOF
	return 1
}

# Return 0 when a slot is free, or was freed by removing the extra endpoint
# with the highest id of another device. An id of 128 or more alone does not
# make an extra endpoint: one the kernel picked (no id given, or the WAN's id
# was held by another device) can be the only endpoint of a WAN, which was
# then taken out of MPTCP. The device must keep an endpoint with a lower id.
_free_endpoint_slot() {
	local line ep_addr ep_id ep_dev victim_id="" victim_addr="" victim_dev="" count=0 listing
	listing="$(ip mptcp endpoint show)"
	while IFS= read -r line; do
		[ -n "$line" ] || continue
		count=$((count + 1))
		set -- $line
		ep_addr="$1"
		ep_id=""
		ep_dev=""
		while [ "$#" -gt 1 ]; do
			case "$1" in
				id) ep_id="$2" ;;
				dev) ep_dev="$2" ;;
			esac
			shift
		done
		[ -n "$ep_id" ] && [ "$ep_id" -ge 128 ] && [ "$ep_dev" != "$DEVICE" ] || continue
		[ -z "$victim_id" ] || [ "$ep_id" -gt "$victim_id" ] || continue
		_dev_has_lower_id "$listing" "$ep_dev" "$ep_id" || continue
		victim_id="$ep_id"
		victim_addr="$ep_addr"
		victim_dev="$ep_dev"
	done <<EOF
$listing
EOF
	[ "$count" -lt "$MPTCP_ENDPOINTS_MAX" ] && return 0
	[ -n "$victim_id" ] || return 1
	ip mptcp endpoint delete id "$victim_id" >/dev/null 2>&1 || return 1
	logger -t "MPTCP" "Endpoint limit ($MPTCP_ENDPOINTS_MAX) reached: $victim_addr of $victim_dev removed so that $DEVICE gets one"
}

add_endpoints() {
	local addr addr_id err n=0
	for addr in $IP; do
		addr_id=""
		if [ -n "$ENDPOINT_ID" ]; then
			case "$n" in
				0) addr_id="$ENDPOINT_ID" ;;
				1) addr_id=$((ENDPOINT_ID + 128)) ;;
				2) addr_id=$((ENDPOINT_ID + 192)) ;;
				*) addr_id=256 ;;
			esac
			endpoint_ids "$endpoint" "$addr_id" "$DEVICE"
			if [ -n "$id_used" ] && [ "$id_holder" != "$DEVICE" ]; then
				logger -t "MPTCP" "Endpoint id $addr_id for $addr on $DEVICE is used by ${id_holder:-another endpoint}"
				addr_id=256
			fi
			if [ "$addr_id" -gt 255 ]; then
				if [ "$n" -gt 0 ]; then
					logger -t "MPTCP" "No endpoint id left for $addr on $DEVICE, not added to MPTCP"
					n=$((n + 1))
					continue
				fi
				addr_id=""
			fi
		fi
		if [ "$n" -eq 0 ]; then
			_free_endpoint_slot
		elif [ "$(ip mptcp endpoint show | grep -c .)" -ge "$MPTCP_ENDPOINTS_MAX" ]; then
			logger -t "MPTCP" "Endpoint limit ($MPTCP_ENDPOINTS_MAX) reached: $addr on $DEVICE not added to MPTCP"
			n=$((n + 1))
			continue
		fi
		err="$(ip mptcp endpoint add "$addr" dev "$DEVICE" "$@" ${addr_id:+id "$addr_id"} 2>&1)" || \
			logger -t "MPTCP" "Can't add endpoint $addr on $DEVICE: $err"
		n=$((n + 1))
	done
}

case $1 in
   "-h")
        echo "          Multipath-TCP configuration tool"
        echo "show/update flags:"
        echo "  multipath [device]"
        if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
                echo "  multipath device {on | off | backup }"
        else
                echo "  multipath device {on | off | signal | backup } [id]"
        fi
        echo
        echo "show established conections: -c"
        echo "show fullmesh info: -f"
        echo "show kernel config: -k"
        echo "show MPTCP info: -m"
        if [ -f /proc/sys/net/mptcp/enabled ]; then
		echo "verify MPTCP interface: -v device"
		echo "check the endpoint id of an interface: -i device id"
        fi
        echo
        echo "Flag on the device, to enable/disable MPTCP for this interface. The backup-flag"
        echo "will allow a subflow to be established across this interface, but only be used"
        echo "as backup."
        echo
        echo "Optional trailing [id] (on/signal/backup only): explicit MPTCP endpoint id"
        echo "(0-255) to pass to 'ip mptcp endpoint add ... id <id>' instead of letting the"
        echo "kernel auto-assign one. Needed for a stable id per WAN across reconnects, e.g."
        echo "so a peer (VPS) can pin traffic to this WAN via the subflow's remote_id."
        echo "It goes to the first address of the device, the next ones get id+128 and"
        echo "id+192 (IPv6 on a dual-stack WAN)."
        echo
        exit 0 ;;
   "-c")
        if [ -f /proc/net/mptcp_net/mptcp ]; then
            cat /proc/net/mptcp_net/mptcp
        else
            ss -M
        fi
        exit 0;;
   "-f")
        if [ -f /proc/net/mptcp_fullmesh ]; then
            cat /proc/net/mptcp_fullmesh
        else
            ip mptcp endpoint | grep fullmesh
        fi
        exit 0;;
   "-k")
        if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
            echo Enabled: `cat /proc/sys/net/mptcp/mptcp_enabled`
        elif [ -f /proc/sys/net/mptcp/enabled ]; then
            echo Enabled: `cat /proc/sys/net/mptcp/enabled`
        fi
        if [ -f /proc/sys/net/mptcp/mptcp_path_manager ]; then
            echo Path Manager: `cat /proc/sys/net/mptcp/mptcp_path_manager`
        fi
        if [ -f /proc/sys/net/mptcp/mptcp_checksum ]; then
            echo Use checksum: `cat /proc/sys/net/mptcp/mptcp_checksum`
        elif [ -f /proc/sys/net/mptcp/checksum_enabled ]; then
            echo Use checksum: `cat /proc/sys/net/mptcp/checksum_enabled`
        fi
        if [ -f /proc/sys/net/mptcp/mptcp_scheduler ]; then
            echo Scheduler: `cat /proc/sys/net/mptcp/mptcp_scheduler`
        fi
        if [ -f /proc/sys/net/mptcp/mptcp_syn_retries ]; then
            echo Syn retries: `cat /proc/sys/net/mptcp/mptcp_syn_retries`
        fi
        if [ -f /proc/sys/net/mptcp/mptcp_debug ]; then
            echo Debugmode: `cat /proc/sys/net/mptcp/mptcp_debug`
        fi
        echo
        exit 0 ;;
   "-m")
	if [ -f /proc/net/mptcp_net/snmp ]; then
	    cat /proc/net/mptcp_net/snmp
	else
	    # Absolute counters (-a), without touching nstat's history (-s):
	    # plain "nstat -z" printed the delta since anyone's previous nstat
	    # call and reset it, so two viewers zeroed each other's numbers.
	    nstat -a -s -z | grep -i mptcp
	fi
        exit 0 ;;
   "-v")
	if [ -f /proc/sys/net/mptcp/enabled ]; then
		DEVICE="$2"
		endpoint="$(ip mptcp endpoint show)"
		if [ -f /usr/bin/jsonfilter ]; then
			IP=$(ip -j a show $DEVICE scope global | jsonfilter -e '@[0].addr_info[*].local')
		elif [ -f /usr/bin/jq ]; then
			IP=$(ip -j a show $DEVICE scope global  | jq -r '.[0].addr_info[].local')
		else
			echo "jsonfilter or jq are required"
			exit 1
		fi
        if [ -n "$IP" ] && [ -n "$endpoint" ]; then
            for i in $IP; do
                ID=$(endpoint_id_of_addr "${endpoint}" "$i")
                [ -n "$ID" ] && break
            done
        fi
	fi
	if [ -n "$ID" ]; then
		echo "OK"
		exit 0
	else
		echo "NOK"
		exit 1
	fi
	;;
   "-i")
	# Exit 1 when the endpoint of device $2 doesn't have the explicit id $3
	# although no other endpoint holds it, so that recreating it gives that
	# id back. An endpoint that kept an old id is never fixed by the mode
	# checks of the callers, and it blocks the WAN that now has this id.
	[ -f /proc/sys/net/mptcp/enabled ] || exit 0
	case "$3" in
		''|*[!0-9]*) exit 0 ;;
	esac
	[ "$3" -le 255 ] || exit 0
	endpoint_ids "$(ip mptcp endpoint show)" "$3" "$2"
	if [ -z "$dev_ids" ] || [ -n "$id_used" ]; then
		exit 0
	fi
	exit 1
	;;
   "")
        for ifpath in /sys/class/net/*; do
                $0 ${ifpath##*/}
        done
        exit 0;;
   *);;
esac

DEVICE="$1"
TYPE="$2"
# Optional explicit MPTCP endpoint id (stable across delete/recreate, unlike
# the kernel's auto-assigned one) so a peer can key on subflow->remote_id.
# Anything that isn't a plain 0-255 integer is ignored (kernel auto-assigns).
ENDPOINT_ID="$3"
case "$ENDPOINT_ID" in
        ''|*[!0-9]*) ENDPOINT_ID="" ;;
        *) [ "$ENDPOINT_ID" -ge 0 ] 2>/dev/null && [ "$ENDPOINT_ID" -le 255 ] 2>/dev/null || ENDPOINT_ID="" ;;
esac
#FLAG_PATH=`find /sys/devices/ -path "*/net/$DEVICE/flags"`

[ -e "/sys/class/net/$DEVICE/" ] || {
        #echo "Device '$DEVICE' can't found!"
        #echo "Use the hardware name like in ifconfig"
        exit 1
}

if [ -f /proc/sys/net/mptcp/mptcp_enabled ]; then
        FLAG_PATH="/sys/class/net/$DEVICE/flags"
        IFF=`cat $FLAG_PATH`

        IFF_OFF="0x80000"
        IFF_ON="0x00"
        IFF_BACKUP="0x100000"
        IFF_HANDOVER="0x200000"
        IFF_MASK="0x380000"

        case $TYPE in
                "off")          FLAG=$IFF_OFF;;
                "on")           FLAG=$IFF_ON;;
                "backup")       FLAG=$IFF_BACKUP;;
                "handover")     FLAG=$IFF_HANDOVER;;
                "")
                        IFF=`printf "0x%02x" $(($IFF&$IFF_MASK))`
                        case "$IFF" in
                                $IFF_OFF)       echo $DEVICE is deactivated;;
                                $IFF_ON)        echo $DEVICE is in default mode;;
                                $IFF_BACKUP)    echo $DEVICE is in backup mode;;
                                $IFF_HANDOVER)  echo $DEVICE is in handover mode;;
                                *) echo "Unkown state!" && exit 1;;
                        esac
                        exit 0;;
                *) echo "Unkown flag! Use 'multipath -h' for help" && exit 1;;
        esac

        printf "0x%02x" $(($(($IFF^$(($IFF&$IFF_MASK))))|$FLAG)) > $FLAG_PATH
else
	endpoint="$(ip mptcp endpoint show)"
	if [ -n "$TYPE" ]; then
		# Remove not needed if* interfaces in MPTCP
		[ -n "$endpoint" ] && oldids=$(echo "${endpoint}" | grep "dev if" | awk '{ print $3 }')
		[ -n "$oldids" ] && {
			for oldid in $oldids; do
				ip mptcp endpoint delete id $oldid >/dev/null 2>&1
			done
			endpoint="$(ip mptcp endpoint show)"
		}
	fi
	if [ -n "$endpoint" ]; then
		# The ids of the device's endpoints, and the flags of its first
		# endpoint in sorted order. A sort|grep|awk pipeline for each of the
		# three cost 15 processes per call, and a status query runs for every
		# device ("multipath" alone lists them all): one pass over the list
		# gives the same values without any when the device has one endpoint.
		ep_count=0
		while IFS= read -r ep_line; do
			case "$ep_line" in
				*"dev $DEVICE "*)
					ep_first="$ep_line"
					ep_count=$((ep_count + 1))
					;;
			esac
		done <<EOF
$endpoint
EOF
		if [ "$ep_count" -gt 1 ]; then
			ID=$(echo "${endpoint}" | sort | grep -F "dev $DEVICE " | awk '{print $3}')
			ep_first=$(echo "${endpoint}" | sort | grep -F -m 1 "dev $DEVICE ")
		fi
		if [ "$ep_count" -gt 0 ]; then
			read -r ep_addr ep_idkw ep_id IFF IFF2 ep_rest <<EOF
$ep_first
EOF
			[ "$ep_count" -gt 1 ] || ID="$ep_id"
		fi
	fi
	if [ -n "$TYPE" ]; then
		#IP=$(ip a show $DEVICE | sed -En 's/127.0.0.1//;s/.*inet (addr:)?(([0-9]*\.){3}[0-9]*).*/\2/p')
		if [ -f /usr/bin/jsonfilter ]; then
			IP=$(ip -j a show $DEVICE scope global | jsonfilter -e '@[0].addr_info[*].local')
		elif [ -f /usr/bin/jq ]; then
			IP=$(ip -j a show $DEVICE scope global  | jq -r '.[0].addr_info[].local')
		else
			echo "jsonfilter or jq are required"
			exit 1
		fi
        if [ -z "$ID" ] && [ -n "$IP" ] && [ -n "$endpoint" ]; then
            for i in $IP; do
                ID=$(endpoint_id_of_addr "${endpoint}" "$i")
                [ -n "$ID" ] && break
            done
        fi
		[ -n "$endpoint" ] && RMID=$(echo "${endpoint}" | grep '::ffff' | awk '{ print $3 }')
		# One id per call: "delete id 3 4" is a syntax error for ip, so
		# with two such endpoints neither was removed
		for i in $RMID; do
			ip mptcp endpoint delete id $i >/dev/null 2>&1
		done
	fi
	case $TYPE in
                "off")
                        [ -n "$ID" ] && {
                            for i in $ID; do
                                ip mptcp endpoint delete id $i >/dev/null 2>&1
                            done
                        }
                        exit 0;;
                "on")
                        [ -z "$IP" ] && {
                            echo "No global IP found on $DEVICE"
                            exit 1
                        }
                        [ -n "$ID" ] && {
                            for i in $ID; do
                                ip mptcp endpoint delete id $i >/dev/null 2>&1
                            done
                        }
                        add_endpoints subflow fullmesh
                        exit 0;;
                "signal")
                        [ -z "$IP" ] && {
                            echo "No global IP found on $DEVICE"
                            exit 1
                        }
                        [ -n "$ID" ] && {
                            for i in $ID; do
                                ip mptcp endpoint delete id $i >/dev/null 2>&1
                            done
                        }
                        #add_endpoints signal subflow fullmesh
                        add_endpoints signal
                        exit 0;;
                "backup")
                        [ -z "$IP" ] && {
                            echo "No global IP found on $DEVICE"
                            exit 1
                        }
                        [ -n "$ID" ] && {
                            for i in $ID; do
                                ip mptcp endpoint delete id $i >/dev/null 2>&1
                            done
                        }
                        add_endpoints subflow backup fullmesh
                        exit 0;;
                "")
                        case "$IFF" in
                                "")          echo $DEVICE is deactivated;;
                                "subflow")
                                    case "$IFF2" in
                                        "backup")    echo $DEVICE is in backup mode;;
                                        "fullmesh")  echo $DEVICE is in default mode;;
                                        "")  echo $DEVICE is in subflow mode;;
                                    esac
                                    ;;
                                "signal")    echo $DEVICE is in signal mode;;
                                *)           echo "$DEVICE Unkown state!" && exit 1;;
                        esac
                        exit 0;;
                *) echo "Unkown flag! Use 'multipath -h' for help" && exit 1;;
        esac

fi

