Introduction: The Moment Your Bandwidth Is Saturated, Your Business Is Bleeding
Anyone who runs a VPS has been here:
- 3 AM: an upstream SaaS dependency fails, your service goes into a retry storm, egress jumps from 200 Mbps to the 8 Gbps cap, the provider rate-limits you, and every real user hits 502s;
- A crawler bypasses your CDN and hits the origin directly, pulls 400 GB of static assets in 10 minutes, and doubles your monthly bill;
- A backup job forgets its rate limit, and
rsynchogs every last bit of bandwidth — even SSH feels like a slideshow; - A DDoS pulse that a fixed
tclimit simply cannot stop, because the attack rides the same ports as legitimate traffic.
The root problem with traditional traffic shaping is this: the policy is static.
You configure tc with classfq + CBQ, iptables with hashlimit, nftables with per-IP limits — all of it “rule-based.” But real-world traffic anomalies don’t follow rules:
- Attackers use HTTP/2 today, gRPC tomorrow, WebSocket the day after;
- Normal business peaks shift week to week (payroll days, release days, flash sales);
- A retry storm caused by upstream failure and a DDoS look almost identical on a bandwidth graph — but their correct responses are opposites (one needs a hard cutoff, the other needs to keep real users through).
A fixed limit is, fundamentally, managing today’s traffic with yesterday’s knowledge.
This article presents an AI-driven dynamic traffic-shaping system: a local LLM “reads” the live traffic, distinguishes anomaly patterns, auto-generates QoS policies (rate limits, priority queues, connection caps, temporary bans), and applies them on the VPS with second-level effect — then automatically rolls back when the situation clears. All inference runs locally via Ollama + Qwen2.5: zero API cost, zero data leaving your machine.
1. Why Fixed QoS Policies Fall Short
1.1 The Four Blind Spots of Static Policies
| Blind spot | What happens | Consequence |
|---|---|---|
| Unknown patterns | Anything not written into a rule is invisible | Bandwidth saturation, bill explosion |
| Normal peaks get collateral damage | Fixed limits apply during business peaks too | Real users get degraded |
| Policy conflicts | Stacked iptables/nftables rules behave unpredictably | Hard to debug |
| No memory on recovery | After an incident the policy stays in place until a human cleans it up | Long-term false positives |
1.2 Three Capabilities AI Adds
- Pattern recognition: the LLM reads live traffic features (per-source rate, burstiness, duration, packet-size distribution) and labels the semantics — “crawler”, “retry storm”, “DDoS”, “normal peak”
- Policy generation: from the label + business SLOs (e.g. “normal users p99 < 200 ms”), the LLM auto-generates
tc/iptables/nftablespolicies with an estimated blast radius - Closed-loop verification: after a policy goes live, the LLM watches a 5–15 minute window, confirms “anomaly suppressed, normal traffic unharmed,” and auto-rolls back if not
2. System Architecture
┌───────────────────────────────────────────────────────────────┐
│ VPS host (Ubuntu 24.04) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Collector │──▶│ Feature │──▶│ LLM infer │──▶│Policy │ │
│ │ (eBPF / │ │ extractor │ │ (Ollama + │ │ engine │ │
│ │ netstat) │ │ (Python) │ │ Qwen2.5) │ │(tc/ipt)│ │
│ └───────────┘ └───────────┘ └───────────┘ └─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────────┐ │
│ │ Prometheus│ │ Watchdog │──▶ roll- │
│ │ (bandwidth│ │ (circuit │ back / │
│ │ /conn) │ │ breaker) │ hold │
│ └───────────┘ └───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Decision log + effect review (LLM 2nd pass) │──▶ Telegram│
│ └───────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Core modules:
- Collector: eBPF (
bpftool/ off-the-shelf probes) orip -s link+conntrackperiodic snapshots; outputs per-source-IP rate, connection count, packet-size distribution - Feature extractor: Python script compresses raw data into LLM-friendly JSON (Top 50 sources + global stats), kept under 2 KB
- LLM inference: Qwen2.5 analyzes the feature JSON and outputs “anomaly label + confidence + proposed policy + expected impact”
- Policy engine: translates the LLM’s proposal into
tc qdisc/iptables -m hashlimit/nftablescommands, validates syntax in dry-run, then applies atomically - Watchdog (circuit breaker): within a 5-minute post-application window, if “the protected service’s p99 degrades > 30%” it rolls back immediately
3. Key Implementation Details
3.1 Feature Extraction: Compress “Raw Traffic” into LLM-Readable JSON
Feeding raw traffic logs to an LLM blows the context window. The right move is to distill semantic features first:
def build_features(collect_window_seconds=300):
"""Returns compressed feature JSON, usually < 2KB"""
top_sources = get_top_sources_by_egress(n=50) # top 50 by egress
global_stats = {
"egress_mbps_p50": ...,
"egress_mbps_p95": ...,
"egress_mbps_burst": ..., # peak within window
"conn_total": ...,
"http_status_5xx_ratio": ..., # if Nginx exposes it
}
return {
"ts": now_iso(),
"window_seconds": collect_window_seconds,
"top_sources": [
{"ip": "203.0.113.7", "egress_mbps": 340,
"conns": 12, "avg_pkt_bytes": 1400,
"burst_ratio": 8.2, "seen_for_minutes": 3}
# ...
],
"global": global_stats,
}
Key fields:
burst_ratio: current rate / that source’s 7-day same-time-of-day baseline (kept in local SQLite); >5 is a strong anomaly signalseen_for_minutes: duration, distinguishes “one-off blip” from “sustained attack”avg_pkt_bytes: many small packets → likely SYN flood / long-poll probing; large packets → file transfer / crawler pulling assets
3.2 LLM Prompt Design
You are a VPS traffic-security expert. Here is the last 5 minutes of
traffic feature JSON:
{features_json}
Business SLO: normal users p99 < 200ms, egress cap 800Mbps,
and above 600Mbps prioritize long-lived connections on ports 80/443.
Output JSON only (no extra text):
{
"anomaly_type": "crawler | retry_storm | ddos | backup_spike | normal_peak | unknown",
"confidence": 0.0-1.0,
"evidence": ["burst_ratio=8.2", "avg_pkt=1400", ...],
"actions": [
{"tool": "tc", "desc": "limit 203.0.113.7 to 20Mbps",
"cmd": "tc qdisc add dev eth0 handle 1:0 fq ...",
"scope": "specific", "revert_after_minutes": 30},
{"tool": "iptables", "desc": "global 500Mbps backstop",
"cmd": "...", "scope": "global", "revert_after_minutes": 60}
],
"safety_check": {"expected_p99_impact": "normal users unaffected",
"rollback_trigger": "p99 > 300ms for 3min"}
}
Design notes:
- Force JSON output + a
safety_checkfield, so every generated policy ships with its own “rollback condition” instead of guessing later - Give SLO constraints: the LLM must consider “real business first” when generating policies, no blanket limits
scopedistinction:specific(only the bad source) vsglobal(backstop) — prefer specific
3.3 Policy Engine: LLM Output → Executable Commands
The LLM emits proposals, not ready-to-run commands. The policy engine’s job:
def apply_strategy(action, dry_run=True):
# 1. Syntax check: tc -s qdisc show / iptables -C
# 2. Idempotency: if same qdisc exists, del then add
# 3. dry-run mode: print only, never execute (default on first deploy)
# 4. Atomic apply: tc uses "build mirror + verify → switch" to avoid
# a momentary outage
# 5. Log the op + schedule auto-revert (cron or systemd-timer)
tc layer (fine-grained shaping):
# fq + ingress classifier, limit a specific source
tc qdisc add dev eth0 root handle 1: fq
tc filter add dev eth0 parent 1:0 protocol ip u32 \
match ip src 203.0.113.7/32 flowid 1:10
tc class add dev eth0 parent 1:0 classid 1:10 htb rate 20mbit
iptables layer (coarse backstop):
# Global 500Mbps cap (hashlimit: per-connection + global, two dimensions)
iptables -t mangle -A FORWARD -m hashlimit \
--hashlimit-above 500mbit --hashlimit-burst 2mbit -j DROP
Why both layers? tc is precise but stateful and fiddly; iptables is a dumb but reliable backstop against “the policy didn’t take effect” windows. The LLM emits both by default, with different revert_after_minutes (fine-grained 30 min, backstop 60 min) to avoid long-lived collateral damage.
3.4 Watchdog (The Critical Safety Mechanism)
The LLM will make mistakes. There must be a hard rule independent of the LLM as a backstop:
class Watchdog:
"""5-minute observation window after a policy is applied"""
def observe(self, strategy_id, window_min=5):
baseline = capture_p99_snapshot() # p99 before apply
for _ in range(window_min * 10):
sleep(6)
if current_p99() > baseline * 1.3: # 30% degradation → rollback
rollback(strategy_id)
report_to_telegram(strategy_id, "rollback")
return
# Window passed: mark "verified", may extend revert time
This is non-optional. The LLM might misclassify a flash-sale peak as a crawler; without the watchdog, real users get rate-limited along with it.
3.5 Decision Log & Second-Pass Review
Every applied policy is written to local SQLite:
CREATE TABLE decisions (
ts, anomaly_type, confidence, source_ip, cmd,
dry_run, applied, p99_before, p99_after, rolled_back
);
Daily at 06:00, an LLM “self-review” pass pulls all decisions from the last 24 h and outputs:
- Misjudgment rate (rolled_back / total)
- Recurring misjudgment patterns (“treated a release-day traffic bump as a crawler”)
- Baseline update suggestions (write newly confirmed normal-peak patterns into the baseline)
Results go to Telegram, and the next day’s prompt constraints are tuned accordingly.
4. Deployment (VPS-Tested)
Tested on a 4C8G VPS (Ubuntu 24.04, 800 Mbps egress cap):
# 1. Dependencies
sudo apt install -y iproute2 iptables-persistent ipset
# 2. Ollama
docker run -d --name ollama -v ollama:/root/.ollama -p 11434:11434 ollama/ollama
docker exec -it ollama ollama pull qwen2.5:7b
# 3. Deploy the traffic shaper
git clone https://git.example.com/myorg/vps-traffic-shaper.git
cd vps-traffic-shaper
docker compose up -d
# modules: collector, feature-extractor, llm-strategy-engine,
# tc-applier, iptables-applier, watchdog, reviewer, telegram-reporter
# 4. First run in dry-run
curl -X POST localhost:8080/api/v1/run -d '{"dry_run": true}'
# inspect /opt/vps-traffic-shaper/decisions.log to confirm LLM judgments look sane
# 5. Switch to live mode (careful!)
curl -X POST localhost:8080/api/v1/mode -d '{"mode": "live", "min_confidence": 0.8}'
Measured results (2 weeks of production):
| Metric | Value |
|---|---|
| Feature extraction latency | < 0.3 s |
| LLM inference latency | ~2.5 s (Qwen2.5-7B on 4C8G) |
| tc policy apply latency | < 1 s |
| Anomaly events intercepted | 14 in 2 weeks (7 crawlers, 4 retry storms, 3 DDoS pulses) |
| False-positive rollbacks | 2 (a flash-sale peak mislabeled as crawler) |
| Egress bandwidth P95 | 620 Mbps → 410 Mbps |
| Monthly bandwidth cost | ↓ 31% |
| Normal-user p99 | unchanged (< 200 ms maintained) |
Cost: Ollama local inference, zero API fees; extra memory ~1.8 GB (7B model, Q4_K_M quantization).
5. Practical Advice & Pitfalls
Advice
- Always dry-run for a week first: read the LLM’s judgment log, confirm misjudgment rate < 5% before going live
- Gate on
min_confidence0.8: low-confidence cases log-only, never auto-apply - Keep the global backstop always on: the
iptables500 Mbps backstop stays in place; the LLM only does fine-grained work inside that ceiling - Rebaseline weekly: the
burst_ratiodenominator (7-day same-time baseline) should be recalibrated every Monday 00:00; stale baselines go bad fast - Telegram only for “applied” + “rolled back”: silent on normal pass-through events to avoid alert fatigue
Common Pitfalls
| Pitfall | Fix |
|---|---|
| LLM emits cmds with shell injection | The engine never execs the LLM string directly — it parses structured parameters and builds commands itself; cmd is descriptive only |
| Stacked tc policies behave oddly | tc qdisc del dev eth0 root before every apply, keep it idempotent |
| Rate-limited normal sources suffer silently | Watchdog must track global p99, not just the targeted source; rollback conditions must cover global health |
| Model hallucinates IPs | The feature JSON is the single source of truth; any IP the LLM emits that isn’t in the JSON gets discarded |
| Quota changes monthly (elastic caps) | The baseline module should also read the provider’s quota API (where available) and adjust the global ceiling dynamically |
6. Extension Directions
- Deeper eBPF observability: upgrade the collector from
ip -s linkto eBPF (bcc/bpftrace) for per-packet protocol type, TLS SNI, and HTTP path prefixes — another step up in LLM judgment accuracy - Multi-VPS federation: multiple VPS in the same domain share one LLM baseline; if crawler hits VPS-A, VPS-B pre-configures its defenses
- WAF integration: LLM-identified “suspicious sources” feed straight into local ModSecurity/Coraza for application-layer bans
- Cost loop: wire bandwidth cost into the FinOps module; when the budget is exceeded, the LLM tightens the global backstop by another notch
Conclusion
Traffic shaping is, at its core, making resource-allocation decisions within a bandwidth budget. Fixed QoS hard-codes those decisions in a config file; AI-driven dynamic shaping hands the decision to a brain that can “read” live traffic, understand business SLOs, and comes with a built-in rollback safety net.
Three rules to remember:
- The LLM is an advisor, not an executor — every policy must pass structured validation in the policy engine;
- The watchdog is the safety net — non-optional — the LLM will be wrong; rollback must be a hard rule;
- Dry-run for a week first — no live mode until misjudgment rate is under 5%.
Your VPS bandwidth stops being a passive resource you only notice when it’s on fire. It becomes an actively guarded line — watched by AI, defended by policy, and backstopped by automatic rollback.
All script examples are available in full in the vps-traffic-shaper/ repository.
