🇬🇧 English · 🇮🇹 Italiano
An isolated hardware lab for researching real-time detection and inline mitigation of DoS/DDoS attacks. A FortiGate NGFW segments the network and enforces blocks, a Suricata sensor turns traffic into per-flow features, an edge-AI cluster (NVIDIA Jetson + Turing Pi running k3s) classifies those flows, and a closed-loop orchestrator pushes block decisions back to the firewall over its REST API — a full sensor → AI → enforcement loop on real hardware.
Status: decommissioned. The physical lab no longer exists, so the tests here can't be re-run. This repository is the as-built documentation, the exact configuration commands, and the pipeline/ML code, kept as a reference and portfolio piece. All real credentials have been replaced with
<PLACEHOLDER>tokens (seesecrets.example.md), and the private IPs/MACs below are historical.
WiFi (uplink for the control laptop only)
│
[LAPTOP "parrot"] ── eno1 ──┐ control host + traffic generator
10.10.40.104 (VLAN40) │
▼
[MikroTik RB5009] ── ether1 (802.1q trunk) ──> [FortiGate 50G] lan3
L2 switch core / VLANs router + DHCP + inspection
│ │ │ │ │ lan1 = mgmt 192.168.1.99
ether2..ether8 (access ports) wan = 10.99.0.0/24 (lab-only)
│ │ │ │ │
┌──────────────┘ │ │ │ └───────────────┐
ether4 = Futro ether3 = Turing Pi ether5-8 = Jetson ×3 / firefly
(VLAN10, target) (VLAN40, k3s) (VLAN40, analysis)
The FortiGate sits inline between segments: attacker, benign, target and analysis traffic live in separate VLANs so the firewall can route and inspect between them. The MikroTik (formerly the site's internet router, repurposed) is a pure L2 VLAN switch and also SPAN-mirrors the target port to the Suricata sensor. The laptop reaches every device through the FortiGate's routing while keeping its own internet on WiFi.
| VLAN | Name (FortiGate) | Subnet / gateway | Role |
|---|---|---|---|
| 10 | lab-target |
10.10.10.0/24 — gw .1 |
Victim server (Futro, nginx) |
| 20 | lab-attacker |
10.10.20.0/24 — gw .1 |
Malicious traffic (from laptop) |
| 30 | lab-benign |
10.10.30.0/24 — gw .1 |
Benign traffic (from laptop) |
| 40 | lab-analyzer |
10.10.40.0/24 — gw .1 |
Analysis / AI stack + management |
Inter-VLAN traffic in the lab is routed only, never NATed — so the firewall (and the AI pipeline) sees real source IPs and can block them.
| Device | Board / OS | Role |
|---|---|---|
| FortiGate 50G | FortiOS 7.0.15 | Router, DHCP, inter-VLAN inspection, enforcement |
| MikroTik RB5009 | RouterOS 7.18.2 | L2 VLAN switch core + hardware port mirror |
| firefly | Firefly ROC-RK3588S, Ubuntu 20.04 | DPI sensor (Suricata) + telemetry shipper |
| jetson-1 | Jetson Orin Nano #1, JetPack 6.x | AI: RandomForest flow classifier |
| jetson-2 | Jetson Orin Nano #2, JetPack 6.x | AI: autoencoder anomaly detector |
| nano-nanissimo | Jetson Orin NX, JetPack R36.3 | Data spine: VictoriaMetrics + Grafana + MinIO |
| Turing Pi 2 | 4× RK1 modules, Ubuntu 24.04 | k3s cluster: NATS bus + orchestrator |
| Futro | x86 thin client | Target/victim host (nginx) |
| laptop | "parrot" | Control host + traffic generation |
⚠️ Naming gotcha: there are two Jetson Orin Nano boards (jetson-1,jetson-2). The node nicknamed "nano" / "nano-nanissimo" is a different board — a Jetson Orin NX — used as the data-spine node, not a third Orin Nano. Three Jetson boards total: 2× Orin Nano + 1× Orin NX.
The core of the lab is a closed loop from packet capture to firewall enforcement:
flowchart LR
A[Target traffic] -->|SPAN mirror| B[Suricata<br/>firefly]
B -->|eve.json flows| C[shipper.py<br/>async tail]
C -->|lab.features| D[(NATS bus<br/>k3s)]
D --> E[classifier.py<br/>RandomForest · jetson-1]
D --> F[autoencoder.py<br/>MLP · jetson-2]
E -->|lab.verdicts| G[orchestrator.py<br/>k3s]
F -->|lab.anomalies| G
G -->|sustained evidence| H[FortiGate REST API<br/>add IP to ddos_blocklist]
H --> I[Top deny-policy<br/>inline block]
C -.metrics.-> J[(VictoriaMetrics)]
J --> K[Grafana dashboard]
- Sense — Suricata on
fireflyreads a hardware port mirror and emits per-flow records toeve.json. Flow timeouts are tuned low (new=5s / established=30s / closed=5s) so flows surface in near real time instead of minutes later. - Ship —
shipper.pytailseve.jsonasynchronously (asyncio.create_subprocess_exec+await readline) and publishes flow features to thelab.featuresNATS subject, plus Suricata stats to VictoriaMetrics. - Classify —
classifier.py(RandomForest) scores each flow and emitsmaliciousverdicts tolab.verdicts;autoencoder.pyflags reconstruction-error outliers tolab.anomalies. - Decide & enforce —
orchestrator.pyblocks a source IP only after sustained evidence (≥ N malicious verdicts within a time window), then calls the FortiGate REST API to add it to theddos_blocklistaddress group, which a top-priority deny policy references — an inline block. ANEVER_BLOCKwhitelist protects the target and all gateways. - Observe — packets/s, bytes/s, alerts and total blocks stream to Grafana
(
grafana-dashboard.json).
Both models use 8 sensor-agnostic flow features — duration, tot_pkts, tot_bytes, pkts_toserver, pkts_toclient, bytes_toserver, bytes_toclient, bytes_per_pkt — chosen so the
same model works on both the training data and live Suricata flows (rate/is_tcp features were
dropped because their units/precision differ between sources).
- Training data: CIC-IDS2017 (benign + DoS/DDoS) combined with flows captured from this lab's own floods, giving realistic benign traffic instead of "small flow = attack".
- Models: RandomForest classifier + MLPRegressor autoencoder + a
StandardScaler, trained bygen/train2.py(→~99% precision/recallon a realistic test split). Artifacts are read frommeta.jsonat runtime, so re-training and swapping the model needs no code change on the Jetsons — just replace the files anddocker restart.
- Per-flow AI vs. volumetric floods. The per-flow path (Suricata → shipper → NATS →
classifier → orchestrator) has a throughput ceiling: high-rate floods (thousands of pps)
congest the downstream consumers, so blocks arrive late or not at all. Moderate floods
(≲ a few hundred pps, e.g.
hping3 -i u6000) are detected and blocked in real time (verified: ~80 verdicts → block). For true volumetric DDoS the right tool is the FortiGate DoS-policy (hardware fast-path); the AI path covers moderate-rate and L7 attacks. - NAT hides the attacker. With
wan→labNAT enabled, the source collapses to the gateway IP and can't be blocked individually — real tests require NAT off so the source stays visible. - Subnet collisions bite. The lab WAN must avoid
10.42.0.0/16(the k3s/flannel pod CIDR);10.99.0.0/24was used instead. - Safety first. Block only on sustained evidence (never a single verdict), and keep the target + gateways permanently whitelisted.
| Path | What it is |
|---|---|
docs/en/as-built.md |
Detailed as-built configuration |
docs/en/commands.md |
Every command used to build the lab, in order |
docs/en/runbook.md |
Original bring-up runbook / plan |
docs/en/guide.html |
Standalone illustrated guide (design decisions & why) |
docs/it/ |
Same four documents in Italian 🇮🇹 |
services/ |
On-node agents: shipper, classifier, autoencoder, orchestrator |
gen/ |
Traffic generators (locust/hping3) + model training + flow capture |
fgt.sh mt.sh nssh.sh zy_cli.py |
Non-interactive SSH/CLI wrappers (FortiGate / MikroTik / nodes / Zyxel) |
reset-block.sh |
Clears the FortiGate blocklist (unblock all) |
k8s-nats.yaml k8s-orchestrator.yaml |
k3s manifests for the NATS bus and the orchestrator |
nano-compose.yml |
Data-spine stack (VictoriaMetrics + Grafana + MinIO) |
grafana-dashboard.json |
Grafana dashboard definition |
secrets.example.md · .env.example |
Credential/config templates (fill in locally) |
The lab is hardware-specific, but the code is reusable. Roughly:
# 1) Control-host tooling (traffic gen + model training)
python3 -m venv genv && genv/bin/pip install -r requirements.txt
# 2) Secrets: export as env vars (see secrets.example.md) — nothing is hardcoded
export FGTPW='...' MTPW='...' FGT_KEY='...'
# 3) Dataset for training: download CIC-IDS2017 "MachineLearningCVE" CSVs into cic/
# https://www.unb.ca/cic/datasets/ids-2017.html (gitignored, not shipped here)
# 4) Data-spine stack
cp .env.example .env # then edit
docker compose -f nano-compose.yml up -dFull step-by-step (VLANs, DHCP reservations, DoS-policy, IPS, NetFlow, k3s, deployment, and the
end-to-end validation loop) is in docs/en/commands.md.
This is defensive security research run inside an isolated lab the author owns. The traffic
generators (hping3, locust, slowloris) are for that closed environment only — don't point them
at hosts you don't control. All credentials in this repo are placeholders; the real lab is
offline.
MIT © 2026 Edoardo Besana