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
JT Updated by JIAXIN TAN 1 day ago
Jason Ish wrote in #note-1:
Question for anyone.. Does cluster_cpu require any special configuration outside of Suricata? On the network card?
Yes, we understand that cluster_cpu requires the NIC to deliver all packets of a flow to the same CPU. We've verified our NIC configuration:
NIC: Intel X710 (i40e driver), 40 RX queues
RSS is active — ethtool -x eno1np0 shows a populated indirection table mapping hash buckets to all 40 queues. Per-queue packet counts confirm even distribution (6-9B packets per queue).
IRQ affinity is pinned — All 40 IRQs (43-82) have smp_affinity = 00,3ff003ff (CPUs 0-9, 20-29 on NUMA node0). effective_affinity_list confirms each IRQ is actually landing on a specific CPU:
code
IRQ 43 (queue 0) → CPU 6
IRQ 44 (queue 1) → CPU 7
...
IRQ 56 (queue 13) → CPU 29
IRQ 57 (queue 14) → CPU 0
...
IRQ 82 (queue 39) → CPU 5
So the chain is: flow → RSS hash → queue → IRQ → fixed CPU.
irqbalance is disabled (systemctl is-active irqbalance → inactive).
RPS is disabled (rps_cpus = 00,00000000).
Despite all of the above, tcp.pkt_on_wrong_thread is still incrementing steadily:
code
tcp.pkt_on_wrong_thread = 157,646,795 (~3.4% of 4.6B total packets)
tcp.reassembly_gap = 53,841,984
tcp.insert_data_normal_fail= 54,271,386
capture.kernel_drops = 551,339 (0.012%, negligible)
One thing we have NOT done: pin Suricata's 16 worker threads to specific CPUs. The container runs with network_mode: host and no cpuset constraint, so the 16 threads are free to migrate across all 40 host CPUs.
Questions:
Is thread-to-CPU pinning also required for cluster_cpu to work correctly? If so, what's the recommended mapping (16 threads → which 16 of the 40 CPUs)?
Or is the 3.4% pkt_on_wrong_thread rate expected even with perfect NIC configuration, and the real issue is something else in the reassembly path?
Would cluster_qm (RSS-queue-based) be a better fit for this NIC since it has 40 hardware queues? That would bypass the CPU indirection entirely.
We're happy to test any of these configurations and report back.