# Bug Report: HTTP traffic detected by Zeek but missed by Suricata (af-packet `cluster_cpu` breaks TCP stream reassembly)

- **Project**: https://github.com/monstertsl/SenseMind
- **Related forum thread**: https://forum.suricata.io/t/http-traffic-detected-by-zeek-but-missed-by-suricata-on-incomplete-tcp-streams/6441?u=monstertsl
- **Suricata version**: 8.0.6 RELEASE
- **Mode**: IDS (passive), af-packet capture
- **Attachment**: `poc/suri_capture.pcap` (captured inside the Suricata container, on the same interface Suricata monitors)
- **Reproduced**: 2026-08-24

---

## Summary

A scanner on host `10.10.168.224` sends normal, well-formed HTTP requests (complete 3-way handshake, valid sequence numbers, server returns `404`) to `10.10.168.197:80`.

- **Zeek** (libpcap, same interface `eno1np0`) logs all HTTP transactions correctly.
- **Suricata (live)** produces **zero events** for these flows — not even a `flow` event in `eve.json`.
- **Suricata (offline `-r` replay of the same pcap)** matches the rules **100%**.

The traffic *does* reach the monitored interface; the problem is in Suricata's live capture → stream pipeline.

---

## Environment

### Deployment (Docker Compose, host network)

Both engines listen on the **same** physical interface `eno1np0` (SPAN mirror port):

| | Suricata | Zeek |
|---|---|---|
| Image | `jasonish/suricata:latest` (8.0.6) | `zeek/zeek:latest` (8.2.0) |
| Network | `host` | `host` |
| CapAdd | `CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_NICE` | `CAP_NET_ADMIN CAP_NET_RAW` |
| Interface | `eno1np0` | `eno1np0` |
| Cmd | `-i eno1np0` | `zeek -i eno1np0 ...` |
| Capture | **af-packet** | **libpcap** |

### af-packet config (`suricata.yaml`)

```yaml
af-packet:
  - interface: default        # wildcard; real iface passed via -i eno1np0
    threads: 16
    cluster-id: 99
    cluster-type: cluster_cpu # <-- load-balances by CPU, NOT by flow
    ring-size: 65536
```

### Host NIC

```
eno1np0  : SPAN mirror port (PROMISC, no IP), monitored by both engines
eno3np2  : management NIC, host IP 10.10.64.113/24
```

---

## Evidence

### 1. Two capture points see identical packets (traffic DOES reach the mirror port)

While the attack was running, we captured `eno1np0` at two points simultaneously (`host 10.10.168.197 and tcp port 80`):

| Capture point | File | MD5 |
|---|---|---|
| Inside Suricata container (`tcpdump -i eno1np0`) | `suri_capture.pcap` | `819fdd3f37445b93bd0e054593878078` |
| Host (`tcpdump -i eno1np0`) | `host_capture.pcap` | `819fdd3f37445b93bd0e054593878078` |

**Both pcaps are byte-identical.** The captured flows are complete and well-formed:

```
10.10.168.224.57707 > 10.10.168.197.80  GET /.git/index HTTP/1.1
10.10.168.197.80  > 10.10.168.224.57707 HTTP/1.1 404 Not Found (720 bytes)
10.10.168.224.57709 > 10.10.168.197.80  GET /WEB-INF/web.xml HTTP/1.1
10.10.168.197.80  > 10.10.168.224.57709 HTTP/1.1 404 Not Found (720 bytes)
10.10.168.224.57710 > 10.10.168.197.80  GET /druid/index.html HTTP/1.1
10.10.168.197.80  > 10.10.168.224.57710 HTTP/1.1 404 Not Found (720 bytes)
... (6 attack URIs total, all tagged vlan 168)
```

### 2. Live Suricata produces zero events for these flows

```bash
# inside the suricata container, search eve.json for the attack source ports
grep -E "57707|57708|57709|57710|57711|57712" /var/log/suricata/eve.json
# → only unrelated DNS (UDP 53) records from OTHER hosts; no http/flow for this TCP stream
```

### 3. Offline replay of the same pcap matches rules 100%

```bash
suricata -r suri_capture.pcap -c test.yaml -k none -l out
```

```
08/24/2026-07:00:15.596530  [1:90015:1]  file read：/.git/ Git 仓库源码泄露    10.10.168.224:57707 -> 10.10.168.197:80
08/24/2026-07:00:15.596530  [1:91025:1]  source leak：GIT 源码目录泄露探测     10.10.168.224:57707 -> 10.10.168.197:80
08/24/2026-07:00:16.176527  [1:91272:1]  auth bypass：Druid 未授权访问漏洞     10.10.168.224:57712 -> 10.10.168.197:80
08/24/2026-07:00:16.176527  [1:91273:1]  auth bypass：若依 未授权访问漏洞      10.10.168.224:57712 -> 10.10.168.197:80
```

---

## Rules that matched offline (for reference)

```suricata
alert http any any -> any any (msg:"file read：/.git/ Git 仓库源码泄露"; flow:established,to_server; http.uri; content:"/.git/"; fast_pattern; nocase; classtype:web-application-attack; sid:90015; rev:1;)

alert http any any -> any any (msg:"source leak：GIT 源码目录泄露探测"; flow:established,to_server; http.uri; content:"/.git/"; fast_pattern; classtype:attempted-recon; sid:91025; rev:1;)

alert http any any -> any any (msg:"auth bypass：Druid 未授权访问漏洞"; flow:established,to_server; http.uri; content:"/druid/index.html"; nocase; fast_pattern; classtype:web-application-attack; sid:91272; rev:1;)

alert http any any -> any any (msg:"auth bypass：若依管理系统 未授权访问漏洞"; flow:established,to_server; http.uri; content:"/prod-api/druid/index.html"; nocase; fast_pattern; classtype:web-application-attack; sid:91273; rev:1;)
```

---

## Root cause hypothesis

`stats.log` shows massive stream-reassembly failures that correlate with the `cluster_cpu` load-balancing:

```
tcp.pkt_on_wrong_thread    = 1,478,842,804   (packets delivered to the wrong thread)
tcp.reassembly_gap         =   369,926,805   (reassembly gaps)
tcp.insert_data_normal_fail =   369,702,088  (insert failures)
```

With `cluster-type: cluster_cpu` (16 threads), packets of the **same TCP flow** get hashed to different CPU threads, producing reassembly gaps and incomplete HTTP streams. libhtp then cannot parse the request URI, so `http.uri` rules never match. Offline `-r` replay runs single-threaded and is unaffected — hence the 100% match.

This is the accurate meaning of the forum thread title "HTTP traffic detected by Zeek but missed by Suricata on **incomplete TCP streams**": it is not that the scanner sent incomplete streams, but that Suricata's thread fan-out broke complete streams apart.

---

## Prior incorrect diagnoses (corrected)

Two earlier hypotheses were ruled out during investigation and are documented here for completeness:

1. **"Scanners send half-open / seq-broken HTTP data"** — Incorrect. ES logs (source `1.13.9.36`) + Zeek offline analysis confirmed real scanner attack requests are complete, well-formed HTTP (GET + 404). Half-open probes (`history:ShAR`) are data-less port-liveness checks (nmap `-sS`), not attack requests.

2. **"Same-VLAN L2 traffic bypasses the SPAN mirror port"** — Incorrect. The dual-capture experiment above proves the traffic *does* reach `eno1np0` (byte-identical pcaps at two capture points).

---

## Next steps

1. Test `cluster-type: cluster_flow` (flow-hash to keep a flow on one thread) and observe whether `tcp.reassembly_gap` drops and detection recovers — while monitoring `capture.kernel_drops` (a prior 12.5% drop issue on a single hot flow was the reason we switched to `cluster_cpu`, so this is a trade-off to validate).

2. The attached `suri_capture.pcap` reproduces "Zeek sees it, Suricata misses it" deterministically and is available for your analysis.
