From 0c3038985866c39f14f7f0f2e3372eb9288c0230 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 14:33:59 +0700 Subject: [PATCH] feat: DDoS detector for ports 80/443 with Telegram alert (cron 1min, ss-based) --- README.md | 58 +++++++++++++++++++ ddos_detector.py | 148 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 README.md create mode 100644 ddos_detector.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..77419fa --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# DDoS Detector (port 80/443 → Telegram alert) + +Script Python ringan yang monitor koneksi aktif ke **port 80 & 443** tiap 1 menit. +Kalau ada IP yang bomb (> batas) atau total koneksi port melebihi batas, kirim **alert ke Telegram**. +Pakai `ss` (bawaan Linux), gak butuh dependency pip. + +## Cara kerja + +1. Cron jalanin `ddos_detector.py` tiap 1 menit +2. Hitung koneksi per-IP ke port 80 & 443 (`ss -tn src :80 or src :443`) +3. Kalau `koneksi 1 IP > CONN_PER_IP` atau `total port > CONN_TOTAL` → alert Telegram +4. State file cegah spam: cuma alert pas **crossing threshold**, + kirim "cleared" kalau balik normal + +## Config + +Semua via env (atau file `/root/stock-monitor/.env`): + +| Var | Default | Keterangan | +|-----|---------|-----------| +| `TELEGRAM_BOT_TOKEN` | (dari .env) | token bot | +| `TELEGRAM_CHAT_ID` | `5722352727` | chat tujuan | +| `DDOS_PER_IP` | `100` | batas koneksi per IP per port | +| `DDOS_TOTAL` | `500` | batas total koneksi per port | + +## Install + +```bash +# taruh script +cp ddos_detector.py /root/ddos_detector.py + +# pastikan python3 + ss ada +which python3 ss + +# crontab tiap 1 menit +(crontab -l 2>/dev/null; echo "* * * * * /usr/bin/python3 /root/ddos_detector.py >> /root/ddos_detector.log 2>&1") | crontab - +``` + +## Output + +- Log: `/root/ddos_detector.log` (stats tiap menit, gak ada secret) +- State: `/root/ddos_state.json` (flag alerting, cegah spam) +- Telegram: + - `🚨 DDoS ALERT @ ...` + top IP kalau anomali + - `✅ DDoS alert cleared @ ...` kalau balik normal + +## Test manual + +```bash +python3 /root/ddos_detector.py +# lihat log +tail -f /root/ddos_detector.log +``` + +## Catatan + +- Hanya monitor 80/443 (permukaan publik). Kalau mau port lain, edit `PORTS` di script. +- Threshold default konservatif — turunin `DDOS_PER_IP` (misal 50) kalau mau lebih sensitif. +- Gak auto-mitigasi (cuma alert). Buat mitigasi: iptables rate-limit / Cloudflare "Under Attack". diff --git a/ddos_detector.py b/ddos_detector.py new file mode 100644 index 0000000..6a7aa3a --- /dev/null +++ b/ddos_detector.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""DDoS detector for public web ports (80/443). +Runs every minute via cron. Counts active connections per IP on 80/443, +alerts Telegram if a single IP exceeds CONN_PER_IP or total exceeds CONN_TOTAL. +State file prevents alert spam (only alerts on threshold crossing). +""" +import os +import re +import subprocess +import datetime + +# Load telegram creds from stock-monitor .env (or env) +def load_env(path): + env = {} + try: + with open(path) as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, v = line.split("=", 1) + env[k.strip()] = v.strip() + except FileNotFoundError: + pass + return env + +ENV = load_env("/root/stock-monitor/.env") +BOT = os.environ.get("TELEGRAM_BOT_TOKEN") or ENV.get("TELEGRAM_BOT_TOKEN", "") +CHAT = os.environ.get("TELEGRAM_CHAT_ID") or ENV.get("TELEGRAM_CHAT_ID", "5722352727") + +PORTS = [80, 443] +CONN_PER_IP = int(os.environ.get("DDOS_PER_IP", "100")) # alert if 1 IP > this +CONN_TOTAL = int(os.environ.get("DDOS_TOTAL", "500")) # alert if total on a port > this +STATE_FILE = "/root/ddos_state.json" + +# ports currently exposed to public (from ss output) +def get_connections(): + """Return dict: port -> {ip: count}""" + out = subprocess.run( + ["ss", "-tn", "src", ":80", "or", "src", ":443"], + capture_output=True, text=True + ).stdout + result = {p: {} for p in PORTS} + # ss -tn src :80 or src :443 -> lines like: + # ESTAB 0 0 127.0.0.1:80 1.2.3.4:55555 + for line in out.splitlines()[1:]: + parts = line.split() + if len(parts) < 4: + continue + local = parts[3] # e.g. 0.0.0.0:80 or [::]:443 + remote = parts[4] + # extract port from local + m = re.search(r":(\d+)$", local) + if not m: + continue + port = int(m.group(1)) + if port not in result: + continue + # extract IP from remote (strip port) + ip = remote.rsplit(":", 1)[0] + ip = ip.strip("[]") + result[port][ip] = result[port].get(ip, 0) + 1 + return result + + +def send_telegram(msg): + if not BOT: + print("NO BOT TOKEN, skip send") + return + import urllib.request + import urllib.parse + url = f"https://api.telegram.org/bot{BOT}/sendMessage" + data = urllib.parse.urlencode({"chat_id": CHAT, "text": msg}).encode() + req = urllib.request.Request(url, data=data, method="POST") + try: + urllib.request.urlopen(req, timeout=10) + except Exception as e: + print(f"telegram send failed: {e}") + + +def load_state(): + import json + try: + with open(STATE_FILE) as f: + return json.load(f) + except (FileNotFoundError, ValueError): + return {} + + +def save_state(s): + import json + with open(STATE_FILE, "w") as f: + json.dump(s, f) + + +def main(): + conns = get_connections() + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + alerts = [] + for port in PORTS: + ip_map = conns[port] + total = sum(ip_map.values()) + # top offenders + top = sorted(ip_map.items(), key=lambda x: -x[1])[:5] + for ip, cnt in top: + if cnt > CONN_PER_IP: + alerts.append( + f"⚠️ Port {port}: IP {ip} = {cnt} koneksi (batas {CONN_PER_IP})" + ) + if total > CONN_TOTAL: + alerts.append( + f"⚠️ Port {port}: TOTAL {total} koneksi (batas {CONN_TOTAL})" + ) + + state = load_state() + new_alert = False + if alerts: + # only alert if we weren't already in alert state (avoid spam) + if not state.get("alerting"): + new_alert = True + state["alerting"] = True + state["last_alerts"] = alerts + else: + if state.get("alerting"): + # send "cleared" message once + send_telegram( + f"✅ DDoS alert cleared @ {now}\n" + f"Port 80: {sum(conns[80].values())} koneksi\n" + f"Port 443: {sum(conns[443].values())} koneksi\n" + f"(batas per-IP {CONN_PER_IP}, total {CONN_TOTAL})" + ) + state["alerting"] = False + + if new_alert: + top80 = sorted(conns[80].items(), key=lambda x: -x[1])[:5] + top443 = sorted(conns[443].items(), key=lambda x: -x[1])[:5] + msg = f"🚨 DDoS ALERT @ {now}\n\n" + "\n".join(alerts) + "\n\n" + msg += f"Top IP port 80: {top80}\n" + msg += f"Top IP port 443: {top443}\n" + msg += f"(batas per-IP {CONN_PER_IP}, total {CONN_TOTAL})" + send_telegram(msg) + + save_state(state) + # debug log (no secrets) + print(f"{now} 80={sum(conns[80].values())} 443={sum(conns[443].values())} alert={state.get('alerting')}") + + +if __name__ == "__main__": + main()