#!/bin/sh
# velmo-modo del router (spec 2026-09-15 porting, §14.6): commuta la modalità di
# un televisore o di tutta la casa aggiornando gli insiemi ipset, come
# velmo-modo.j2 di main con nft.
#
#   leggibile   in velmo_non_aggressivi e in velmo_solo_leggibile
#   protetto    in velmo_non_aggressivi
#   aggressivo  in nessun insieme: chi non ha istruzioni ha la modalità più chiusa (ADR 0015)
#   bloccato    in velmo_bloccati: non esce niente, e il DNS risponde 0.0.0.0
#
#   velmo-modo                              mostra la modalità di ogni televisore
#   velmo-modo <modo>                       la applica a tutti
#   velmo-modo <indirizzo> <modo> [durata]  a un televisore, e con la durata (90s, 30m, 2h) poi torna
#   velmo-modo --riapplica                  rimette negli insiemi ciò che è dichiarato e riarma i ritorni
#   velmo-modo --dimentica <indirizzo>      toglie la modalità di quell'indirizzo
#
# Differenze da main: gli insiemi si riscrivono con «ipset swap» (atomico, come la
# transazione di nft); i ritorni a tempo sono un processo che dorme, lanciato con
# start-stop-daemon, al posto dei timer di systemd-run, e il gettone su disco li
# fa riarmare a ogni --riapplica (anche dopo un riavvio); il lucchetto è flock su
# un file in /var/run/velmo, cartella di root.

. /etc/velmo/router.conf
MODI=${MODI:-/etc/velmo/modi}
EVENTI=${EVENTI:-$VELMO_DATI/eventi.log}
STATO_DIR=${STATO_DIR:-/var/run/velmo}
IN_VIGORE=${IN_VIGORE:-$STATO_DIR/modi-in-vigore}
RITORNI_DIR=${RITORNI_DIR:-/etc/velmo/ritorni}
MODALITA="leggibile protetto aggressivo bloccato"
IP_RE='^([0-9]{1,3}\.){3}[0-9]{1,3}$'

annota() { printf '%s\tmodo\t%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" >> "$EVENTI" 2>/dev/null; logger -t velmo "velmo-modo: $1"; }
e_ip() { echo "$1" | grep -qE "$IP_RE"; }
in_secondi() { case "$1" in *s) echo "${1%s}" ;; *m) echo $(( ${1%m} * 60 )) ;; *h) echo $(( ${1%h} * 3600 )) ;; *) echo "$1" ;; esac; }

modo_di() {
	local m
	m=$(awk -v ip="$1" '$1 == ip {print $2; exit}' "$MODI" 2>/dev/null)
	case " $MODALITA " in *" $m "*) [ -n "$m" ] && { echo "$m"; return; } ;; esac
	echo aggressivo
}
televisori() { awk '!/^#/ && NF >= 2 {print $1}' "$MODI" 2>/dev/null; }

scrivi_modo() {
	local t
	t=$(mktemp)
	{ grep -v "^$1 " "$MODI" 2>/dev/null; echo "$1 $2"; } | sort -u > "$t"
	chmod 0644 "$t"; mv -f "$t" "$MODI"
}
togli_modo() {
	local t
	t=$(mktemp)
	awk -v ip="$1" '$1 != ip' "$MODI" > "$t" 2>/dev/null
	chmod 0644 "$t"; mv -f "$t" "$MODI"
}

rango() { case "$1" in leggibile) echo 0 ;; protetto) echo 1 ;; bloccato) echo 3 ;; *) echo 2 ;; esac; }
# Verso una modalità più stretta si chiudono le connessioni aperte (spec del
# blocco per nome §4.2, punto 9); verso una più larga no.
chiudi_se_piu_stretta() {
	[ "$(rango "$3")" -gt "$(rango "$2")" ] && conntrack -D -s "$1" >/dev/null 2>&1
	return 0
}
# La deviazione DNS si decide sul primo pacchetto di ogni voce conntrack.
chiudi_dns_deviato() {
	local ip p porta
	for ip in "$@"; do
		for p in udp tcp; do
			for porta in 53 $VELMO_DNS_LEGGIBILE $VELMO_DNS_PROTETTA $VELMO_DNS_AGGRESSIVA $VELMO_DNS_BLOCCATA; do
				conntrack -D -s "$ip" -p $p --orig-port-dst $porta >/dev/null 2>&1
			done
		done
	done
}

# Un ritorno a tempo: un processo che dorme e poi chiama --ritorno con il gettone.
arma_ritorno() { # ip gettone secondi
	start-stop-daemon -S -b -m -p "$STATO_DIR/ritorno-$1.pid" -x /bin/sh -- -c "sleep $3; exec /usr/sbin/velmo-modo --ritorno $1 $2" 9>&-
}
disarma_ritorno() {
	[ -f "$STATO_DIR/ritorno-$1.pid" ] && kill "$(cat "$STATO_DIR/ritorno-$1.pid")" 2>/dev/null
	rm -f "$STATO_DIR/ritorno-$1.pid"
}
ritorno_armato() { [ -f "$STATO_DIR/ritorno-$1.pid" ] && kill -0 "$(cat "$STATO_DIR/ritorno-$1.pid")" 2>/dev/null; }

riprendi_ritorni() {
	local adesso g ip atteso precedente scadenza
	adesso=$(date +%s)
	for g in "$RITORNI_DIR"/ritorno-*; do
		[ -f "$g" ] || continue
		ip=${g##*/ritorno-}
		ritorno_armato "$ip" && continue
		atteso=$(cut -f1 "$g"); precedente=$(cut -f3 "$g"); scadenza=$(cut -f4 "$g")
		case " $MODALITA " in *" $precedente "*) ;; *) precedente=aggressivo ;; esac
		if ! echo "$scadenza" | grep -qE '^[0-9]+$' || [ "$scadenza" -le "$adesso" ]; then
			rm -f "$g"
			scrivi_modo "$ip" "$precedente"
			annota "$ip $precedente (ritorno automatico, scaduto mentre il dispositivo era fermo)"
			continue
		fi
		arma_ritorno "$ip" "$atteso" $(( scadenza - adesso ))
		annota "$ip ritorno a $precedente riarmato fra $(( scadenza - adesso ))s"
	done
}

# riscrivi INSIEME ELENCO: il contenuto nuovo in un insieme accanto, poi uno scambio.
riscrivi() {
	local ip
	ipset -exist create "$1" hash:ip
	ipset -exist create "$1.nuovo" hash:ip
	ipset flush "$1.nuovo"
	for ip in $2; do ipset -exist add "$1.nuovo" "$ip"; done
	ipset swap "$1.nuovo" "$1" && ipset destroy "$1.nuovo"
}

scrivi_in_vigore() {
	local t ip
	t="$IN_VIGORE.parziale"
	{
		echo "# Scritto da velmo-modo dopo aver letto il firewall. Non modificare."
		for ip in $(televisori); do
			if ipset test velmo_bloccati "$ip" 2>/dev/null; then echo "$ip bloccato"
			elif ! ipset test velmo_non_aggressivi "$ip" 2>/dev/null; then echo "$ip aggressivo"
			elif ipset test velmo_solo_leggibile "$ip" 2>/dev/null; then echo "$ip leggibile"
			else echo "$ip protetto"; fi
		done
	} > "$t"
	chmod 0644 "$t"; mv -f "$t" "$IN_VIGORE"
}

riapplica() {
	# Le variabili delle funzioni sono locali: «ip» era la stessa del chiamante,
	# e dopo riapplica --dimentica e --ritorno annotavano l'ultimo televisore
	# dell'elenco e gli tagliavano le connessioni (17 settembre 2026, la riga
	# «10.42.0.144 ... dimenticata» della sera prima era di un altro apparecchio).
	local leggibili esenti bloccati ip scritto modo
	leggibili=""; esenti=""; bloccati=""
	riprendi_ritorni
	for ip in $(televisori); do
		scritto=$(awk -v ip="$ip" '$1 == ip {print $2; exit}' "$MODI")
		modo=$(modo_di "$ip")
		if [ "$scritto" != "$modo" ]; then
			scrivi_modo "$ip" "$modo"; annota "$ip $modo (era «$scritto», che non è una modalità)"
		fi
		case $modo in
		leggibile) leggibili="$leggibili $ip"; esenti="$esenti $ip" ;;
		protetto) esenti="$esenti $ip" ;;
		# Un bloccato sta solo nel suo insieme: il firewall lo scarta prima di
		# guardare gli altri due, e il DNS lo manda alla sua istanza.
		bloccato) bloccati="$bloccati $ip" ;;
		esac
	done
	# Lo stato in vigore non si cancella prima di riscrivere gli insiemi: lo
	# sostituisce scrivi_in_vigore in un colpo solo, leggendolo dal firewall. Fino
	# al 5 ottobre 2026 qui c'era un «rm -f», e per un secondo a ogni commutazione
	# la console diceva «lo stato in vigore non si legge».
	riscrivi velmo_non_aggressivi "$esenti"
	riscrivi velmo_solo_leggibile "$leggibili"
	riscrivi velmo_bloccati "$bloccati"
	scrivi_in_vigore
	chiudi_dns_deviato $(televisori)
}

mkdir -p "$STATO_DIR" "$RITORNI_DIR"; chmod 0700 "$RITORNI_DIR"
touch "$MODI"

case "${1:-}" in
""|stato)
	for ip in $(televisori); do
		printf '%-16s %s' "$ip" "$(modo_di "$ip")"
		[ -f "$RITORNI_DIR/ritorno-$ip" ] && printf ' (ritorno a %s alle %s)' "$(cut -f3 "$RITORNI_DIR/ritorno-$ip")" "$(cut -f2 "$RITORNI_DIR/ritorno-$ip")"
		echo
	done
	exit 0 ;;
-h|--help|aiuto) sed -n '2,16p' "$0"; exit 0 ;;
esac

# Un comando alla volta: due commutazioni insieme riscriverebbero gli insiemi l'una sull'altra.
exec 9> "$STATO_DIR/modo.lucchetto"
# flock di BusyBox non ha -w: si riprova per 10 secondi. Il lucchetto non passa
# ai processi dei ritorni a tempo (9>&- in arma_ritorno).
n=0
while ! flock -n 9 && [ $n -lt 100 ]; do sleep 0.1; n=$((n + 1)); done
if [ $n -ge 100 ]; then
	logger -t velmo "velmo-modo: lucchetto occupato da più di 10 secondi, comando non eseguito: $*"
	exit 75
fi

case "$1" in
--riapplica) riapplica; exit 0 ;;
--ritorno)
	ip=$2; atteso=$3; g="$RITORNI_DIR/ritorno-$ip"
	[ -n "$atteso" ] && [ "$atteso" = "$(cut -f1 "$g" 2>/dev/null)" ] || exit 0
	precedente=$(cut -f3 "$g"); rm -f "$g" "$STATO_DIR/ritorno-$ip.pid"
	attuale=$(modo_di "$ip")
	scrivi_modo "$ip" "$precedente"
	riapplica
	chiudi_se_piu_stretta "$ip" "$attuale" "$precedente"
	annota "$ip $precedente (ritorno automatico)"
	exit 0 ;;
--dimentica)
	ip=$2; e_ip "$ip" || { echo "uso: velmo-modo --dimentica <indirizzo>" >&2; exit 2; }
	vecchio=$(awk -v ip="$ip" '$1 == ip {print $2; exit}' "$MODI")
	rm -f "$RITORNI_DIR/ritorno-$ip"; disarma_ritorno "$ip"
	togli_modo "$ip"
	riapplica
	conntrack -D -s "$ip" >/dev/null 2>&1
	annota "$ip aggressivo (modalità dimenticata, era «$vecchio»)"
	exit 0 ;;
esac

if e_ip "$1"; then bersagli=$1; shift; else bersagli=$(televisori); fi
modo=$1; durata=$2
case " $MODALITA " in *" $modo "*) [ -n "$modo" ] || exit 2 ;; *) echo "modi: $MODALITA" >&2; exit 2 ;; esac
[ -n "$bersagli" ] || { echo "nessun televisore dichiarato in $MODI" >&2; exit 2; }
if [ -n "$durata" ] && ! echo "$durata" | grep -qE '^[1-9][0-9]{0,4}[smh]$'; then echo "durata: 90s, 30m, 2h" >&2; exit 2; fi

attuali=""
for ip in $bersagli; do
	precedente=$(modo_di "$ip")
	attuali="$attuali $ip=$precedente"
	# Una modalità a tempo sopra un'altra a tempo torna a quella di prima di tutte.
	if [ -n "$durata" ] && [ -f "$RITORNI_DIR/ritorno-$ip" ]; then
		salvata=$(cut -f3 "$RITORNI_DIR/ritorno-$ip")
		case " $MODALITA " in *" $salvata "*) precedente=$salvata ;; esac
	fi
	rm -f "$RITORNI_DIR/ritorno-$ip"; disarma_ritorno "$ip"
	scrivi_modo "$ip" "$modo"
	if [ -n "$durata" ]; then
		gettone="$(date +%s)$$"
		istante=$(( $(date +%s) + $(in_secondi "$durata") ))
		printf '%s\t%s\t%s\t%s\n' "$gettone" "$(date -d "@$istante" '+%H:%M:%S')" "$precedente" "$istante" > "$RITORNI_DIR/.ritorno-$ip"
		mv -f "$RITORNI_DIR/.ritorno-$ip" "$RITORNI_DIR/ritorno-$ip"
		arma_ritorno "$ip" "$gettone" "$(in_secondi "$durata")"
		annota "$ip $modo per $durata"
	else
		annota "$ip $modo"
	fi
done
riapplica
for voce in $attuali; do chiudi_se_piu_stretta "${voce%%=*}" "${voce#*=}" "$modo"; done
