#!/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