Skip to content

aakri0/Levee

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Levee

A small, demonstrable DDoS mitigation engine. Levee sits in front of a backend as an inline TCP proxy and decides, per connection, whether to drop or forward the traffic. It uses a counting Bloom filter to track known-bad sources and a per-IP leaky bucket for rate limiting. A Scapy sniffer thread reports live traffic stats, and a Flask dashboard makes every decision visible in real time.

client ──► [ Levee :8080 ] ──► [ backend :8080 ]
              │
              ├── Allowlist       (trusted IPs → always forward)
              ├── Counting Bloom  (known bad IPs → drop, per-IP TTL)
              ├── Leaky bucket    (per-IP rate → drop on overflow, add to bloom)
              ├── Reaper thread   (expires each block on its own timer)
              ├── Scapy sniffer   (observability: pps, proto mix)
              └── Flask dashboard (:5001) + REST API + /metrics

Quick start

Requires Docker.

./start.sh up
open http://localhost:5001

./start.sh legit                       # send 5 legitimate requests
RATE=300 DURATION=10 ./start.sh attack  # run the flooder
./start.sh logs                         # tail mitigation logs
./start.sh down                         # tear everything down

How it works

The mitigation container is the public face of the system. Every accepted TCP connection is evaluated by the decision engine, in this order:

  1. Allowlist. If the source IP is trusted, forward immediately.
  2. Manual block. If the IP was blocked by hand, drop it.
  3. Bloom filter. If the IP is in the counting Bloom filter, drop it.
  4. Leaky bucket. Add one token. If the bucket overflows, add the IP to the Bloom filter (with a TTL) and drop it.
  5. Otherwise, splice the connection to the backend.

Temporary, per-IP blocks

The Bloom filter is a counting Bloom filter: each slot is a small counter rather than a single bit, which is what makes per-entry removal possible. Every automatically blocked IP gets its own TTL (BLOCK_TTL_SEC). A reaper thread expires each block individually when its timer elapses; on expiry the IP is decremented out of the Bloom filter and dropped from the leaky bucket, so it starts fresh. The reaper idles whenever the leaky bucket tracks no IPs. The dashboard shows a live per-IP countdown to each block's expiry.

Observability

A Scapy thread sniffs the mitigation interface for observability only — packets/sec and protocol mix. Enforcement happens at the proxy layer, where dropping a connection means closing the socket without ever opening one upstream.

Configuration

Set on the mitigation service in docker-compose.yml:

Variable Default Meaning
BUCKET_CAPACITY 20 Max tokens per IP before overflow
BUCKET_LEAK_RATE 10 Tokens drained per second
BLOOM_BITS 100000 Number of counter slots (m)
BLOOM_HASHES 4 Number of hash functions (k)
BLOCK_TTL_SEC 60 How long an IP stays blocked before its block expires
TARGET_HOST 172.30.0.10 Backend host the proxy forwards to
TARGET_PORT 8080 Backend port
ALLOWLIST (empty) Comma-separated IPs that always bypass blocking
ALERT_BPS 20 Blocks/sec that trips the dashboard "under attack" status

At m=100,000 / k=4, the estimated false-positive rate stays well under 0.001% for up to ~1,000 blocked IPs. The dashboard shows the live estimate.

When is traffic classified as an attack?

A source IP is classified as abusive by its per-IP leaky bucket. Each connection adds one token; the bucket drains at BUCKET_LEAK_RATE tokens per second (default 10/s) and overflows once it would exceed BUCKET_CAPACITY (default 20).

So, for a source sending at a sustained rate of R requests/second:

  • R ≤ 10 req/s — the bucket drains as fast as it fills; the source is never classified as an attack.
  • R > 10 req/s — the bucket fills at R − 10 tokens/s and overflows after roughly 20 / (R − 10) seconds. The first 20 requests are tolerated as a burst allowance.

On overflow the IP is added to the Bloom filter and every later connection is dropped (bloom_hit) until its block TTL expires. In short: the per-IP attack threshold is a sustained rate above BUCKET_LEAK_RATE (10 req/s), after a burst of BUCKET_CAPACITY (20 requests).

Separately, the dashboard's red "UNDER ATTACK" status is a system-wide indicator (it does not change enforcement) and trips when the measured blocks/second crosses ALERT_BPS (default 20).

Where to change it

  • At runtime (recommended): edit the environment variables on the mitigation service in docker-compose.ymlBUCKET_LEAK_RATE, BUCKET_CAPACITY, and ALERT_BPS — then restart.
  • In code (the defaults): the leaky-bucket thresholds are set in mitigation/main.py, in the LeakyBucketRegistry(...) constructor call — env_float("BUCKET_CAPACITY", 20.0) and env_float("BUCKET_LEAK_RATE", 10.0). The "under attack" threshold default is in mitigation/dashboard.py: alert_threshold_bps = int(os.environ.get("ALERT_BPS", 20)).

Project layout

mitigation/
  main.py          wiring + thread orchestration (sampler, reaper)
  bloom_filter.py  counting Bloom filter, double-hashing, add/remove
  leaky_bucket.py  per-IP buckets, LRU bounded
  blocker.py       evaluate(ip) → (allowed, reason) + per-IP block table
  proxy.py         TCP listener, splices to backend on accept
  sniffer.py       scapy thread, stats only
  geoip.py         best-effort country/ASN lookup, cached
  dashboard.py     Flask dashboard + REST API + /metrics
target/server.py   trivial echo backend
attacker/flood.py  multi-threaded TCP connect flood

Dashboard

The dashboard at http://localhost:5001 auto-refreshes every second and shows:

  • A pulsing system-status panel — green "SYSTEM PROTECTED", red "UNDER ATTACK" once blocks/sec crosses ALERT_BPS.
  • Decision counters (allowed, blocked, block rate, reasons).
  • Packets/second received, and leaky-bucket throughput — leak rate, per-IP capacity, packets remaining in buckets, and packets forwarded.
  • Counting Bloom filter stats (IPs added/removed, slots used, FP rate).
  • The live allowlist with Add / Remove controls.
  • A rolling 60-second traffic chart (packets/sec and blocks/sec).
  • Active blocks with a per-IP TTL countdown.
  • A manual block control and the recent-blocks table with Geo-IP data.

Allowlist

IPs on the allowlist skip the Bloom filter and leaky bucket entirely — they are always forwarded and counted as allowed. Useful for trusted monitoring or health-check sources that should never be throttled.

Seed it at startup with the ALLOWLIST env var, or manage it live from the dashboard's Allowlist card, or via the API:

curl -X POST localhost:5001/api/allow   -H 'Content-Type: application/json' -d '{"ip":"172.30.0.50"}'
curl -X POST localhost:5001/api/unallow -H 'Content-Type: application/json' -d '{"ip":"172.30.0.50"}'

Manual block / unblock

Block or unblock an IP by hand:

curl -X POST localhost:5001/api/block   -H 'Content-Type: application/json' -d '{"ip":"203.0.113.7"}'
curl -X POST localhost:5001/api/unblock -H 'Content-Type: application/json' -d '{"ip":"203.0.113.7"}'

Manual blocks are kept in a separate set, so they never expire and are cleared only by unblock. Automatic blocks (bucket overflow) instead expire on their own per-IP TTL. Each blocked IP in recent_blocks is enriched with a best-effort Geo-IP lookup (country + ASN); private/reserved addresses are labelled locally without a network call.

API

Method & path Purpose
GET / The HTML dashboard
GET /api/stats Full JSON snapshot of engine state
GET /metrics Prometheus-format metrics (see below)
GET /healthz Liveness probe — {"status": "ok", "uptime_sec": N}
GET /api/blocks.csv Recent blocks as a downloadable CSV
POST /api/block Manually block an IP — body {"ip": "..."}
POST /api/unblock Remove a manual block — body {"ip": "..."}
POST /api/allow Add an IP to the allowlist — body {"ip": "..."}
POST /api/unallow Remove an IP from the allowlist — body {"ip": "..."}

GET /api/stats returns a JSON snapshot:

{
  "uptime_sec": 60,
  "packets_total": 5200,
  "packets_per_sec": 5,
  "proto_counts": {"TCP": 5200},
  "allowed": 25,
  "blocked": 1305,
  "block_reasons": {"bucket_overflow": 1, "bloom_hit": 1304},
  "bloom": {"added": 1, "bits": 100000, "hashes": 4, "removed": 0,
            "fill_ratio": 0.00004, "estimated_fp_rate": 0.0},
  "bucket": {"leak_rate": 10.0, "capacity": 20.0,
             "packets_remaining": 6.0, "tracked_ips": 2},
  "active_blocks": [{"ip": "172.30.0.30", "expires_in": 42.0}],
  "top_buckets": [{"ip": "172.30.0.30", "level": 0.0}],
  "recent_blocks": [{"ts": 1778..., "ip": "172.30.0.30", "reason": "bloom_hit",
                     "geo": {"country": "Private network", "asn": "-"}}],
  "allowlist": ["172.30.0.50"],
  "manual_blocks": ["203.0.113.7"],
  "history": [{"ts": 1778..., "pps": 5, "bps": 0}]
}

Prometheus metrics

Levee exposes its counters in the Prometheus text exposition format at GET /metrics, so they can be scraped, graphed, and alerted on.

Accessing the metrics

1. Quick look from the shell — just curl the endpoint:

curl http://localhost:5001/metrics

Example output:

# HELP levee_uptime_seconds Time since the mitigation engine started.
# TYPE levee_uptime_seconds gauge
levee_uptime_seconds 312
# HELP levee_packets_total Packets observed by the sniffer.
# TYPE levee_packets_total counter
levee_packets_total 5200
# HELP levee_packets_per_second Packets observed in the last second.
# TYPE levee_packets_per_second gauge
levee_packets_per_second 5
# HELP levee_connections_allowed_total Connections forwarded to the backend.
# TYPE levee_connections_allowed_total counter
levee_connections_allowed_total 25
# HELP levee_connections_blocked_total Connections dropped at the proxy.
# TYPE levee_connections_blocked_total counter
levee_connections_blocked_total 1305
# HELP levee_blocks_by_reason_total Blocked connections grouped by reason.
# TYPE levee_blocks_by_reason_total counter
levee_blocks_by_reason_total{reason="bucket_overflow"} 1
levee_blocks_by_reason_total{reason="bloom_hit"} 1304
# HELP levee_bloom_added Total IPs inserted into the Bloom filter.
# TYPE levee_bloom_added gauge
levee_bloom_added 1
# HELP levee_bloom_estimated_fp_rate Estimated Bloom false-positive rate.
# TYPE levee_bloom_estimated_fp_rate gauge
levee_bloom_estimated_fp_rate 0.0

2. Scrape it with a Prometheus server. Add Levee as a scrape target in prometheus.yml:

scrape_configs:
  - job_name: levee
    scrape_interval: 5s
    static_configs:
      - targets: ['localhost:5001']

If you run Prometheus inside Docker, replace localhost with the mitigation container's address — host.docker.internal:5001, or 172.30.0.20:5001 if Prometheus joins the levee_net bridge network.

Reload Prometheus, then confirm the target is UP under Status → Targets. You can now query the metrics in the Prometheus expression browser or in Grafana, for example:

rate(levee_connections_blocked_total[1m])   # blocks per second
levee_packets_per_second                    # live packet rate
levee_bloom_estimated_fp_rate                # Bloom false-positive estimate

Metrics reference

Metric Type Meaning
levee_uptime_seconds gauge Seconds since the engine started
levee_packets_total counter Total packets observed by the sniffer
levee_packets_per_second gauge Packets seen in the last second
levee_connections_allowed_total counter Connections forwarded to the backend
levee_connections_blocked_total counter Connections dropped at the proxy
levee_blocks_by_reason_total counter Blocks, labelled by reason
levee_bloom_added gauge Total IPs inserted into the Bloom filter
levee_bloom_estimated_fp_rate gauge Estimated Bloom false-positive rate

Note on "logs": /metrics exposes numeric counters, not a text log. For a human-readable record of recent blocks use the dashboard's recent-blocks table or GET /api/blocks.csv; for the engine's stdout log use ./start.sh logs.

Limitations

This is a teaching-grade demo, not a production WAF. See SECURITY.md for the full security model. In particular:

  • A Bloom false positive (extremely rare) blocks an IP that was never in the per-IP table, so it cannot be expired by the reaper.
  • The per-IP bucket registry is LRU-bounded at 10k entries.
  • Enforcement is at L4 (TCP accept), so a pure SYN flood that never completes the handshake is observed by the sniffer but not throttled.
  • The engine is a single Python process — the GIL caps throughput at one core.

License

MIT.

About

A lightweight DDoS mitigation proxy that filters traffic per connection using a Bloom filter for known bad IPs and a leaky bucket for rate limiting. Suspicious sources are dropped before reaching the backend, while a Scapy-powered dashboard provides live traffic insights.

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors