chore: checkpoint initial — Zektyc dataset, avant humanisation

- site vitrine + fingerprint test + zmap + z-panel
- remplacement duckdns -> riricdev.tail5ea5cd.ts.net (canonical/og/alternates/docs)
- suppression du token DuckDNS (plus utilisé)
This commit is contained in:
riricdev 2026-09-06 13:03:25 +02:00
commit 8e7ec41f18
104 changed files with 19256 additions and 0 deletions

124
scripts/antiflood.sh Executable file
View file

@ -0,0 +1,124 @@
#!/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="https://discord.com/api/webhooks/***REMOVED***"
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

187
scripts/ipwatch.sh Executable file
View file

@ -0,0 +1,187 @@
#!/usr/bin/env bash
# Zektyc — moniteur d'IP suspectes (staff only)
#
# Toutes les 30 s, analyse deux sources pour détecter des IP anormales qui
# tapent sur la stack, et envoie un rapport (embed) sur le webhook Discord staff.
#
# Source 1 — pare-feu netfilter : log kernel "UFW BLOCK" (vraies IP externes,
# probes, scanners, SYN floods résiduels au niveau réseau).
#
# Source 2 — reverse-proxy nginx : refus HTTP 429/503 émis par les limites
# par IP réelle (limit_req / limit_conn). Comme la terminaison TLS a
# été déplacée vers nginx (mode funnel tcp brut + PROXY protocol),
# c'est ICI qu'apparaissent les attaquants d'applicatif (bursts,
# h2load, etc.) que le pare-feu réseau ne voit plus. On relit le log
# à partir d'un offset pour ne traiter que les lignes nouvelles.
#
# Anti-re-spam : une IP déjà signalée n'est renvoyée que si son compteur de
# tentatives a augmenté depuis le dernier rapport.
set -o pipefail
WEBHOOK="https://discord.com/api/webhooks/***REMOVED***"
INTERVAL=30
STATE="/home/riricdev/zektyc/scripts/.ipwatch-state" # IP:compteur déjà signalés
NGINX_LOG="/var/log/nginx/access.log"
# Taille (octets) du bloc de fin du log relue à chaque cycle puis filtrée au
# timestamp : couvre INTERVAL s même au pic (h2load). Rotation-safe, pas d'offset.
TL_BACK=16777216 # 16 Mo
# IP publique ? 0=oui(suspecte possible) 1=non. On exclut aussi le CGNAT
# Tailscale (100.64.0.0/10) pour ne pas signaler le VPN légitime / les tests.
is_public() {
local ip="$1" a b c d
IFS='.' read -r a b c d <<<"$ip"
[[ "$a" == "127" ]] && return 1
[[ "$a" == "10" ]] && return 1
[[ "$a" == "192" && "$b" == "168" ]] && return 1
[[ "$a" == "172" && "$b" -ge 16 && "$b" -le 31 ]] && return 1
[[ "$a" == "100" && "$b" -ge 64 && "$b" -le 127 ]] && return 1
[[ "$a" == "169" && "$b" == "254" ]] && return 1
[[ "$a" -ge 224 && "$a" -le 239 ]] && return 1
[[ "$a" == "0" || "$a" == "255" ]] && return 1
return 0
}
# Envoie l'embed. Retour 0 si envoyé.
send_report() {
local report_txt="$1" now
now=$(date +"%Y-%m-%d %H:%M:%S")
local json
json=$(jq -n \
--arg title "🚨 IP suspectes détectées" \
--arg desc "$report_txt" \
--arg time "$now" \
--argjson color 15548997 \
'{embeds:[{title:$title,description:$desc,color:$color,footer:{text:("Zektyc Sécu — "+$time)}}]}')
curl -s -m 10 -H "Content-Type: application/json" -X POST "$WEBHOOK" -d "$json" >/dev/null 2>&1
}
# Charge l'état des IP déjà signalées (format "IP compteur" par ligne)
load_state() {
declare -gA last=()
if [[ -f "$STATE" ]]; then
while read -r k v; do last["$k"]="$v"; done <"$STATE"
fi
}
# Sauvegarde l'état
save_state() {
: >"$STATE"
for k in "${!last[@]}"; do printf '%s %s\n' "$k" "${last[$k]}" >>"$STATE"; done
}
# --- Source 2 : relit la FIN du log nginx et garde les refus des 30 dernières s ---
# Pas d'offset : on lit un bloc fixe de la fin du fichier (TL_BACK, dimensionné
# pour couvrir INTERVAL s même au pic de trafic) puis on ne garde QUE les lignes
# dont l'horodatage est dans la fenêtre [ début, date_actuelle ]. Robuste face
# aux rotations/troncatures, aucun offset à resynchroniser manuellement.
# Tout le filtrage se fait dans UN SEUL awk (pas de fork par ligne).
read_nginx_refus() {
local awkprog ip c ts uals from
from="$(date -d "@$(( now_s - INTERVAL ))" +%d/%b/%Y:%H:%M:%S)"
awkprog='
function ispub(ip, a,b,c,d,p,n) {
n=split(ip,p,"."); if (n!=4) return 0
a=p[1]+0; b=p[2]+0; c=p[3]+0; d=p[4]+0
if (a==127||a==10||(a==192&&b==168)||(a==172&&b>=16&&b<=31)\
||(a==100&&b>=64&&b<=127)||(a==169&&b==254)) return 0
if (a>=224&&a<=239||a==0||a==255) return 0
return 1
}
{
# Gardes de structure : le buffer tail -c commence en plein milieu d une
# ligne -> son 1er enregistrement est un fragment casse. On n accepte que
# les lignes completes : IP v4 en tete + timestamp nginx complet en $3.
if ($1 !~ /^[0-9]{1,3}(\.[0-9]{1,3}){3}$/) next
ts=$3; sub(/^\[/,"",ts)
if (ts !~ /^[0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2}/) next
# nginx logue en local time (+0200) : on n accepte que les lignes >= debut fenetre
if (ts < from) next
code=""
for (i=1;i<=NF;i++)
if ($i ~ /^[0-9]{3}$/ && (i+1)<=NF && $(i+1) ~ /^[0-9]+$/) code=$i
if (code!="429" && code!="503") next
ip=$1
if (!ispub(ip)) next
cnt[ip]++
n=split($0,q,"\"")
ua=(n>=2 ? substr(q[n-1],1,40) : "")
if (ua!="" && !((ip SUBSEP ua) in seen)) { seen[ip SUBSEP ua]=1; ual[ip]=(ual[ip]==""?ua:ual[ip]","ua) }
last[ip]=ts
}
END { for (ip in cnt) print ip "|" cnt[ip] "|" last[ip] "|" ual[ip] }
'
while IFS='|' read -r ip c ts uals; do
cnt["$ip"]=$(( ${cnt["$ip"]:-0} + c ))
[[ "${protos["$ip"]:-}" != *"http"* ]] && protos["$ip"]="${protos["$ip"]:-}http,"
[[ "${dstports["$ip"]:-}" != *"443"* ]] && dstports["$ip"]="${dstports["$ip"]:-}443,"
lastts["$ip"]="$ts"
[[ -n "$uals" ]] && uas["$ip"]="${uals},"
done < <(tail -c "$TL_BACK" "$NGINX_LOG" | awk -v "from=$from" "$awkprog")
}
load_state
echo "IP-watch démarré — fenêtre ${INTERVAL}s/signalement (ufw + nginx), webhook staff (actif)."
while true; do
now_s=$(date +%s)
since=$(( now_s - INTERVAL ))
declare -A cnt=()
declare -A protos=()
declare -A dstports=()
declare -A lastts=()
declare -A uas=()
# --- source 1 : blocs netfilter (log kernel UFW BLOCK) ---
while IFS= read -r ln; do
[[ "$ln" != *"UFW BLOCK"* ]] && continue
if [[ "$ln" =~ \ SRC=([0-9.]+)\ ]]; then
ip="${BASH_REMATCH[1]}"
is_public "$ip" || continue
cnt["$ip"]=$(( ${cnt["$ip"]:-0} + 1 ))
if [[ "$ln" =~ \ PROTO=([A-Za-z0-9]+)\ ]]; then
p="${BASH_REMATCH[1]}"
[[ "${protos["$ip"]:-}" != *"$p"* ]] && protos["$ip"]="${protos["$ip"]:-}$p,"
fi
if [[ "$ln" =~ \ DPT=([0-9]+) ]]; then
d="${BASH_REMATCH[1]}"
[[ "${dstports["$ip"]:-}" != *"$d"* ]] && dstports["$ip"]="${dstports["$ip"]:-}$d,"
fi
if [[ "$ln" =~ ^[A-Z][a-z]{2}\ [0-9]{2}\ [0-9]{2}\:[0-9]{2}\:[0-9]{2} ]]; then
lastts["$ip"]="${BASH_REMATCH[0]}"
fi
fi
done < <(sudo journalctl -k --no-pager --since "@${since}" 2>/dev/null | grep "UFW BLOCK")
# --- source 2 : refus HTTP nginx (429/503 par IP réelle) ---
read_nginx_refus
# --- filtrer : ne signaler que les IP nouvelles ou dont le compteur a augmenté ---
newfound=0
report=""
for ip in "${!cnt[@]}"; do
c="${cnt[$ip]}"
prev="${last["$ip"]:-0}"
if (( c > prev )); then
proto="${protos[$ip]%,}"
dports="${dstports[$ip]%,}"
ua="${uas[$ip]%,}"
report+=$':shield: **`'"${ip}"$'`** — **'"${c}"$'** tentative(s) bloquée(s)\n'
report+=$'└ Sources: `'"${proto:-?}"$'` | Ports cibles: `'"${dports:-?}"$'`\n'
if [[ -n "$ua" ]]; then
report+=$'└ User-Agent: `'"${ua}"$'`\n'
fi
report+=$'└ Dernière tentative: `'"${lastts[$ip]:-?}"$'`\n\n'
last["$ip"]="$c"
((newfound++))
fi
done
if (( newfound > 0 )); then
send_report "$report"
fi
save_state
sleep "$INTERVAL"
done

29
scripts/upnp-watch.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/bash
UPNP_URL="http://192.168.1.254:5678/desc/root"
LOCAL_IP="192.168.1.50"
LOG_TAG="upnp-watch"
log() {
logger -t "$LOG_TAG" "$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
MAPPINGS=$(upnpc -u "$UPNP_URL" -l 2>/dev/null)
if [ -z "$MAPPINGS" ]; then
log "ERREUR: passerelle UPnP injoignable"
exit 1
fi
for PORT in 80 443; do
if echo "$MAPPINGS" | grep -q "TCP.*${PORT}->${LOCAL_IP}:${PORT}"; then
log "OK: port ${PORT}/TCP déjà ouvert"
else
log "port ${PORT}/TCP absent -> ouverture"
RESULT=$(upnpc -u "$UPNP_URL" -a "$LOCAL_IP" "$PORT" "$PORT" TCP 2>&1 | tail -1)
if echo "$RESULT" | grep -q "is redirected"; then
log "port ${PORT}/TCP ouvert avec succès"
else
log "ECHEC ouverture ${PORT}/TCP: $RESULT"
fi
fi
done

64
scripts/uptime-check.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Zektyc — moniteur de disponibilité (uptime checker)
# Vérifie toutes les 5 secondes le code HTTP de réponse de la homepage.
# - Code 2xx/3xx -> online
# - Code 000 (échec connexion / funnel coupé), 5xx (erreur serveur) ou autre
# code d'échec -> incident technique
set -uo pipefail
URL="https://riricdev.tail5ea5cd.ts.net/"
WEBHOOK="https://discord.com/api/webhooks/***REMOVED***"
STATE_FILE="/home/riricdev/zektyc/scripts/.uptime-state"
INTERVAL=5
# Un code HTTP est "online" s'il est 2xx/3xx, ou 429 (rate limit = serveur up,
# juste throttlé). Tout le reste (000, 4xx hors 429, 5xx) = down.
http_is_online() {
local code="$1"
case "$code" in
2*|3*|429) return 0 ;; # 200-399 et 429 : online
*) return 1 ;; # 000, 4xx (hors 429), 5xx : down
esac
}
down() {
local payload
payload=$(printf '{"content":"🔴 **Zektyc** — incident technique."}' )
curl -s -m 10 -H "Content-Type: application/json" -X POST "$WEBHOOK" -d "$payload" >/dev/null 2>&1 || true
echo down >"$STATE_FILE"
}
up() {
local payload
payload=$(printf '{"content":"🟢 **Zektyc** — le site est de nouveau en ligne."}' )
curl -s -m 10 -H "Content-Type: application/json" -X POST "$WEBHOOK" -d "$payload" >/dev/null 2>&1 || true
echo up >"$STATE_FILE"
}
if [[ ! -f "$STATE_FILE" ]]; then
echo "unknown" >"$STATE_FILE"
fi
while true; do
code="$(curl -s -o /dev/null -w '%{http_code}' -m 10 "$URL" 2>/dev/null || echo 000)"
code="${code:-000}"
state="$(cat "$STATE_FILE")"
if http_is_online "$code"; then
if [[ "$state" != "up" ]]; then
up
fi
else
if [[ "$state" != "down" ]]; then
# On n'envoie qu'une fois la notification de panne (quand on était up).
if [[ "$state" == "up" ]]; then
down
else
# État initial inconnu : on se contente de mémoriser sans notifier.
echo down >"$STATE_FILE"
fi
fi
fi
sleep "$INTERVAL"
done