feat: DDoS detector for ports 80/443 with Telegram alert (cron 1min, ss-based)

This commit is contained in:
root
2026-08-21 14:33:59 +07:00
commit 0c30389858
2 changed files with 206 additions and 0 deletions

58
README.md Normal file
View File

@@ -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".

148
ddos_detector.py Normal file
View File

@@ -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()