#!/bin/sh /etc/rc.common

START=99
STOP=10

EXTRA_COMMANDS="status"
EXTRA_HELP="        status   Check if timecontrol rules are active\n"

. /lib/functions.sh

TABLE="timecontrol"
CHAIN="TIMECONTROL"

firewall_backend() {
	if command -v fw4 >/dev/null 2>&1 && command -v nft >/dev/null 2>&1; then
		echo nft
	else
		echo iptables
	fi
}

have_ip6tables() {
	command -v ip6tables >/dev/null 2>&1
}

valid_mac() {
	printf '%s\n' "$1" | grep -Eq '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'
}

valid_time() {
	printf '%s\n' "$1" | grep -Eq '^([01][0-9]|2[0-3]):[0-5][0-9]$'
}

add_nft_range() {
	local macaddr="$1"
	local timeon="$2"
	local timeoff="$3"
	local weekdays="$4"

	nft -f - <<-EOF
		add rule inet $TABLE forward ether saddr $macaddr meta day { $weekdays } meta hour "$timeon"-"$timeoff" counter drop
	EOF
}

add_nft_rule() {
	local macaddr="$1"
	local timeon="$2"
	local timeoff="$3"
	local weekdays="$4"
	local weekdays_next="$5"

	if [ "$timeon" \< "$timeoff" ] || [ "$timeon" = "$timeoff" ]; then
		add_nft_range "$macaddr" "$timeon" "$timeoff" "$weekdays"
	else
		# Range spans midnight: block until 23:59:59 on the selected
		# days, then from 00:00 until timeoff on the following days.
		add_nft_range "$macaddr" "$timeon" "23:59:59" "$weekdays"
		add_nft_range "$macaddr" "00:00" "$timeoff" "$weekdays_next"
	fi
}

add_ipt_range() {
	local cmd="$1"
	local macaddr="$2"
	local timeon="$3"
	local timeoff="$4"
	local weekdays="$5"

	"$cmd" -w -t filter -A "$CHAIN" -m mac --mac-source "$macaddr" \
		-m time --kerneltz --timestart "$timeon" --timestop "$timeoff" \
		--weekdays "$weekdays" -j DROP
}

add_ipt_rule() {
	local macaddr="$1"
	local timeon="$2"
	local timeoff="$3"
	local weekdays="$4"
	local weekdays_next="$5"
	local cmd

	# Mirror every rule into ip6tables as well, otherwise IPv6 traffic
	# would bypass the time control completely.
	for cmd in iptables ip6tables; do
		command -v "$cmd" >/dev/null 2>&1 || continue
		if [ "$timeon" \< "$timeoff" ] || [ "$timeon" = "$timeoff" ]; then
			add_ipt_range "$cmd" "$macaddr" "$timeon" "$timeoff" "$weekdays"
		else
			# Range spans midnight: block until 23:59:59 on the
			# selected days, then from 00:00 until timeoff on the
			# following days.
			add_ipt_range "$cmd" "$macaddr" "$timeon" "23:59:59" "$weekdays"
			add_ipt_range "$cmd" "$macaddr" "00:00" "$timeoff" "$weekdays_next"
		fi
	done
}

load_rule() {
	local section="$1"
	local enabled macaddr timeon timeoff
	local z1 z2 z3 z4 z5 z6 z7
	local ipt_days nft_days ipt_days_next nft_days_next

	config_get_bool enabled "$section" enable 0
	[ "$enabled" -eq 1 ] || return 0

	config_get macaddr "$section" macaddr
	config_get timeon "$section" timeon
	config_get timeoff "$section" timeoff
	valid_mac "$macaddr" && valid_time "$timeon" && valid_time "$timeoff" || {
		logger -t timecontrol "Ignoring invalid rule in section $section"
		return 0
	}

	config_get_bool z1 "$section" z1 0
	config_get_bool z2 "$section" z2 0
	config_get_bool z3 "$section" z3 0
	config_get_bool z4 "$section" z4 0
	config_get_bool z5 "$section" z5 0
	config_get_bool z6 "$section" z6 0
	config_get_bool z7 "$section" z7 0

	# The *_next lists hold each selected weekday shifted by one day;
	# they apply to the after-midnight part of ranges spanning midnight.
	[ "$z1" -eq 1 ] && { append ipt_days Mon ,; append ipt_days_next Tue ,; append nft_days monday ,; append nft_days_next tuesday ,; }
	[ "$z2" -eq 1 ] && { append ipt_days Tue ,; append ipt_days_next Wed ,; append nft_days tuesday ,; append nft_days_next wednesday ,; }
	[ "$z3" -eq 1 ] && { append ipt_days Wed ,; append ipt_days_next Thu ,; append nft_days wednesday ,; append nft_days_next thursday ,; }
	[ "$z4" -eq 1 ] && { append ipt_days Thu ,; append ipt_days_next Fri ,; append nft_days thursday ,; append nft_days_next friday ,; }
	[ "$z5" -eq 1 ] && { append ipt_days Fri ,; append ipt_days_next Sat ,; append nft_days friday ,; append nft_days_next saturday ,; }
	[ "$z6" -eq 1 ] && { append ipt_days Sat ,; append ipt_days_next Sun ,; append nft_days saturday ,; append nft_days_next sunday ,; }
	[ "$z7" -eq 1 ] && { append ipt_days Sun ,; append ipt_days_next Mon ,; append nft_days sunday ,; append nft_days_next monday ,; }
	[ -n "$ipt_days" ] || return 0

	if [ "$BACKEND" = nft ]; then
		add_nft_rule "$macaddr" "$timeon" "$timeoff" "$nft_days" "$nft_days_next"
	else
		add_ipt_rule "$macaddr" "$timeon" "$timeoff" "$ipt_days" "$ipt_days_next"
	fi
}

load_basic() {
	config_get_bool ENABLED "$1" enable 0
}

start_nft() {
	nft -f - <<-EOF
		table inet $TABLE {
			chain forward {
				type filter hook forward priority -1; policy accept;
			}
		}
	EOF

	# Flush fw4's flowtable so that connections already on the fast path
	# (which bypasses this forward hook) are forced back to the slow path
	# where our DROP rules can reach them.  Non-blocked devices will
	# re-offload within seconds; the disruption is minimal.
	nft flush flowtable inet fw4 flowtable_ft 2>/dev/null
}

start_iptables() {
	iptables -w -t filter -N "$CHAIN" || return 1
	iptables -w -t filter -I FORWARD 1 -j "$CHAIN"
	if have_ip6tables; then
		ip6tables -w -t filter -N "$CHAIN" || return 1
		ip6tables -w -t filter -I FORWARD 1 -j "$CHAIN"
	else
		logger -t timecontrol "ip6tables not found; IPv6 traffic will not be controlled"
	fi
}

stop_nft() {
	command -v nft >/dev/null 2>&1 && nft delete table inet "$TABLE" 2>/dev/null
	return 0
}

stop_ipt_family() {
	local cmd="$1"

	command -v "$cmd" >/dev/null 2>&1 || return 0
	while "$cmd" -w -t filter -C FORWARD -j "$CHAIN" 2>/dev/null; do
		"$cmd" -w -t filter -D FORWARD -j "$CHAIN" 2>/dev/null || break
	done
	"$cmd" -w -t filter -F "$CHAIN" 2>/dev/null
	"$cmd" -w -t filter -X "$CHAIN" 2>/dev/null
}

stop_iptables() {
	stop_ipt_family iptables
	stop_ipt_family ip6tables
}

start() {
	config_load timecontrol
	ENABLED=0
	config_foreach load_basic basic
	[ "$ENABLED" -eq 1 ] || return 0

	stop_nft
	stop_iptables
	BACKEND="$(firewall_backend)"
	mkdir -p /var/etc
	printf '%s\n' "/etc/init.d/timecontrol reload" > /var/etc/timecontrol.include

	if [ "$BACKEND" = nft ]; then
		start_nft || return 1
	else
		start_iptables || return 1
	fi

	config_foreach load_rule macbind
}

stop() {
	stop_nft
	stop_iptables
}

reload() {
	stop
	start
}

status() {
	if [ "$(firewall_backend)" = nft ]; then
		nft list table inet "$TABLE" >/dev/null 2>&1
	else
		iptables -w -t filter -S "$CHAIN" >/dev/null 2>&1
	fi
}
