Project

General

Profile

Actions

Bug #8940

open
JT OD

Suricata 8.0.6: af-packet cluster_cpu breaks TCP reassembly — HTTP detected by Zeek but missed by Suricata

Bug #8940: Suricata 8.0.6: af-packet cluster_cpu breaks TCP reassembly — HTTP detected by Zeek but missed by Suricata

Added by JIAXIN TAN 3 days ago. Updated 1 day ago.

Status:
Feedback
Priority:
Normal
Assignee:
Target version:
Affected Versions:
Effort:
Difficulty:
Label:
Protocol

Description

  1. 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


  1. 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.


  1. Environment
  1. 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
  1. 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
```

  1. Host NIC

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


  1. Evidence
  1. 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)
```

  1. 2. Live Suricata produces zero events for these flows
```bash
  1. 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
  2. → only unrelated DNS (UDP 53) records from OTHER hosts; no http/flow for this TCP stream
    ```
  1. 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
```


  1. 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;)
```


  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.


  1. 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).


  1. 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

suri_capture.pcap (10.4 KB) suri_capture.pcap JIAXIN TAN, 08/24/2026 07:29 AM
repro_http_stream.py (4.5 KB) repro_http_stream.py JIAXIN TAN, 08/24/2026 07:30 AM
Suricata漏检复现说明.md (6.94 KB) Suricata漏检复现说明.md JIAXIN TAN, 08/24/2026 07:30 AM

JI Updated by Jason Ish 1 day ago Actions #1

  • Status changed from New to Feedback
  • Assignee set to OISF Dev

Question for anyone.. Does cluster_cpu require any special configuration outside of Suricata? On the network card?

JT Updated by JIAXIN TAN 1 day ago Actions #2

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.

Actions

Also available in: PDF Atom