* refactor: Rename project from NetGuardia to Mantis * fix: convert mantis-frontend from tracked files to submodule * wip * feat: Suricata integration stabilization and config unification Fix a series of bugs in the Suricata daemon integration and unify configuration so users interact only with config.toml. * docs: sur-001-c1 Suricata integration stabilization findings
5.8 KiB
sur-001: Suricata Integration Stabilization and Config Unification
Cycle: 1 | Theme: backend-detection | Kind: investigation + design + fix | Status: done Date: 2026-05-22
Summary
Diagnosed and resolved a series of bugs in the Suricata daemon integration, then refactored
the configuration system so users interact with a single config.toml instead of maintaining
a separate suricata.yaml. Config and suppress content are now generated in-memory and
delivered to Suricata via memfd_create, writing nothing to disk.
Findings
Q: Why did Suricata fail with MirrorSetupFailed "No such file or directory (os error 2)"?
A: Two independent causes, both present simultaneously.
-
iproute2was not installed in the container.Command::new("ip").output()returnsio::Error(ENOENT)when the binary is not found, which maps toMirrorSetupFailed. -
MIRROR_PEER = "mantis-mirror-peer"is 18 characters, exceeding the Linux kernel limit ofIFNAMSIZ - 1 = 15. The kernel rejects the interface name;ip link addfails, so/sys/class/net/mantis-mirror/ifindexnever appears, andget_ifindexreturns ENOENT.
Fix: Installed iproute2; renamed MIRROR_PEER to "mantis-peer" (11 chars).
Confidence: high — confirmed by testing ip link add with both names.
Q: Why did Suricata log output not appear when stderr was piped?
A: Suricata's console logger calls isatty(2) at startup. When stderr is not a TTY
(i.e., Stdio::piped() or Stdio::null()), Suricata automatically disables console output —
this is documented behavior matching daemon mode. stdbuf has no effect because Suricata's
log system does not use libc stdio buffering.
Fix: Configure Suricata to write operational logs to /tmp/suricata.log via the yaml
logging: file: section. A dedicated suricata-log thread tails the file and forwards lines
into Mantis's tracing system, routing by prefix (Error/Warn/Notice/Info).
Confidence: high — confirmed via web search (Suricata forum + OISF docs).
Q: Why did Suricata fail to start after switching to generated config via memfd?
A: The yaml template used Rust's "\n\ + source newline` continuation syntax, which is
designed to strip leading whitespace from the next source line. This silently removed all
YAML indentation, producing a structurally invalid document that Suricata rejected.
Fix: Switched template to r#"..."# raw string, which preserves whitespace exactly as
written in source. No external YAML library required.
Confidence: high — confirmed by inspecting /tmp/suricata-mantis.yaml before and after.
Q: Did Suricata support /proc/self/fd/N as a config path?
A: Yes. memfd_create without MFD_CLOEXEC produces a file descriptor that survives
fork+exec into the Suricata child process. Suricata can open /proc/self/fd/N to read
the in-memory config. The approach works for both the main config and the suppress/threshold
file. The earlier failure was entirely due to malformed YAML, not the memfd mechanism.
Confidence: high — confirmed working after YAML fix.
Q: What was wrong with the af-packet interface name in suricata.yaml?
A: The static suricata.yaml still referenced mantis-mirror-peer (the old peer name)
after the rename to mantis-peer. Suricata was listening on a non-existent interface and
capturing no traffic. This was silently ignored — Suricata started but processed zero packets.
Fix: Interface name is now derived from the MIRROR_PEER constant in engine.rs and
injected into the generated yaml, making divergence impossible.
Confidence: high.
Design Decisions
Single config entry point
suricata.yaml was promoted from a user-edited file to a generated internal artifact.
All user-facing Suricata settings live in config.toml under [Config.suricata]:
| Field | Type | Notes |
|---|---|---|
home_net |
String |
Required. Protected network CIDR. |
worker_cpu_set |
Option<[u32; 2]> |
Same semantics as xsk_cpu_set. |
management_cpu |
Option<u32> |
Management thread CPU pin. |
af_packet_threads |
String |
Default "auto". |
af_packet_ring_size |
u32 |
Default 2048. |
af_packet_block_size |
u32 |
Default 131072. |
suppress |
Vec<String> |
Raw Suricata suppress/threshold lines. |
Removing [Config.suricata] entirely disables the rule engine.
suppress as raw strings
Rather than defining a structured SuppressEntry with parsed fields, suppress entries are
stored as raw Suricata syntax strings. This is more flexible (supports threshold,
rate_filter, etc.) and lets users copy directly from Suricata documentation.
memfd_create for config delivery
Both the generated yaml and suppress content are written to anonymous in-memory files via
memfd_create(0) (no MFD_CLOEXEC), inherited by the Suricata child process, and passed
as /proc/self/fd/N paths. Parent closes its copies immediately after spawn(). Nothing
is written to the filesystem.
Unexpected Discoveries
suricata.yamlreferenced the old peer interface name (mantis-mirror-peer) even after the veth rename, causing Suricata to silently capture zero traffic. The bug was masked because Suricata started without error.- Rust's
"\n\continuation eats leading whitespace — a non-obvious footgun when building indentation-sensitive file formats inline.
Open Questions
- Should
af_packet_ring_sizeandaf_packet_block_sizebe exposed to users, or are the defaults sufficient for the research prototype?
Impact on Downstream Tasks
- active-response: XDP blocking now has a working rule engine to corroborate with. Fusion alerts from Suricata + ML are available as the trigger signal.
- tls-analysis: The
app-layer: tls: enabled: yesand EVE tls event output can be enabled in the generated yaml without user-visible config changes.