Mantis/.research/findings/tasks/sur-001-c1.md
ParrotXray 46a69dd49c
refactor/rename-mantis (#12)
* 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
2026-05-22 18:42:51 +08:00

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.

  1. iproute2 was not installed in the container. Command::new("ip").output() returns io::Error(ENOENT) when the binary is not found, which maps to MirrorSetupFailed.

  2. MIRROR_PEER = "mantis-mirror-peer" is 18 characters, exceeding the Linux kernel limit of IFNAMSIZ - 1 = 15. The kernel rejects the interface name; ip link add fails, so /sys/class/net/mantis-mirror/ifindex never appears, and get_ifindex returns 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.yaml referenced 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_size and af_packet_block_size be 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: yes and EVE tls event output can be enabled in the generated yaml without user-visible config changes.