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:
commit
8e7ec41f18
104 changed files with 19256 additions and 0 deletions
124
scripts/antiflood.sh
Executable file
124
scripts/antiflood.sh
Executable 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
187
scripts/ipwatch.sh
Executable 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
29
scripts/upnp-watch.sh
Executable 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
64
scripts/uptime-check.sh
Executable 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
|
||||
Loading…
Reference in a new issue