Rockfish Suricata Payload Entropy Plugin

Three configurable signals per flow, all over the same sampled packet window. Each signal is independently enable/disable-able from suricata.yaml; by default all three are emitted when the plugin is on.

Signal Fields
Entropy entropy_toserver, entropy_toclient, bytes_sampled_to{server,client}
PCR (producer/consumer ratio) pcr
SPLT (Sequence of Packet Lengths and Times) splt, splt_lengths, splt_iats_us

All three operate on application payload bytes (post-TCP/UDP/IP headers). Emitted as payload_entropy events through Suricata’s normal eve-log pipeline.

Build

./scripts/build-entropy.sh                    # build only
./scripts/build-entropy.sh --install          # build + install
./scripts/build-entropy.sh --test             # cargo test

Configuration

plugins:
  - /usr/lib/suricata/plugins/rockfish-payload-entropy.so

outputs:
  - eve-log:
      filetype: unix_stream
      filename: /var/run/rockfish/rockfish.sock
      types:
        - alert
        - flow
        - tcp_perf
        - udp_perf
        - payload_entropy        # <-- enable this plugin's output

rockfish-payload-entropy:
  enabled: yes
  tcp: yes
  udp: yes
  sample-rate: 1                       # 1-in-N flow sampling
  max-flows: 100000
  max-packets-per-direction: 16        # caps SPLT length to ~32
  max-bytes-per-direction: 8192        # caps entropy histogram

  # Per-feature emit toggles. All default to yes when the plugin is enabled.
  emit:
    entropy: yes
    pcr: yes
    splt: yes

Any combination is valid (including splt: no, entropy: no, pcr: yes). If all three are off the plugin refuses to register.

Output format

{
  "timestamp": "2026-04-29T01:14:22.018452Z",
  "flow_id": 17628341205823,
  "event_type": "payload_entropy",
  "src_ip": "10.1.2.45", "src_port": 49215,
  "dest_ip": "10.1.2.10", "dest_port": 443,
  "proto": "TCP",
  "payload_entropy": {
    "entropy_toserver": 7.94,
    "entropy_toclient": 7.91,
    "bytes_sampled_toserver": 8192,
    "bytes_sampled_toclient": 6234,

    "pcr": 0.568,

    "splt": "HhHhKHkkkk",
    "splt_lengths": [224, 198, 230, 211, 1340, 187, 1460, 1460, 1460, 870],
    "splt_iats_us": [0, 1842, 90, 12000, 250, 4500, 80, 80, 80, 90]
  }
}

Disabled signals are omitted entirely from the record (not emitted as null). Enabled but no-data signals (e.g., entropy enabled but no packets seen in one direction) drop only the affected fields.

SPLT letter encoding

Each character in splt represents one observed packet, in arrival order across both directions:

Letter Size bucket
A / a ≤ 2
B / b 3–4
C / c 5–8
D / d 9–16
E / e 17–32
F / f 33–64
G / g 65–128
H / h 129–256
I / i 257–512
J / j 513–1024
K / k 1025–2048+

splt, splt_lengths, and splt_iats_us are index-aligned: position i in all three describes the same packet. splt_iats_us[0] is always 0 (no previous packet). Length is capped at 64 (default config: ≤32).

Querying

-- Likely encrypted exfil
SELECT src_ip, dest_ip, dest_port, entropy_toserver, pcr,
       bytes_sampled_toserver, splt
FROM read_parquet('.../payload_entropy/...', union_by_name=true)
WHERE entropy_toserver >= 7.8
  AND pcr   >= 0.85
  AND bytes_sampled_toserver >= 1024
ORDER BY entropy_toserver DESC, pcr DESC;

-- Cluster by SPLT shape
SELECT splt, COUNT(*) AS flows
FROM read_parquet('.../payload_entropy/...', union_by_name=true)
WHERE splt IS NOT NULL
GROUP BY splt
ORDER BY flows DESC
LIMIT 50;

-- TLS-handshake-shaped flows: small Hello exchange + larger key/cert
SELECT src_ip, dest_ip, dest_port, splt, splt_lengths, splt_iats_us
FROM read_parquet('.../payload_entropy/...', union_by_name=true)
WHERE splt LIKE 'HhH%K%'
LIMIT 20;

-- Long inter-arrival times (possible C2 beaconing)
SELECT src_ip, dest_ip, dest_port,
       list_max(splt_iats_us) AS max_iat_us,
       splt
FROM read_parquet('.../payload_entropy/...', union_by_name=true)
WHERE list_max(splt_iats_us) > 10000000  -- > 10 seconds
ORDER BY max_iat_us DESC;

Storage

Field EVE wire Parquet (compressed)
Header (timestamp, flow_id, 5-tuple, proto) ~200 B ~30–50 B
Entropy + bytes_sampled + pcr ~150 B ~30 B
SPLT (≤32 packets default): splt + splt_lengths + splt_iats_us ~250 B ~50–100 B
Total per record ~600 B ~120–180 B

At a million flows/day with all three signals on, that’s ~600 MB EVE wire, ~150 MB on disk after compression. Disable any signal to cut that proportionally.