Featured image of post AI-Driven VPS Traffic Shaping & Burst Protection: A Local LLM Dynamic QoS Policy Engine

AI-Driven VPS Traffic Shaping & Burst Protection: A Local LLM Dynamic QoS Policy Engine

Your VPS bandwidth is a scarce, paid resource — yet DDoS bursts, crawler storms, accidental backup pushes, and upstream retry storms can blow it to the limit in minutes and take down your real business. This article shows a dynamic traffic-shaping system powered by a local LLM (Ollama + Qwen2.5) that identifies anomalous traffic patterns in real time, auto-generates and applies tc/iptables shaping policies, circuit-breaks in seconds when needed, and keeps all data on your own VPS.

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 rsync hogs every last bit of bandwidth — even SSH feels like a slideshow;
  • A DDoS pulse that a fixed tc limit 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 spotWhat happensConsequence
Unknown patternsAnything not written into a rule is invisibleBandwidth saturation, bill explosion
Normal peaks get collateral damageFixed limits apply during business peaks tooReal users get degraded
Policy conflictsStacked iptables/nftables rules behave unpredictablyHard to debug
No memory on recoveryAfter an incident the policy stays in place until a human cleans it upLong-term false positives

1.2 Three Capabilities AI Adds

  1. 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”
  2. Policy generation: from the label + business SLOs (e.g. “normal users p99 < 200 ms”), the LLM auto-generates tc/iptables/nftables policies with an estimated blast radius
  3. 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:

  1. Collector: eBPF (bpftool / off-the-shelf probes) or ip -s link + conntrack periodic snapshots; outputs per-source-IP rate, connection count, packet-size distribution
  2. Feature extractor: Python script compresses raw data into LLM-friendly JSON (Top 50 sources + global stats), kept under 2 KB
  3. LLM inference: Qwen2.5 analyzes the feature JSON and outputs “anomaly label + confidence + proposed policy + expected impact”
  4. Policy engine: translates the LLM’s proposal into tc qdisc / iptables -m hashlimit / nftables commands, validates syntax in dry-run, then applies atomically
  5. 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 signal
  • seen_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_check field, 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
  • scope distinction: specific (only the bad source) vs global (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):

MetricValue
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 intercepted14 in 2 weeks (7 crawlers, 4 retry storms, 3 DDoS pulses)
False-positive rollbacks2 (a flash-sale peak mislabeled as crawler)
Egress bandwidth P95620 Mbps → 410 Mbps
Monthly bandwidth cost↓ 31%
Normal-user p99unchanged (< 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_confidence 0.8: low-confidence cases log-only, never auto-apply
  • Keep the global backstop always on: the iptables 500 Mbps backstop stays in place; the LLM only does fine-grained work inside that ceiling
  • Rebaseline weekly: the burst_ratio denominator (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

PitfallFix
LLM emits cmds with shell injectionThe engine never execs the LLM string directly — it parses structured parameters and builds commands itself; cmd is descriptive only
Stacked tc policies behave oddlytc qdisc del dev eth0 root before every apply, keep it idempotent
Rate-limited normal sources suffer silentlyWatchdog must track global p99, not just the targeted source; rollback conditions must cover global health
Model hallucinates IPsThe 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

  1. Deeper eBPF observability: upgrade the collector from ip -s link to eBPF (bcc/bpftrace) for per-packet protocol type, TLS SNI, and HTTP path prefixes — another step up in LLM judgment accuracy
  2. Multi-VPS federation: multiple VPS in the same domain share one LLM baseline; if crawler hits VPS-A, VPS-B pre-configures its defenses
  3. WAF integration: LLM-identified “suspicious sources” feed straight into local ModSecurity/Coraza for application-layer bans
  4. 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.

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials