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
This commit is contained in:
ParrotXray 2026-05-22 18:42:51 +08:00 committed by GitHub
parent 40d420a0d7
commit 46a69dd49c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 1255 additions and 485 deletions

View File

@ -1,11 +1,11 @@
---
name: research
description: Interactive research assistant for NetGuardia. You ask questions; I research, reason, and discuss with you.
description: Interactive research assistant for Mantis. You ask questions; I research, reason, and discuss with you.
---
# Interactive Research — Ask / Research / Discuss
You are a research discussion partner for the NetGuardia project.
You are a research discussion partner for the Mantis project.
## Hierarchy
@ -38,7 +38,7 @@ these priorities and surface that context in your response.
## How This Works
1. **User asks a question** — about ML inference, eBPF capture, rule engine, API/frontend, or anything NetGuardia-related.
1. **User asks a question** — about ML inference, eBPF capture, rule engine, API/frontend, or anything Mantis-related.
2. **Research** — search docs, source code, papers, or reason from existing findings. Check `.research/findings/` for prior context before searching externally.
3. **Discuss** — present findings clearly, state confidence level, surface open questions, and invite follow-up.

2
.gitignore vendored
View File

@ -12,7 +12,7 @@ target/
logs
.log
.txt
net-guardia/static/web
mantis/static/web
*.mmdb
node_modules/
.next/

4
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "net-guardia-frontend"]
path = net-guardia-frontend
[submodule "mantis-frontend"]
path = mantis-frontend
url = https://github.com/ParrotXray/NetGuardia-frontend-academic-research.git

View File

@ -0,0 +1,138 @@
# 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.

View File

@ -1,9 +1,9 @@
# NetGuardia Research State
# Mantis Research State
# Updated: 2026-05-21
[[epics]]
id = "nids-v1"
title = "NetGuardia NIDS v1 — Research Prototype"
title = "Mantis NIDS v1 — Research Prototype"
status = "active"
description = """
End-to-end NIDS combining eBPF/AF_XDP packet capture, LSTM autoencoder ML inference,

View File

@ -1,4 +1,4 @@
# NetGuardia
# Mantis
Network intrusion detection system combining eBPF packet capture with ML-based anomaly detection and Suricata/Snort rule matching.
@ -19,12 +19,12 @@ Network intrusion detection system combining eBPF packet capture with ML-based a
## Workspace Structure
```
net-guardia/ - Main application (ML, eBPF userspace, HTTP API, WebSocket)
mantis/ - Main application (ML, eBPF userspace, HTTP API, WebSocket)
common/ - Shared types used by both userspace and eBPF programs
macros/ - Procedural macros: log!, traceable!, loggable!
ingress-ebpf/ - eBPF ingress packet capture program
egress-ebpf/ - eBPF egress packet capture program
net-guardia-frontend/ - Next.js web UI
mantis-frontend/ - Next.js web UI
```
## Build
@ -33,17 +33,17 @@ Full build requires eBPF toolchain and system libs (libelf, boost for vectorscan
To type-check without eBPF (for ML/API changes):
```bash
SKIP_EBPF_BUILD=1 cargo check --package net-guardia
SKIP_EBPF_BUILD=1 cargo check --package mantis
```
Ignore these expected errors when SKIP_EBPF_BUILD is set:
- `environment variable ARTIFACTCS_PATH not defined`
- `environment variable CSV_RECORD_PATH not defined`
- `environment variable RULES_DB_PATH not defined`
- `couldn't read .../net-guardia-ingress`
- `couldn't read .../net-guardia-egress`
- `couldn't read .../mantis-ingress`
- `couldn't read .../mantis-egress`
## net-guardia Source Layout
## mantis Source Layout
```
src/
@ -129,7 +129,7 @@ Session is wrapped in `Mutex<Session>` because `Session::run` requires `&mut sel
## Config
Runtime config: `config.toml`
ML artifacts: `net-guardia/static/artifacts/`
ML artifacts: `mantis/static/artifacts/`
- `deep_autoencoder.onnx` - LSTM autoencoder model
- `inference_config.json` - window size, feature names, scaler params, threshold

88
Cargo.lock generated
View File

@ -1662,6 +1662,50 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "mantis"
version = "0.1.0"
dependencies = [
"actix",
"actix-cors",
"actix-web",
"actix-ws",
"aya",
"aya-log",
"cargo_metadata",
"cc",
"chrono",
"common",
"crossbeam",
"dotenvy",
"futures",
"futures-util",
"libc",
"lru",
"macros",
"maxminddb",
"mime_guess",
"ndarray 0.17.2",
"network-types",
"ort",
"ort-tract",
"parking_lot",
"rust-embed",
"serde",
"serde_json",
"sysinfo",
"thiserror 2.0.16",
"tokio",
"tokio-tungstenite",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tract-onnx",
"url",
"xsk-rs",
]
[[package]]
name = "maplit"
version = "1.0.2"
@ -1797,50 +1841,6 @@ dependencies = [
"rawpointer",
]
[[package]]
name = "net-guardia"
version = "0.1.0"
dependencies = [
"actix",
"actix-cors",
"actix-web",
"actix-ws",
"aya",
"aya-log",
"cargo_metadata",
"cc",
"chrono",
"common",
"crossbeam",
"dotenvy",
"futures",
"futures-util",
"libc",
"lru",
"macros",
"maxminddb",
"mime_guess",
"ndarray 0.17.2",
"network-types",
"ort",
"ort-tract",
"parking_lot",
"rust-embed",
"serde",
"serde_json",
"sysinfo",
"thiserror 2.0.16",
"tokio",
"tokio-tungstenite",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tract-onnx",
"url",
"xsk-rs",
]
[[package]]
name = "network-types"
version = "0.1.0"

View File

@ -1,7 +1,7 @@
[workspace]
resolver = "2"
members = ["net-guardia", "common", "macros", "ingress-ebpf", "egress-ebpf"]
default-members = ["net-guardia", "common"]
members = ["mantis", "common", "macros", "ingress-ebpf", "egress-ebpf"]
default-members = ["mantis", "common"]
[workspace.dependencies]
aya = { version = "0.13.1", default-features = false }

View File

@ -1,8 +1,8 @@
# NetGuardia
# Mantis
## Project Overview
**NetGuardia** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware.
**Mantis** is a high-performance network security solution that combines eBPF XDP technology with deep learning models to provide advanced network protection. The system operates as a standalone network appliance that can run on any Ubuntu-based system with compatible network hardware.
## Core Technologies
@ -50,11 +50,10 @@
- Root/sudo access for eBPF program loading
## Hardware Compatibility
NetGuardia is designed to work on any Ubuntu-based system meeting the following requirements:
Mantis is designed to work on any Ubuntu-based system meeting the following requirements:
- Network Interface: Any dual-port NIC supporting XDP native or offload mode (Intel i350 T2 recommended)
- CPU: Multi-core processor recommended for optimal performance
- Memory: 8GB RAM minimum, 16GB or more for high-traffic environments
The system is not limited to embedded platforms and can be deployed on standard server hardware, virtual machines, or dedicated appliances running Ubuntu.

6
TODO
View File

@ -1,5 +1,5 @@
============================================================
NetGuardia TODO (updated 2026-05-21)
Mantis TODO (updated 2026-05-21)
============================================================
-- DONE (archived) ----------------------------------------
@ -17,7 +17,7 @@ NetGuardia TODO (updated 2026-05-21)
Removed: vectorscan, rusqlite, protolens, pcre2, byte_test/jump/extract,
app-layer-protocol, threshold, QUIC parser, suppress list (SQLite).
Replaced by: suricata daemon + veth mirror + EVE JSON unix socket.
Suricata handles all signature matching; NetGuardia reads alerts via output.rs.
Suricata handles all signature matching; Mantis reads alerts via output.rs.
[x] Suricata EVE socket race condition (bind before spawn)
[x] af-packet block-size too small (32768 -> 131072)
[x] Suricata noisy rule categories (exclude emerging-info/policy/user_agents)
@ -80,4 +80,4 @@ NetGuardia TODO (updated 2026-05-21)
[ ] Dashboard attack/protocol counters never reset
Accumulate for entire session lifetime; charts show historical totals.
Consider: sliding window reset every N minutes, or cap at last 500 alerts.
File: net-guardia-frontend/...dashboard.tsx
File: mantis-frontend/...dashboard.tsx

View File

@ -40,5 +40,17 @@ ml_cpu = 7
ae_threshold_method = "94"
# Suricata daemon mode. Remove or comment out to disable the rule engine.
suricata_name = "suricata.yaml"
# Suricata rule engine. Remove this entire section to disable.
[Config.suricata]
home_net = "140.130.34.0/24"
worker_cpu_set = [4, 6]
management_cpu = 0
af_packet_threads = "auto"
af_packet_ring_size = 2048
af_packet_block_size = 131072
# Suppress known false positives — paste Suricata suppress lines directly.
# suppress = [
# "suppress gen_id 1, sig_id 2001234",
# "suppress gen_id 1, sig_id 2001234, track by_src, ip 192.168.1.0/24",
# ]

View File

@ -13,7 +13,7 @@ aya-log-ebpf = { workspace = true }
which = "8.0.0"
[[bin]]
name = "net-guardia-egress"
name = "mantis-egress"
path = "src/main.rs"
test = false
doctest = false

View File

@ -20,7 +20,7 @@ static PARSED_PACKET: PerCpuArray<Event> = PerCpuArray::with_max_entries(1, 0);
static EGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
#[xdp]
pub fn net_guardia(ctx: XdpContext) -> u32 {
pub fn mantis(ctx: XdpContext) -> u32 {
unsafe {
let _ = packet_intake(ctx);
xdp_action::XDP_PASS

View File

@ -14,7 +14,7 @@ network-types = { workspace = true }
which = "8.0.0"
[[bin]]
name = "net-guardia-ingress"
name = "mantis-ingress"
path = "src/main.rs"
test = false
doctest = false

View File

@ -22,7 +22,7 @@ static PARSED_PACKET: PerCpuArray<Event> = PerCpuArray::with_max_entries(1, 0);
static INGRESS_XSKS_MAP: XskMap = XskMap::pinned(64, 0);
#[xdp]
pub fn net_guardia(ctx: XdpContext) -> u32 {
pub fn mantis(ctx: XdpContext) -> u32 {
unsafe {
let _ = packet_intake(&ctx);
let _ = PROGRAM_ARRAY.tail_call(&ctx, TRANSMISSION);

View File

@ -1,5 +1,5 @@
[package]
name = "net-guardia"
name = "mantis"
version = "0.1.0"
edition = "2024"
@ -53,5 +53,5 @@ cc = "1"
dotenvy = "0.15.7"
[[bin]]
name = "net-guardia"
name = "mantis"
path = "src/main.rs"

View File

@ -5,7 +5,7 @@ use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::SystemTime;
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind};
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target};
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
@ -22,8 +22,6 @@ fn main() {
.unwrap()
.join(format!("{}-frontend", project_name));
// ── All cargo: directives in one place ────────────────────────────────
println!("cargo:rustc-env=ARTIFACTCS_PATH={}", artifact_dir.display());
println!("cargo:rustc-env=CSV_RECORD_PATH={}", csv_dir.display());
println!("cargo:rustc-env=ONNXRUNTIME_PATH={}", onnxruntime_dir.display());
@ -43,11 +41,9 @@ fn main() {
println!("cargo:rerun-if-changed={}", frontend_dir.join(item).display());
}
// ─────────────────────────────────────────────────────────────────────
if env::var_os("SKIP_EBPF_BUILD").is_some() {
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
for name in &["net-guardia-ingress", "net-guardia-egress"] {
for name in &["mantis-ingress", "mantis-egress"] {
let path = out.join(name);
if !path.exists() {
fs::write(&path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
@ -79,104 +75,91 @@ fn build_ingress_ebpf() {
panic!("unsupported endian={:?}", endian)
};
let build_ebpf = true;
if build_ebpf {
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let target = format!("{target}-unknown-none");
let target = format!("{target}-unknown-none");
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
]);
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
]);
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}
cmd.current_dir(ebpf_dir);
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
let ebpf_target_dir = out_dir.join("../ingress-ebpf");
cmd.arg("--target-dir").arg(&ebpf_target_dir);
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}"));
let Child { stdout, stderr, .. } = &mut child;
let stderr = stderr.take().unwrap();
let stderr = BufReader::new(stderr);
let stderr = std::thread::spawn(move || {
for line in stderr.lines() {
let line = line.unwrap();
println!("{line}");
}
cmd.current_dir(ebpf_dir);
});
let ebpf_target_dir = out_dir.join("../ingress-ebpf");
cmd.arg("--target-dir").arg(&ebpf_target_dir);
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}"));
let Child { stdout, stderr, .. } = &mut child;
let stderr = stderr.take().unwrap();
let stderr = BufReader::new(stderr);
let stderr = std::thread::spawn(move || {
for line in stderr.lines() {
let line = line.unwrap();
println!("{line}");
let stdout = stdout.take().unwrap();
let stdout = BufReader::new(stdout);
let mut executables = Vec::new();
for message in Message::parse_stream(stdout) {
#[allow(clippy::collapsible_match)]
match message.expect("valid JSON") {
Message::CompilerArtifact(Artifact {
executable,
target: Target { name, .. },
..
}) => {
if let Some(executable) = executable {
executables.push((name, executable.into_std_path_buf()));
}
}
});
let stdout = stdout.take().unwrap();
let stdout = BufReader::new(stdout);
let mut executables = Vec::new();
for message in Message::parse_stream(stdout) {
#[allow(clippy::collapsible_match)]
match message.expect("valid JSON") {
Message::CompilerArtifact(Artifact {
executable,
target: Target { name, .. },
..
}) => {
if let Some(executable) = executable {
executables.push((name, executable.into_std_path_buf()));
}
}
Message::CompilerMessage(CompilerMessage { message, .. }) => {
for line in message.rendered.unwrap_or_default().split('\n') {
println!("{line}");
}
}
Message::TextLine(line) => {
Message::CompilerMessage(CompilerMessage { message, .. }) => {
for line in message.rendered.unwrap_or_default().split('\n') {
println!("{line}");
}
_ => {}
}
}
let status = child
.wait()
.unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}"));
assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}");
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
} else {
let Package { targets, .. } = ebpf_package;
for Target { name, kind, .. } in targets {
if *kind != [TargetKind::Bin] {
continue;
Message::TextLine(line) => {
println!("{line}");
}
let dst = out_dir.join(name);
fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}"));
_ => {}
}
}
let status = child
.wait()
.unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}"));
assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}");
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
}
fn build_egress_ebpf() {
@ -198,105 +181,92 @@ fn build_egress_ebpf() {
panic!("unsupported endian={:?}", endian)
};
let build_ebpf = true;
if build_ebpf {
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let target = format!("{target}-unknown-none");
let target = format!("{target}-unknown-none");
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
println!("cargo:rerun-if-changed={}", ebpf_dir.as_str());
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
]);
let mut cmd = Command::new("cargo");
cmd.args([
"build",
"-Z",
"build-std=core",
"--bins",
"--message-format=json",
"--release",
"--target",
&target,
]);
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
cmd.env("CARGO_TERM_COLOR", "always");
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
cmd.env("CARGO_TERM_COLOR", "always");
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}
cmd.current_dir(ebpf_dir);
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
let ebpf_target_dir = out_dir.join("../egress-ebpf");
cmd.arg("--target-dir").arg(&ebpf_target_dir);
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}"));
let Child { stdout, stderr, .. } = &mut child;
let stderr = stderr.take().unwrap();
let stderr = BufReader::new(stderr);
let stderr = std::thread::spawn(move || {
for line in stderr.lines() {
let line = line.unwrap();
println!("{line}");
}
cmd.current_dir(ebpf_dir);
});
let ebpf_target_dir = out_dir.join("../egress-ebpf");
cmd.arg("--target-dir").arg(&ebpf_target_dir);
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}"));
let Child { stdout, stderr, .. } = &mut child;
let stderr = stderr.take().unwrap();
let stderr = BufReader::new(stderr);
let stderr = std::thread::spawn(move || {
for line in stderr.lines() {
let line = line.unwrap();
println!("{line}");
let stdout = stdout.take().unwrap();
let stdout = BufReader::new(stdout);
let mut executables = Vec::new();
for message in Message::parse_stream(stdout) {
#[allow(clippy::collapsible_match)]
match message.expect("valid JSON") {
Message::CompilerArtifact(Artifact {
executable,
target: Target { name, .. },
..
}) => {
if let Some(executable) = executable {
executables.push((name, executable.into_std_path_buf()));
}
}
});
let stdout = stdout.take().unwrap();
let stdout = BufReader::new(stdout);
let mut executables = Vec::new();
for message in Message::parse_stream(stdout) {
#[allow(clippy::collapsible_match)]
match message.expect("valid JSON") {
Message::CompilerArtifact(Artifact {
executable,
target: Target { name, .. },
..
}) => {
if let Some(executable) = executable {
executables.push((name, executable.into_std_path_buf()));
}
}
Message::CompilerMessage(CompilerMessage { message, .. }) => {
for line in message.rendered.unwrap_or_default().split('\n') {
println!("{line}");
}
}
Message::TextLine(line) => {
Message::CompilerMessage(CompilerMessage { message, .. }) => {
for line in message.rendered.unwrap_or_default().split('\n') {
println!("{line}");
}
_ => {}
}
}
let status = child
.wait()
.unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}"));
assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}");
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
} else {
let Package { targets, .. } = ebpf_package;
for Target { name, kind, .. } in targets {
if *kind != [TargetKind::Bin] {
continue;
Message::TextLine(line) => {
println!("{line}");
}
let dst = out_dir.join(name);
fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}"));
_ => {}
}
}
let status = child
.wait()
.unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}"));
assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}");
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
}
fn build_frontend(frontend_dir: &PathBuf, static_dir: &PathBuf) {
@ -433,4 +403,3 @@ fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
}
Ok(())
}

View File

@ -51,12 +51,12 @@ impl EbpfServices {
pub async fn run(
self: Arc<Self>,
ml_engine: Arc<Engine>,
suricata_engine: Arc<SuricataEngine>,
suricata_engine: Option<Arc<SuricataEngine>>,
) -> Result<(), Error> {
let xsk_manager = self.xsk_manager.clone();
let statistics = self.statistics.clone();
xsk_manager.run(Some(ml_engine), Some(suricata_engine), &self.shutdowns)?;
xsk_manager.run(Some(ml_engine), suricata_engine, &self.shutdowns)?;
let statistics_shutdown = statistics.run().await;
self.shutdowns.push(statistics_shutdown);

View File

@ -21,7 +21,7 @@ pub struct GeoIpService {
impl GeoIpService {
pub fn new(db_name: &str) -> Result<Self, MaxMindDbError> {
let db_path = PathBuf::from("net-guardia/static/geo").join(db_name);
let db_path = PathBuf::from("mantis/static/geo").join(db_name);
Self::with_cache_size(db_path, 10000)
}

View File

@ -1,4 +1,3 @@
// net-guardia/src/core/ebpf/health.rs
use std::sync::Arc;
use std::time::Duration;
@ -29,7 +28,6 @@ pub struct SystemHealth {
broadcast_tx: broadcast::Sender<SystemHealthMetrics>,
ingress_interface: String,
egress_interface: String,
// management_interface: String,
}
@ -44,7 +42,6 @@ impl SystemHealth {
broadcast_tx,
ingress_interface: config.ingress_ifname.clone(),
egress_interface: config.egress_ifname.clone(),
// management_interface: config.management_ifindex.clone(),
};
Ok(health)
@ -88,7 +85,6 @@ impl SystemHealth {
&components,
&self.ingress_interface,
&self.egress_interface,
// &self.management_interface,
);
drop(system);
@ -108,7 +104,6 @@ impl SystemHealth {
components: &Components,
ingress_interface: &str,
egress_interface: &str,
// management_interface: &str,
) -> SystemHealthMetrics {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@ -134,7 +129,6 @@ impl SystemHealth {
networks,
ingress_interface,
egress_interface,
// management_interface,
);
let load_average = System::load_average();
@ -218,7 +212,6 @@ impl SystemHealth {
networks: &Networks,
ingress_interface: &str,
egress_interface: &str,
// management_interface: &str,
) -> ConfiguredNetworkStats {
let create_network_stats = |interface_name: &str| -> Option<NetworkStats> {
networks.get(interface_name).map(|network| NetworkStats {
@ -234,7 +227,6 @@ impl SystemHealth {
let ingress = create_network_stats(ingress_interface);
let egress = create_network_stats(egress_interface);
// let management = create_network_stats(management_interface);
if ingress.is_none() {
log!(Health::InterfaceNotFound("Ingress".to_string(), ingress_interface.to_string()));
@ -242,14 +234,10 @@ impl SystemHealth {
if egress.is_none() {
log!(Health::InterfaceNotFound("Egress".to_string(), egress_interface.to_string()));
}
// if management.is_none() {
// warn!("Management interface '{}' not found", management_interface);
// }
ConfiguredNetworkStats {
ingress,
egress,
// management,
}
}
@ -268,7 +256,6 @@ impl SystemHealth {
&components,
&self.ingress_interface,
&self.egress_interface,
// &self.management_interface,
)
}
@ -330,11 +317,6 @@ impl SystemHealth {
status.overall_healthy = false;
status.issues.push("Egress interface not available".to_string());
}
// if metrics.network_stats.management.is_none() {
// status
// .warnings
// .push("Management interface not available".to_string());
// }
status
}

View File

@ -33,7 +33,7 @@ pub struct AppServices {
pub fusion_engine: Arc<FusionEngine>,
pub ml_models: Arc<MLModels>,
pub ml_engine: Arc<Engine>,
pub suricata_engine: Arc<SuricataEngine>,
pub suricata_engine: Option<Arc<SuricataEngine>>,
shutdowns: SegQueue<oneshot::Sender<()>>,
}
@ -77,14 +77,13 @@ impl AppServices {
app_config.ml_cpu,
));
let rule_path = PathBuf::from(env!("RULE_PATH")).join(&app_config.suricata_name);
let eve_socket = PathBuf::from(env!("RULE_EVE_PATH"));
let suricata_engine = SuricataEngine::start(
rule_path,
eve_socket,
fusion_engine.clone(),
)?;
let suricata_engine = if let Some(ref sc) = app_config.suricata {
let rule_path = PathBuf::from(env!("RULE_PATH"));
let eve_socket = PathBuf::from(env!("RULE_EVE_PATH"));
Some(SuricataEngine::start(sc, &rule_path, &eve_socket, fusion_engine.clone())?)
} else {
None
};
Ok(Self {
health: Arc::new(health),

View File

@ -1,3 +1,3 @@
pub mod ebpf;
pub mod infrastructure;
pub mod system;
pub mod ebpf;
pub mod infrastructure;
pub mod system;

View File

@ -112,13 +112,13 @@ impl System {
Self::set_memory_limit()?;
let ingress_xdp: &mut Xdp = self
.ingress_ebpf
.program_mut("net_guardia")
.program_mut("mantis")
.ok_or(EbpfError::ProgramNotFound)?
.try_into()
.map_err(EbpfError::GetProgramFailed)?;
let egress_xdp: &mut Xdp = self
.egress_ebpf
.program_mut("net_guardia")
.program_mut("mantis")
.ok_or(EbpfError::ProgramNotFound)?
.try_into()
.map_err(EbpfError::GetProgramFailed)?;
@ -174,7 +174,7 @@ impl System {
fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
env!("OUT_DIR"),
"/net-guardia-ingress"
"/mantis-ingress"
)))
.map_err(EbpfError::EbpfNotFound)?;
let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
@ -199,7 +199,7 @@ impl System {
fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
env!("OUT_DIR"),
"/net-guardia-egress"
"/mantis-egress"
)))
.map_err(EbpfError::EbpfNotFound)?;
let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;

View File

@ -48,10 +48,6 @@ impl AttackAggregator {
false
}
/// L2 aggregation: track anomalous events per src_ip regardless of src_port.
/// Returns "FLOOD" when total anomalous flows exceed the threshold, or "SCAN"
/// when the number of distinct dst_ports exceeds the scan threshold.
/// A per-src_ip cooldown equal to the window duration prevents alert storms.
pub fn should_alert_src_ip(&mut self, src_ip: &str, dst_port: u16) -> Option<&'static str> {
let now = Instant::now();
let window = self.window_duration;

View File

@ -222,8 +222,6 @@ impl FlowData {
}
}
/// 每個 thread 獨立擁有,不共享,無鎖。
/// RSS 保證同一條 flow 永遠落在同一個 queue。
pub struct FlowTracker {
flows: HashMap<FlowKey, FlowData>,
max_flows: usize,
@ -308,8 +306,6 @@ impl FlowTracker {
.map(|d| d.as_micros() as u64)
.unwrap_or(0);
self.flows.retain(|_, flow| {
// Remove flows that have completed TCP teardown (both FIN or RST)
// or have exceeded the idle timeout
!flow.is_finished()
&& now.saturating_sub(flow.last_time_us) < max_age_us
});

View File

@ -0,0 +1,393 @@
use std::io::{BufRead, BufReader};
use std::mem;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crossbeam::channel::{bounded, Sender};
use macros::log;
use crate::detection::fusion::FusionEngine;
use crate::model::config::SuricataConfig;
use crate::model::error::suricata::SuricataError;
use crate::model::log::suricata::SuricataLog;
use super::output;
const MIRROR_IFACE: &str = "mantis-mirror";
const MIRROR_PEER: &str = "mantis-peer";
const SURICATA_LOG: &str = "/tmp/suricata.log";
const CHANNEL_CAP: usize = 4096;
pub struct SuricataEngine {
tx: Sender<Vec<u8>>,
child: std::sync::Mutex<Child>,
}
impl SuricataEngine {
pub fn start(
config: &SuricataConfig,
rule_path: &Path,
eve_socket: &Path,
fusion: Arc<FusionEngine>,
) -> Result<Arc<Self>, SuricataError> {
Self::setup_veth()?;
let ifindex = Self::get_ifindex(MIRROR_IFACE)?;
if let Some(path) = eve_socket.to_str() {
output::start_eve_reader(path, fusion);
}
let rule_path_str = rule_path.to_str()
.ok_or_else(|| SuricataError::InvalidPath { path: rule_path.display().to_string() })?;
let eve_socket_str = eve_socket.to_str()
.ok_or_else(|| SuricataError::InvalidPath { path: eve_socket.display().to_string() })?;
let suppress = Self::generate_suppress(&config.suppress);
let suppress_fd = Self::yaml_to_memfd(&suppress)?;
let suppress_path = format!("/proc/self/fd/{}", suppress_fd);
let yaml = Self::generate_yaml(config, rule_path_str, eve_socket_str, &suppress_path);
let config_fd = Self::yaml_to_memfd(&yaml)?;
let config_path = format!("/proc/self/fd/{}", config_fd);
let _ = std::fs::remove_file(SURICATA_LOG);
let child = Command::new("suricata")
.args(["-c", &config_path, "-i", MIRROR_PEER, "--runmode=workers"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?;
// Child has inherited both fds; close our copies.
unsafe { libc::close(suppress_fd) };
unsafe { libc::close(config_fd) };
thread::Builder::new()
.name("suricata-log".into())
.spawn(|| {
let log_path = Path::new(SURICATA_LOG);
for _ in 0..100 {
if log_path.exists() { break; }
thread::sleep(Duration::from_millis(100));
}
let file = match std::fs::File::open(log_path) {
Ok(f) => f,
Err(_) => return,
};
let inotify_fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC) };
if inotify_fd >= 0 {
let path_cstr = std::ffi::CString::new(SURICATA_LOG).unwrap();
unsafe { libc::inotify_add_watch(inotify_fd, path_cstr.as_ptr(), libc::IN_MODIFY) };
}
let mut reader = BufReader::new(file);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => {
if inotify_fd >= 0 {
// Block until Suricata writes more data.
let mut buf = [0u8; 64];
let n = unsafe {
libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if n <= 0 { break; }
} else {
thread::sleep(Duration::from_millis(50));
}
}
Ok(_) => {
let trimmed = line.trim_end().to_string();
if trimmed.is_empty() { continue; }
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("error") || lower.starts_with("critical") {
log!(SuricataLog::ProcessError { line: trimmed });
} else if lower.starts_with("warn") || lower.starts_with("notice") {
log!(SuricataLog::ProcessWarn { line: trimmed });
} else {
log!(SuricataLog::ProcessInfo { line: trimmed });
}
}
Err(_) => break,
}
}
if inotify_fd >= 0 {
unsafe { libc::close(inotify_fd) };
}
})
.map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?;
let (tx, rx) = bounded::<Vec<u8>>(CHANNEL_CAP);
thread::Builder::new()
.name("suricata-mirror".into())
.spawn(move || {
let fd = match Self::open_raw_socket() {
Ok(fd) => fd,
Err(e) => {
log!(e);
return;
}
};
let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() };
sll.sll_family = libc::AF_PACKET as u16;
sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be();
sll.sll_ifindex = ifindex as i32;
log!(SuricataLog::MirrorReady { iface: MIRROR_IFACE.into() });
while let Ok(data) = rx.recv() {
unsafe {
libc::sendto(
fd,
data.as_ptr() as *const libc::c_void,
data.len(),
0,
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
);
}
}
unsafe { libc::close(fd) };
log!(SuricataLog::MirrorStopped);
})
.map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?;
log!(SuricataLog::Initialized);
Ok(Arc::new(Self { tx, child: std::sync::Mutex::new(child) }))
}
/* Non-blocking: drops silently when the channel is full under load. */
pub fn inject(&self, data: Vec<u8>) {
match self.tx.try_send(data) {
Ok(()) => {}
Err(crossbeam::channel::TrySendError::Full(_)) => {
log!(SuricataLog::ChannelFull);
}
Err(crossbeam::channel::TrySendError::Disconnected(_)) => {}
}
}
fn generate_suppress(entries: &[String]) -> String {
entries.join("\n") + "\n"
}
fn generate_yaml(config: &SuricataConfig, rule_path: &str, eve_socket: &str, suppress_path: &str) -> String {
let threading = match (config.worker_cpu_set, config.management_cpu) {
(None, None) => r#"threading:
set-cpu-affinity: no"#.to_string(),
(worker, mgmt) => {
let mgmt_cpu = mgmt.unwrap_or(0);
let worker_block = match worker {
Some([start, end]) => format!(r#" - worker-cpu-set:
cpu: [ "{start}-{end}" ]
mode: "balanced""#),
None => String::new(),
};
format!(r#"threading:
set-cpu-affinity: yes
cpu-affinity:
- management-cpu-set:
cpu: [ {mgmt_cpu} ]
{worker_block}"#)
}
};
format!(r#"%YAML 1.1
---
vars:
address-groups:
HOME_NET: "[{home_net}]"
EXTERNAL_NET: "!$HOME_NET"
HTTP_SERVERS: "$HOME_NET"
SMTP_SERVERS: "$HOME_NET"
SQL_SERVERS: "$HOME_NET"
DNS_SERVERS: "$HOME_NET"
TELNET_SERVERS: "$HOME_NET"
AIM_SERVERS: "$EXTERNAL_NET"
DC_SERVERS: "$HOME_NET"
DNP3_SERVER: "$HOME_NET"
DNP3_CLIENT: "$HOME_NET"
MODBUS_CLIENT: "$HOME_NET"
MODBUS_SERVER: "$HOME_NET"
ENIP_CLIENT: "$HOME_NET"
ENIP_SERVER: "$HOME_NET"
port-groups:
HTTP_PORTS: "80"
SHELLCODE_PORTS: "!80"
ORACLE_PORTS: 1521
SSH_PORTS: 22
DNP3_PORTS: 20000
MODBUS_PORTS: 502
FILE_DATA_PORTS: "[$HTTP_PORTS,110,143]"
FTP_PORTS: 21
VXLAN_PORTS: 4789
TEREDO_PORTS: 3544
default-rule-path: {rule_path}
rule-files:
- "*.rules"
threshold-file: {suppress_path}
logging:
default-log-level: notice
outputs:
- console:
enabled: no
- file:
enabled: yes
level: info
filename: {log_path}
outputs:
- eve-log:
enabled: yes
filetype: unix_stream
filename: {eve_socket}
types:
- alert:
payload: no
packet: no
metadata: no
http-body: no
tagged-packets: no
- fast:
enabled: no
- stats:
enabled: no
app-layer:
protocols:
tls:
enabled: yes
http:
enabled: yes
dns:
enabled: yes
smtp:
enabled: yes
ssh:
enabled: yes
af-packet:
- interface: {iface}
threads: {threads}
use-mmap: yes
tpacket-v3: yes
ring-size: {ring_size}
block-size: {block_size}
{threading}
legacy:
uricontent: enabled
host-mode: sniffer-only
"#,
home_net = config.home_net,
rule_path = rule_path,
eve_socket = eve_socket,
suppress_path = suppress_path,
log_path = SURICATA_LOG,
iface = MIRROR_PEER,
threads = config.af_packet_threads,
ring_size = config.af_packet_ring_size,
block_size = config.af_packet_block_size,
threading = threading,
)
}
fn yaml_to_memfd(yaml: &str) -> Result<i32, SuricataError> {
let fd = unsafe {
libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0)
};
if fd < 0 {
let errno = unsafe { *libc::__errno_location() };
return Err(SuricataError::MirrorSetupFailed {
reason: format!("memfd_create: errno {errno}"),
});
}
let bytes = yaml.as_bytes();
let written = unsafe {
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len())
};
if written < 0 {
unsafe { libc::close(fd) };
let errno = unsafe { *libc::__errno_location() };
return Err(SuricataError::MirrorSetupFailed {
reason: format!("memfd write: errno {errno}"),
});
}
unsafe { libc::lseek(fd, 0, libc::SEEK_SET) };
Ok(fd)
}
fn setup_veth() -> Result<(), SuricataError> {
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
Command::new("ip")
.args(["link", "add", MIRROR_IFACE, "type", "veth", "peer", "name", MIRROR_PEER])
.output()
.map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?;
for iface in [MIRROR_IFACE, MIRROR_PEER] {
Command::new("ip")
.args(["link", "set", iface, "up"])
.output()
.map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?;
}
log!(SuricataLog::VethCreated { iface: MIRROR_IFACE.into(), peer: MIRROR_PEER.into() });
Ok(())
}
fn get_ifindex(name: &str) -> Result<u32, SuricataError> {
std::fs::read_to_string(format!("/sys/class/net/{}/ifindex", name))
.map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?
.trim()
.parse()
.map_err(|e: std::num::ParseIntError| SuricataError::MirrorSetupFailed {
reason: format!("ifindex parse: {e}"),
})
}
fn open_raw_socket() -> Result<i32, SuricataError> {
let fd = unsafe {
libc::socket(
libc::AF_PACKET,
libc::SOCK_RAW,
(libc::ETH_P_ALL as u16).to_be() as i32,
)
};
if fd < 0 {
let errno = unsafe { *libc::__errno_location() };
return Err(SuricataError::MirrorSetupFailed {
reason: format!("socket(AF_PACKET): errno {errno}"),
});
}
Ok(fd)
}
}
impl Drop for SuricataEngine {
fn drop(&mut self) {
if let Ok(mut child) = self.child.lock() {
let _ = child.kill();
}
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
}
}

View File

@ -11,8 +11,6 @@ use crate::detection::fusion::FusionEngine;
use crate::model::log::suricata::SuricataLog;
use crate::model::rule_detection::RuleMatch;
/* ── EVE JSON structs (alert subset) ─────────────────────────────────────── */
#[derive(Deserialize)]
struct EveEvent {
event_type: String,
@ -30,8 +28,6 @@ struct EveAlert {
signature: String,
}
/* ── Unix socket EVE reader ──────────────────────────────────────────────── */
pub fn start_eve_reader(socket_path: &str, fusion: Arc<FusionEngine>) {
let path = socket_path.to_owned();

View File

@ -6,6 +6,25 @@ pub struct ConfigTable {
pub config: Config,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SuricataConfig {
pub home_net: String,
pub worker_cpu_set: Option<[u32; 2]>,
pub management_cpu: Option<u32>,
#[serde(default = "default_af_threads")]
pub af_packet_threads: String,
#[serde(default = "default_af_ring_size")]
pub af_packet_ring_size: u32,
#[serde(default = "default_af_block_size")]
pub af_packet_block_size: u32,
#[serde(default)]
pub suppress: Vec<String>,
}
fn default_af_threads() -> String { "auto".to_string() }
fn default_af_ring_size() -> u32 { 2048 }
fn default_af_block_size() -> u32 { 131072 }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config {
pub ingress_ifname: String,
@ -30,35 +49,18 @@ pub struct Config {
pub flow_timeout_us: u64,
pub traffic_logging_mode: bool,
pub traffic_log_csv_path: String,
/// Optional path to an NSS key log file (SSLKEYLOGFILE) for TLS decryption.
/// Only useful in [external]->NetGuardia->[internal] deployments where the
/// internal server can be configured to write TLS session keys.
pub tls_keylog_path: Option<String>,
/// CPU core range [start, end] (inclusive) for XSK packet threads.
/// Threads are distributed round-robin: core = start + (queue_id % (end - start + 1)).
/// Example: [0, 3] with 8 queues spreads 16 threads across cores 0-3 (4 threads each).
/// If absent, no affinity is set.
pub xsk_cpu_set: Option<[u32; 2]>,
/// CPU core pinned to the ML inference spawn_blocking thread.
/// If absent, defaults to the last available core.
pub ml_cpu: Option<u32>,
/// Alert fusion mode: "or" (alert when either source fires) or "and" (require both).
/// Defaults to "or" when absent.
#[serde(default = "default_fusion_mode")]
pub fusion_mode: String,
/// Seconds within which both ML and Rule must fire to be correlated as Fusion.
/// Only used in "or" (corroboration window) and "and" modes. Defaults to 10.
#[serde(default = "default_fusion_window_secs")]
pub fusion_window_secs: u64,
/// Key into ae_thresholds in inference_config.json that selects the active
/// anomaly detection threshold. Valid values: "90".."99", "mean+2std",
/// "mean+1std", "Q3+1.5IQR", "Q3+3.0IQR". Defaults to "95" when absent.
#[serde(default = "default_ae_threshold_method")]
pub ae_threshold_method: String,
/// Path to suricata.yaml. If absent, Suricata rule engine is disabled.
pub suricata_name: String,
/// Suricata rule engine config. If absent, the rule engine is disabled.
pub suricata: Option<SuricataConfig>,
}
fn default_fusion_mode() -> String {
@ -71,4 +73,4 @@ fn default_fusion_window_secs() -> u64 {
fn default_ae_threshold_method() -> String {
"95".to_string()
}
}

View File

@ -23,5 +23,14 @@ loggable! {
#[error("Suricata mirror channel full — packet dropped")]
ChannelFull => tracing::Level::WARN,
#[error("[suricata] {line}")]
ProcessInfo { line: String } => tracing::Level::INFO,
#[error("[suricata] {line}")]
ProcessWarn { line: String } => tracing::Level::WARN,
#[error("[suricata] {line}")]
ProcessError { line: String } => tracing::Level::ERROR,
}
}
}

View File

@ -16,7 +16,7 @@ impl Logging {
fs::create_dir_all(log_directory)
.map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "NetGuardia");
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "Mantis");
let stdout_layer = tracing_subscriber::fmt::layer()
.with_file(true)

View File

@ -0,0 +1,422 @@
101.96.230.94
102.210.82.20
103.147.14.125
103.210.22.17
103.54.101.248
104.155.110.106
104.155.27.113
104.155.78.140
104.199.19.60
104.199.5.111
104.199.85.216
104.248.60.91
104.252.175.235
104.36.21.137
106.13.209.152
107.174.1.138
107.189.24.162
110.43.37.72
111.170.34.11
112.203.68.87
115.190.167.144
116.110.144.105
116.110.145.122
116.110.145.99
116.110.147.23
116.110.152.206
116.110.157.92
116.110.158.50
116.110.19.247
116.110.208.21
116.110.210.253
116.110.21.114
116.110.211.187
116.110.215.174
116.110.217.125
116.110.219.1
116.110.223.85
116.110.2.39
116.110.4.188
116.110.4.244
116.99.168.218
116.99.171.134
116.99.172.175
116.99.172.29
116.99.173.71
116.99.174.114
117.36.231.242
118.145.243.156
118.196.2.158
121.125.67.137
121.165.84.80
124.123.125.62
125.20.210.182
129.146.128.34
129.153.121.56
129.159.149.21
130.12.180.27
130.211.54.242
130.211.57.13
131.161.204.66
131.186.50.157
134.122.110.16
134.209.239.4
13.52.240.248
138.2.102.66
138.68.40.82
139.59.66.69
14.103.78.102
143.198.65.165
144.126.239.128
144.2.91.96
144.31.152.46
144.31.220.38
144.48.8.86
147.189.161.77
148.135.94.99
148.251.195.206
150.109.254.65
151.115.164.231
151.115.79.140
152.32.162.42
152.53.195.231
154.210.208.250
158.173.67.12
159.203.120.106
159.223.196.179
159.89.2.106
160.119.249.227
160.119.76.45
160.191.89.7
161.132.38.88
161.35.98.97
161.97.66.49
162.243.102.84
163.7.9.84
164.164.197.148
164.90.175.206
165.154.224.129
165.154.52.159
165.232.191.119
165.245.180.161
167.172.64.25
167.99.84.148
168.144.79.32
168.144.88.210
171.231.176.146
171.231.178.49
171.231.180.149
171.231.182.74
171.231.186.125
171.231.186.56
171.231.192.123
171.231.192.199
171.231.192.222
171.231.194.32
171.231.196.77
171.231.197.49
171.231.197.57
171.231.198.171
171.231.199.119
171.231.199.134
171.231.199.189
171.243.148.18
171.243.149.139
171.243.149.96
171.243.150.164
171.243.150.172
171.243.150.236
171.243.151.49
178.104.220.57
178.128.121.17
178.175.167.17
178.62.100.247
180.165.29.129
180.76.175.142
181.104.43.225
18.144.169.142
18.144.86.189
183.98.76.106
185.100.212.141
185.156.42.141
185.156.42.211
185.156.43.181
185.178.47.173
185.187.169.10
185.38.148.2
185.67.3.40
189.219.16.249
190.2.135.111
192.252.215.125
193.142.146.230
193.169.241.19
193.233.127.72
193.32.162.82
194.28.87.177
202.165.15.88
202.69.169.162
203.171.18.62
205.254.166.227
206.212.244.18
207.154.214.103
209.97.136.129
210.116.111.28
210.16.103.246
211.37.174.180
212.227.146.182
212.47.251.8
213.209.159.56
217.154.92.76
217.160.162.192
217.160.172.90
221.120.34.164
221.122.121.219
222.108.39.109
2.26.0.198
2.27.42.94
23.175.145.234
27.155.92.28
27.79.0.43
27.79.1.152
27.79.1.69
27.79.2.106
27.79.2.81
27.79.2.88
27.79.3.14
27.79.40.209
27.79.40.53
27.79.40.99
27.79.41.136
27.79.41.138
27.79.41.73
27.79.4.30
27.79.43.128
27.79.43.239
27.79.44.185
27.79.45.122
27.79.45.186
27.79.45.243
27.79.46.17
27.79.46.194
27.79.46.216
27.79.47.20
27.79.47.210
27.79.47.52
27.79.5.188
27.79.5.212
27.79.5.46
27.79.7.163
27.79.7.22
34.118.255.39
34.140.156.133
34.140.239.78
34.140.57.124
34.140.6.40
34.140.77.166
34.140.84.143
34.14.124.6
34.14.127.78
34.14.26.70
34.14.94.132
34.173.87.191
34.22.170.134
34.22.170.190
34.22.181.240
34.22.191.207
34.22.206.213
34.22.216.80
34.22.219.80
34.22.231.214
34.22.249.41
34.34.133.200
34.34.160.10
34.34.163.200
34.34.172.120
34.38.131.179
34.38.135.188
34.38.13.53
34.38.142.34
34.38.185.18
34.38.220.109
34.38.29.170
34.38.33.18
34.38.38.155
34.38.5.115
34.38.6.243
34.38.64.123
34.52.128.71
34.52.170.246
34.52.188.21
34.52.204.179
34.52.208.122
34.52.221.98
34.53.138.146
34.53.140.122
34.53.141.182
34.53.155.90
34.53.183.148
34.53.189.10
34.53.229.179
34.53.250.34
34.62.117.51
34.62.125.18
34.62.130.149
34.62.141.86
34.62.148.105
34.62.180.175
34.62.184.76
34.62.196.33
34.62.199.99
34.62.215.103
34.62.231.139
34.62.232.201
34.62.2.5
34.62.34.27
34.62.44.251
34.76.119.193
34.76.200.186
34.76.9.196
34.77.146.42
34.77.183.8
34.77.185.43
34.77.201.55
34.77.211.171
34.77.245.99
34.77.84.204
34.78.111.164
34.78.129.216
34.78.132.179
34.78.151.236
34.78.154.150
34.78.155.230
34.78.157.177
34.78.158.49
34.78.182.9
34.78.196.247
34.78.21.97
34.78.31.127
34.78.69.136
34.78.9.129
34.79.163.128
34.79.175.147
34.79.191.233
34.79.215.100
34.79.224.24
34.79.238.246
34.79.6.105
34.79.62.169
34.79.72.179
35.187.64.30
35.190.196.156
35.194.141.75
35.195.125.240
35.195.143.58
35.195.148.6
35.195.162.79
35.195.165.181
35.195.18.109
35.195.222.221
35.195.37.202
35.195.40.98
35.195.68.186
35.195.69.175
35.195.71.153
35.195.85.179
35.195.87.98
35.195.90.213
35.205.107.141
35.205.145.95
35.205.157.203
35.205.178.61
35.205.185.0
35.205.205.195
35.205.214.148
35.205.232.103
35.205.236.118
35.205.244.229
35.205.251.123
35.205.36.247
35.205.78.141
35.205.96.69
35.205.98.220
35.233.113.241
35.233.122.202
35.233.15.213
35.233.20.248
35.233.28.146
35.240.0.184
35.240.56.214
35.240.7.56
35.240.80.222
35.240.92.250
35.241.141.196
35.241.154.33
35.241.164.4
35.241.185.64
35.241.250.0
35.86.165.124
35.87.1.37
35.89.149.93
35.91.227.129
35.91.89.48
35.92.116.30
35.92.64.216
36.253.9.69
38.55.145.239
43.135.124.152
44.250.46.59
45.139.122.80
45.142.193.135
45.148.10.183
45.153.34.205
45.156.22.81
45.156.24.224
45.39.12.34
45.55.91.50
45.82.13.133
46.101.94.59
46.62.207.157
46.8.231.219
49.173.65.19
50.2.184.82
50.6.228.52
51.15.254.120
51.15.51.204
51.15.55.248
51.158.155.6
51.158.160.54
51.159.175.158
51.159.189.185
5.129.238.185
5.144.129.17
52.53.177.79
5.253.59.171
5.255.122.180
54.177.65.211
54.193.42.43
54.219.89.58
54.67.94.209
58.209.82.167
58.226.230.112
59.22.201.143
62.210.237.9
64.23.184.75
68.183.8.104
77.22.211.47
80.94.92.168
82.66.91.30
83.145.42.126
83.168.89.181
85.11.167.8
85.239.56.61
86.48.25.218
87.106.149.146
87.121.84.136
87.249.165.241
88.149.145.190
89.190.156.34
91.210.169.154
94.159.98.224
94.183.177.120
94.26.106.206
95.182.98.181

Some files were not shown because too many files have changed in this diff Show More