- scripts antiflood/ipwatch/uptime-check chargent désormais leur webhook depuis scripts/.webhooks (gitignoré) au lieu d'un token en dur - suppression scripts/antiflood.sh.bak-before-cpu-removal (doublon mort) - untrack + gitignore des fichiers d'état runtime (.ipwatch-state/offset, .synbase, .synstate, .uptime-state) contenant des IP opérationnelles
126 lines
4.9 KiB
Shell
Executable file
126 lines
4.9 KiB
Shell
Executable file
#!/usr/bin/env bash
|
||
# Zektyc — protection contre les anomalies de trafic (DoS/DDoS L4, SYN floods)
|
||
#
|
||
# Mise au point : NE COUPE QUE lorsque le réseau est réellement saturé, au bord
|
||
# de tomber. On ne réagit plus à un simple pic de débit (un burst bien absorbé
|
||
# qui ne sature pas le lien ne doit PAS déclencher). On se base sur le DÉBIT RX
|
||
# mesuré en octets par rapport au PLAFOND DE BANDE PASSANTE du lien (étalonné) :
|
||
# - on coupe seulement quand le débit RX soutenu approche le plafond
|
||
# (le lien est sur le point d'être saturé / de ralentir vraiment),
|
||
# - pendant plus de PERSIST ticks consécutifs (pas un pic bref).
|
||
# Le funnel + playit sont réactivés automatiquement quand ça redescend.
|
||
set -u
|
||
|
||
# Webhook Discord chargé depuis un fichier local gitignoré (jamais commité).
|
||
source /home/riricdev/zektyc/scripts/.webhooks
|
||
WEBHOOK="${ANTIFLOOD_WEBHOOK:-}"
|
||
STATE_FILE="/home/riricdev/zektyc/scripts/.synstate"
|
||
BASE_FILE="/home/riricdev/zektyc/scripts/.synbase"
|
||
LOG="/var/log/zektyc-syn.log"
|
||
|
||
IFACE_ETH="eth0" # interface réseau publique (les SYN floods y arrivent)
|
||
IFACE_TS="tailscale0" # interface tailscale (légitime)
|
||
|
||
INTERVAL=2 # période d'échantillonnage (s)
|
||
|
||
# Plafond de bande passante DOWN mesuré via speedtest (~80 Mbit/s ≈ 10 Mo/s).
|
||
# Tout le trafic entrant (SYN floods compris) passe par eth0 et sature ce plafond.
|
||
# 10000000 octets/s × INTERVAL(2) = octets par tick au plafond.
|
||
CAP_BYTES=$(( 10000000 * INTERVAL ))
|
||
|
||
# Fraction du plafond au-delà de laquelle on considère le lien "au bord de
|
||
# tomber" (0.90 = saturé à 90 %). Un burst bref ne suffit pas : il faut tenir
|
||
# au-dessus pendant PERSIST ticks consécutifs.
|
||
CAP_RATIO=0.90
|
||
PERSIST=4 # ticks consécutifs au-dessus du seuil requis avant de couper
|
||
COOLDOWN_UP=45 # (s) attendre avant de tenter un rétablissement une fois calme
|
||
|
||
log() { printf '%s %s\n' "$(date +%F\ %T)" "$1" >>"$LOG"; }
|
||
notify() { printf '{"content":"%s"}' "$1" | curl -s -m 10 -H "Content-Type: application/json" -X POST "$WEBHOOK" -d @- >/dev/null 2>&1 || true; }
|
||
|
||
# Lit le nombre TOTAL d'OCTETS reçus (RX) sur l'interface donnée, depuis
|
||
# /proc/net/dev (2e champ après le nom d'interface, colonne RX bytes).
|
||
# La saturation de bande passante se mesure en OCTETS, pas en paquets.
|
||
read_rx() { awk -v ifx="$1" '$0 ~ ifx {print $2}' /proc/net/dev; }
|
||
|
||
# Vrai si le funnel TCP du site (:443) est actif (relais brut + PROXY protocol v2
|
||
# vers nginx 127.0.0.1:444). Le nom de domaine est suivi de ":443" - on le cherche
|
||
# explicitement pour ne pas matcher l'entree fantome sur un autre port (:10000).
|
||
funnel_active() { tailscale funnel status 2>/dev/null | grep -qE 'tcp://riricdev\.tail5ea5cd\.ts\.net:443'; }
|
||
|
||
funnel_off() {
|
||
# coupe le funnel tailscale (site web) — mode TCP brut + PROXY protocol
|
||
if funnel_active; then
|
||
tailscale funnel --tcp=443 off >/dev/null 2>&1
|
||
log "Anomalie de trafic : funnel tailscale coupé"
|
||
fi
|
||
# coupe l'entrée playit.gg (serveurs Minecraft publics)
|
||
if systemctl is-active --quiet playit.service; then
|
||
systemctl stop playit.service >/dev/null 2>&1
|
||
log "Anomalie de trafic : entrée playit coupée"
|
||
fi
|
||
notify "🔴 **Zektyc** — trafic anormal détecté, accès temporairement suspendu."
|
||
}
|
||
|
||
funnel_on() {
|
||
# réactive le funnel tailscale (site web) — TCP brut + PPv2 vers nginx :444
|
||
if ! funnel_active; then
|
||
tailscale funnel --bg --proxy-protocol=2 --tcp=443 tcp://127.0.0.1:444 >/dev/null 2>&1
|
||
log "Retour à la normale : funnel tailscale réactivé"
|
||
fi
|
||
# réactive l'entrée playit.gg (serveurs Minecraft publics)
|
||
if ! systemctl is-active --quiet playit.service; then
|
||
systemctl start playit.service >/dev/null 2>&1
|
||
log "Retour à la normale : entrée playit réactivée"
|
||
fi
|
||
notify "🟢 **Zektyc** — accès rétabli."
|
||
}
|
||
|
||
# --- init ---
|
||
now=$(read_rx "$IFACE_ETH")
|
||
prev=$now
|
||
[[ ! -f "$STATE_FILE" ]] && echo "up" >"$STATE_FILE"
|
||
state="$(<"$STATE_FILE")"
|
||
streak=0
|
||
down_since=0
|
||
|
||
# seuil de saturation en octets/tic
|
||
thresh=$(( CAP_BYTES > 0 ? CAP_BYTES : 1 ))
|
||
|
||
while true; do
|
||
now=$(read_rx "$IFACE_ETH")
|
||
delta=$(( now - prev ))
|
||
prev=$now
|
||
[[ "$delta" -lt 0 ]] && delta=0
|
||
state="$(<"$STATE_FILE")"
|
||
|
||
# --- saturation : débit RX au-dessus de CAP_RATIO × plafond du lien ---
|
||
# Un SYN flood qui sature le lien fait monter le débit RX vers le plafond.
|
||
# On ne coupe que si ça tient PERSIST ticks consécutifs (pas un pic bref).
|
||
sat=0
|
||
# delta(thresh×CAP_RATIO) ⇔ delta*100 ≥ thresh×(CAP_RATIO×100)
|
||
if (( delta * 100 >= thresh * 90 )); then
|
||
sat=1
|
||
fi
|
||
|
||
if [[ "$sat" -eq 1 ]]; then
|
||
streak=$(( streak + 1 ))
|
||
else
|
||
streak=0
|
||
fi
|
||
|
||
# --- actions ---
|
||
if [[ "$state" == "up" && "$streak" -ge "$PERSIST" ]]; then
|
||
echo down >"$STATE_FILE"
|
||
down_since=$(date +%s)
|
||
funnel_off
|
||
elif [[ "$state" == "down" ]]; then
|
||
now_s=$(date +%s)
|
||
if (( now_s - down_since >= COOLDOWN_UP && streak == 0 )); then
|
||
echo up >"$STATE_FILE"
|
||
funnel_on
|
||
fi
|
||
fi
|
||
|
||
sleep "$INTERVAL"
|
||
done
|