#!/bin/sh
# /usr/libexec/rpcd/omr-bypass
# rpcd plugin for omr-bypass interface management
#
# Usage:
#   ubus call omr-bypass set_interface '{"section":"cfg01","interface":"wan1","force":true}'
#   ubus call omr-bypass get_interface '{"section":"cfg01"}'
#   ubus call omr-bypass list_sections {}

VALID_SECTION_TYPES="domains ips macs dest_port src_port dpis asns lan_ip"

# Callers pass the network.interface UCI *section* name (e.g. "wan1", see the
# ubus usage example above) -- the same value the LuCI bypass form stores.
# Only omr-bypass-nft's _intf_rule knows that section resolves to a device
# (e.g. "eth1"), and caches the mapping in omr-bypass.ifmap_<section>.device.
# Without resolving through that same cache here, this plugin would check/RST
# against a set that never exists (issue #4340's failure mode, applied to the
# rpcd API instead of the LuCI form).
_map_intf_device() {
	local raw="$1"
	case "$raw" in
		""|all|none|default|srv_vpn1) printf '%s' "$raw"; return ;;
	esac
	local dev
	dev=$(uci -q get "omr-bypass.ifmap_${raw}.device" 2>/dev/null)
	printf '%s' "${dev:-$raw}"
}

# This system's omr-bypass is nftables-based (omr_dst_bypass_<dev>_4/_6 sets
# in table inet fw4) -- there is no ipset binary/subsystem to query here.
_intf_ipset_exists() {
	local intf=$1
	[ -z "$intf" ] || [ "$intf" = "all" ] || [ "$intf" = "srv_vpn1" ] && return 0
	local dev
	dev=$(_map_intf_device "$intf")
	nft list set inet fw4 "omr_dst_bypass_${dev}_4" >/dev/null 2>&1 \
		|| nft list set inet fw4 "omr_dst_bypass_${dev}_6" >/dev/null 2>&1
}

# Delete conntrack entries for IPs in the old interface's bypass nftables
# sets. The kernel sends RST to peers when their tracked entries disappear.
_rst_old_intf_connections() {
	local old_intf=$1
	[ -z "$old_intf" ] || [ "$old_intf" = "all" ] || [ "$old_intf" = "srv_vpn1" ] && return

	local dev
	dev=$(_map_intf_device "$old_intf")

	local set_name
	for set_name in "omr_dst_bypass_${dev}_4" "omr_dst_bypass_${dev}_6"; do
		nft list set inet fw4 "$set_name" 2>/dev/null \
			| sed -n 's/.*elements = { \(.*\) }.*/\1/p' \
			| tr ',' '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
			| while read -r ip; do
				[ -z "$ip" ] && continue
				conntrack -D --orig-dst "$ip" >/dev/null 2>&1
				conntrack -D --reply-src "$ip" >/dev/null 2>&1
			done
	done
}

_json_str() {
	printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

case "$1" in
	list)
		echo '{
  "set_interface": {
    "section":   "str",
    "interface": "str",
    "force":     false,
    "reload":    false
  },
  "get_interface": {
    "section": "str"
  },
  "list_sections": {}
}'
		# force=true: skip active-interface check, RST connections on the
		# previous interface via conntrack deletion, then reload rules.
		# reload=true: reload rules without the RST step.
		;;
	call)
		case "$2" in
			set_interface)
				input=$(cat)
				section=$(echo "$input"   | jsonfilter -q -e '@.section')
				interface=$(echo "$input" | jsonfilter -q -e '@.interface')
				force=$(echo "$input"     | jsonfilter -q -e '@.force')
				reload=$(echo "$input"    | jsonfilter -q -e '@.reload')

				if [ -z "$section" ]; then
					echo '{"error":"missing section parameter"}'
					exit 1
				fi

				section_type=$(uci -q get "omr-bypass.${section}" 2>/dev/null)
				if [ -z "$section_type" ]; then
					echo "{\"error\":\"section '$(_json_str "$section")' not found in omr-bypass config\"}"
					exit 1
				fi

				[ -z "$interface" ] && interface="all"
				safe_intf=$(echo "$interface" | sed 's/[^a-zA-Z0-9_-]//g')
				if [ "$safe_intf" != "$interface" ]; then
					echo '{"error":"invalid interface name"}'
					exit 1
				fi

				if [ "$force" != "true" ] && [ "$interface" != "all" ] && [ "$interface" != "srv_vpn1" ]; then
					if ! _intf_ipset_exists "$interface"; then
						echo "{\"error\":\"interface '$(_json_str "$interface")' is not active; use force:true to override\"}"
						exit 1
					fi
				fi

				# Remember the old interface so we can RST its active connections
				old_interface=$(uci -q get "omr-bypass.${section}.interface" 2>/dev/null)
				[ -z "$old_interface" ] && old_interface="all"

				if [ "$interface" = "all" ]; then
					uci -q delete "omr-bypass.${section}.interface"
				else
					uci -q set "omr-bypass.${section}.interface=${interface}"
				fi
				uci -q commit omr-bypass

				if [ "$force" = "true" ]; then
					_rst_old_intf_connections "$old_interface"
					/etc/init.d/omr-bypass reload_rules >/dev/null 2>&1 &
				elif [ "$reload" = "true" ]; then
					/etc/init.d/omr-bypass reload_rules >/dev/null 2>&1 &
				fi

				echo "{\"result\":\"ok\",\"section\":\"$(_json_str "$section")\",\"interface\":\"$(_json_str "$interface")\",\"prev_interface\":\"$(_json_str "$old_interface")\"}"
				;;

			get_interface)
				input=$(cat)
				section=$(echo "$input" | jsonfilter -q -e '@.section')

				if [ -z "$section" ]; then
					echo '{"error":"missing section parameter"}'
					exit 1
				fi

				section_type=$(uci -q get "omr-bypass.${section}" 2>/dev/null)
				if [ -z "$section_type" ]; then
					echo "{\"error\":\"section '$(_json_str "$section")' not found in omr-bypass config\"}"
					exit 1
				fi

				interface=$(uci -q get "omr-bypass.${section}.interface" 2>/dev/null)
				[ -z "$interface" ] && interface="all"

				active="false"
				_intf_ipset_exists "$interface" && active="true"

				echo "{\"section\":\"$(_json_str "$section")\",\"type\":\"$(_json_str "$section_type")\",\"interface\":\"$(_json_str "$interface")\",\"active\":${active}}"
				;;

			list_sections)
				entries=""
				for type in $VALID_SECTION_TYPES; do
					for key in $(uci -q show omr-bypass 2>/dev/null | grep "=${type}$" | cut -d= -f1); do
						sec=$(echo "$key" | sed 's/omr-bypass\.//')
						intf=$(uci -q get "omr-bypass.${sec}.interface" 2>/dev/null)
						[ -z "$intf" ] && intf="all"
						name=$(uci -q get "omr-bypass.${sec}.name" 2>/dev/null)
						entry="{\"section\":\"$(_json_str "$sec")\",\"type\":\"$(_json_str "$type")\",\"interface\":\"$(_json_str "$intf")\",\"name\":\"$(_json_str "$name")\"}"
						[ -n "$entries" ] && entries="${entries},"
						entries="${entries}${entry}"
					done
				done
				echo "{\"sections\":[${entries}]}"
				;;

			*)
				echo '{"error":"unknown method"}'
				exit 1
				;;
		esac
		;;
esac
