Bug #8940
openSuricata 8.0.6: af-packet cluster_cpu breaks TCP reassembly — HTTP detected by Zeek but missed by Suricata
Description
- 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 load-balances by CPU, NOT by flow
threads: 16
cluster-id: 99
cluster-type: cluster_cpu # <-
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
- 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.
Files