Rockfish Suricata Transport Signals Plugin
Per-flow TCP and UDP signal metrics, emitted as tcp_signal and
udp_signal events through Suricata’s normal eve-log pipeline. Whatever
filetype: your eve-log uses (regular file, unix_dgram, unix_stream,
syslog, redis) — these events go there too, mixed in with your existing
flow / dns / tls events.
| TCP signals | UDP signals |
|---|---|
| Three-way handshake RTT | Request/response RTT (paired) |
| Retransmits per direction | Inter-arrival time (avg, stddev) |
| Out-of-order packets | Per-direction payload byte totals (data_toserver / data_toclient) |
| Zero-window events | |
| Window stats (min/avg/max) | |
Per-direction payload byte totals (data_toserver / data_toclient) |
|
| RST / FIN counts | |
Termination reason (fin / rst / timeout) |
data_toserver and data_toclient are full-flow payload byte totals
(post-TCP/UDP/IP headers). The downstream pipeline derives PCR
(producer/consumer ratio) from them — pcr = data_toserver /
(data_toserver + data_toclient) — without the sample-window cap that
the previous in-Suricata PCR computation had.
Downstream consumers (e.g. rockfish-perf) join these by flow_id with
the standard flow event to derive per-flow odometry — relative,
incremental measurements anchored to the flow’s first observation.
Build
SURICATA_SRC=/path/to/configured/suricata-src make
# or, if libsuricata-config is on PATH:
make
The result is rockfish-transport-signals.so.
Install
Copy the .so into Suricata’s plugin directory and reference it from
suricata.yaml. Then enable tcp_signal and udp_signal under your
existing eve-log.types: list — that’s it.
plugins:
- /usr/lib/suricata/plugins/rockfish-transport-signals.so
outputs:
- eve-log:
enabled: yes
filetype: unix_stream # or whatever you already use
filename: /var/run/rockfish/rockfish.sock
types:
- alert
- flow
- dns
- tls:
extended: yes
# ── Transport signals events ─────────────────────────────
- tcp_signal
- udp_signal
# Optional plugin tuning (all keys optional, defaults shown).
rockfish-transport-signals:
enabled: yes
tcp: yes
udp: yes
sample-rate: 1 # 1-in-N flow sampling
max-flows: 100000
flow-idle-timeout: 60
udp-rtt-pairing-window-ms: 2000
emit:
handshake-rtt: yes
retransmits: yes
zero-window: yes
window-stats: yes
udp-rtt: yes
udp-jitter: yes
No second socket. No second file. No output-file knob. The plugin
hands its events to Suricata’s eve writer and Suricata routes them.
Output format
Sample TCP record (lives in the same eve stream as flow/dns/tls):
{
"timestamp": "2026-04-28T17:32:11.018452Z",
"flow_id": 17628341205823,
"event_type": "tcp_signal",
"src_ip": "10.1.2.45", "src_port": 49215,
"dest_ip": "10.1.2.10", "dest_port": 443,
"proto": "TCP",
"tcp_signal": {
"start_us": 1748365931018452,
"end_us": 1748365941312009,
"duration_us": 10293557,
"handshake_rtt_us": 1842,
"data_toserver": 14820,
"data_toclient": 1284736,
"retransmits_toserver": 1,
"avg_window_toclient": 64240,
"min_window_toclient": 60128,
"max_window_toclient": 65535,
"fin_count": 2,
"close_reason": "fin"
}
}
Sample UDP record:
{
"timestamp": "2026-04-28T17:32:11.118452Z",
"flow_id": 17628341219991,
"event_type": "udp_signal",
"src_ip": "10.1.2.45", "src_port": 53412,
"dest_ip": "10.1.2.1", "dest_port": 53,
"proto": "UDP",
"udp_signal": {
"start_us": 1748365931118452,
"end_us": 1748365931145812,
"duration_us": 27360,
"data_toserver": 64,
"data_toclient": 256,
"rtt_count": 1,
"rtt_min_us": 27188, "rtt_max_us": 27188,
"rtt_avg_us": 27188.0
}
}
Per-direction packet counts are not duplicated here — they live on the
standard flow event for the same flow_id. data_toserver /
data_toclient are emitted directly so the downstream pipeline can
compute PCR without joining against the flow event.
Why TCP doesn’t have iat_* fields
TCP and UDP get different signal sets on purpose. TCP already surfaces
its transport-quality story through handshake_rtt_us, retransmits_*,
out_of_order_*, zero_window_*, and window stats — all direct,
semantically meaningful indicators. Adding packet inter-arrival
statistics on top would mostly measure Nagle, delayed-ACK, congestion-
window pacing, and Suricata’s own reassembly path — not application
behavior. UDP has none of those signals (no handshake, no ACKs, no
retransmit detection), so IAT mean/stddev is what’s left to characterize
pacing, jitter, and beaconing on UDP flows.
How rockfish-perf consumes this
rockfish-perf reads the Suricata eve socket and now sees tcp_signal
/ udp_signal mixed in with flow / dns. The fields surface in the
per-asset feature vector as:
| Feature | Source |
|---|---|
tcp_handshake_rtt_ms_avg |
tcp_signal.handshake_rtt_us |
tcp_retransmit_ratio |
retransmits / packets |
tcp_zero_window_ratio |
zero_window / packets |
tcp_out_of_order_ratio |
out_of_order / packets |
udp_rtt_avg_ms |
udp_signal.rtt_avg_us |
udp_jitter_avg_ms |
mean of per-direction iat_stddev_us |
pcr |
data_toserver / (data_toserver + data_toclient) |
These dimensions are added to the HBOS drift baseline automatically.