diff --git a/.claude/commands/research.md b/.claude/commands/research.md index 40f9297..bc1c087 100644 --- a/.claude/commands/research.md +++ b/.claude/commands/research.md @@ -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. diff --git a/.gitignore b/.gitignore index 8ee9506..5557600 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ target/ logs .log .txt -net-guardia/static/web +mantis/static/web *.mmdb node_modules/ .next/ diff --git a/.gitmodules b/.gitmodules index 4fd0839..e28d3a8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.research/findings/tasks/sur-001-c1.md b/.research/findings/tasks/sur-001-c1.md new file mode 100644 index 0000000..522a93b --- /dev/null +++ b/.research/findings/tasks/sur-001-c1.md @@ -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` | 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` | 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. diff --git a/.research/state.toml b/.research/state.toml index a1f31e4..cfdce52 100644 --- a/.research/state.toml +++ b/.research/state.toml @@ -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, diff --git a/CLAUDE.md b/CLAUDE.md index 39ce0d8..b7e6b8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` 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 diff --git a/Cargo.lock b/Cargo.lock index 7553573..c9238c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index d5bed6b..6330603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/README.md b/README.md index 7b47c7d..0494232 100644 --- a/README.md +++ b/README.md @@ -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. - diff --git a/TODO b/TODO index ea1a7be..0182e7b 100644 --- a/TODO +++ b/TODO @@ -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 diff --git a/config.toml b/config.toml index 01884f0..44186ae 100644 --- a/config.toml +++ b/config.toml @@ -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" \ No newline at end of file +# 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", +# ] \ No newline at end of file diff --git a/egress-ebpf/Cargo.toml b/egress-ebpf/Cargo.toml index 5bb79f9..9c7f497 100644 --- a/egress-ebpf/Cargo.toml +++ b/egress-ebpf/Cargo.toml @@ -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 diff --git a/egress-ebpf/src/main.rs b/egress-ebpf/src/main.rs index b9796ff..a2422f3 100644 --- a/egress-ebpf/src/main.rs +++ b/egress-ebpf/src/main.rs @@ -20,7 +20,7 @@ static PARSED_PACKET: PerCpuArray = 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 diff --git a/ingress-ebpf/Cargo.toml b/ingress-ebpf/Cargo.toml index aaff5dc..7d6f850 100644 --- a/ingress-ebpf/Cargo.toml +++ b/ingress-ebpf/Cargo.toml @@ -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 diff --git a/ingress-ebpf/src/main.rs b/ingress-ebpf/src/main.rs index 15675e3..4855b75 100644 --- a/ingress-ebpf/src/main.rs +++ b/ingress-ebpf/src/main.rs @@ -22,7 +22,7 @@ static PARSED_PACKET: PerCpuArray = 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); diff --git a/net-guardia-frontend b/mantis-frontend similarity index 100% rename from net-guardia-frontend rename to mantis-frontend diff --git a/net-guardia/Cargo.toml b/mantis/Cargo.toml similarity index 96% rename from net-guardia/Cargo.toml rename to mantis/Cargo.toml index cc84780..3b965f5 100644 --- a/net-guardia/Cargo.toml +++ b/mantis/Cargo.toml @@ -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" diff --git a/net-guardia/build.rs b/mantis/build.rs similarity index 52% rename from net-guardia/build.rs rename to mantis/build.rs index 7528c31..6a15d0f 100644 --- a/net-guardia/build.rs +++ b/mantis/build.rs @@ -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(()) } - diff --git a/net-guardia/src/core/ebpf/access_control.rs b/mantis/src/core/ebpf/access_control.rs similarity index 100% rename from net-guardia/src/core/ebpf/access_control.rs rename to mantis/src/core/ebpf/access_control.rs diff --git a/net-guardia/src/core/ebpf/mod.rs b/mantis/src/core/ebpf/mod.rs similarity index 94% rename from net-guardia/src/core/ebpf/mod.rs rename to mantis/src/core/ebpf/mod.rs index 15e9573..f3bccaa 100644 --- a/net-guardia/src/core/ebpf/mod.rs +++ b/mantis/src/core/ebpf/mod.rs @@ -51,12 +51,12 @@ impl EbpfServices { pub async fn run( self: Arc, ml_engine: Arc, - suricata_engine: Arc, + suricata_engine: Option>, ) -> 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); diff --git a/net-guardia/src/core/ebpf/service.rs b/mantis/src/core/ebpf/service.rs similarity index 100% rename from net-guardia/src/core/ebpf/service.rs rename to mantis/src/core/ebpf/service.rs diff --git a/net-guardia/src/core/ebpf/statistics.rs b/mantis/src/core/ebpf/statistics.rs similarity index 100% rename from net-guardia/src/core/ebpf/statistics.rs rename to mantis/src/core/ebpf/statistics.rs diff --git a/net-guardia/src/core/ebpf/xsk_manager.rs b/mantis/src/core/ebpf/xsk_manager.rs similarity index 100% rename from net-guardia/src/core/ebpf/xsk_manager.rs rename to mantis/src/core/ebpf/xsk_manager.rs diff --git a/net-guardia/src/core/infrastructure/app_config.rs b/mantis/src/core/infrastructure/app_config.rs similarity index 100% rename from net-guardia/src/core/infrastructure/app_config.rs rename to mantis/src/core/infrastructure/app_config.rs diff --git a/net-guardia/src/core/infrastructure/detection_alert.rs b/mantis/src/core/infrastructure/detection_alert.rs similarity index 100% rename from net-guardia/src/core/infrastructure/detection_alert.rs rename to mantis/src/core/infrastructure/detection_alert.rs diff --git a/net-guardia/src/core/infrastructure/geoip.rs b/mantis/src/core/infrastructure/geoip.rs similarity index 97% rename from net-guardia/src/core/infrastructure/geoip.rs rename to mantis/src/core/infrastructure/geoip.rs index 0bca797..1a59d41 100644 --- a/net-guardia/src/core/infrastructure/geoip.rs +++ b/mantis/src/core/infrastructure/geoip.rs @@ -21,7 +21,7 @@ pub struct GeoIpService { impl GeoIpService { pub fn new(db_name: &str) -> Result { - 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) } diff --git a/net-guardia/src/core/infrastructure/health.rs b/mantis/src/core/infrastructure/health.rs similarity index 93% rename from net-guardia/src/core/infrastructure/health.rs rename to mantis/src/core/infrastructure/health.rs index 675803e..ce4b33d 100644 --- a/net-guardia/src/core/infrastructure/health.rs +++ b/mantis/src/core/infrastructure/health.rs @@ -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, 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 { 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 } diff --git a/net-guardia/src/core/infrastructure/mod.rs b/mantis/src/core/infrastructure/mod.rs similarity index 90% rename from net-guardia/src/core/infrastructure/mod.rs rename to mantis/src/core/infrastructure/mod.rs index b7ab6cd..eea9b0a 100644 --- a/net-guardia/src/core/infrastructure/mod.rs +++ b/mantis/src/core/infrastructure/mod.rs @@ -33,7 +33,7 @@ pub struct AppServices { pub fusion_engine: Arc, pub ml_models: Arc, pub ml_engine: Arc, - pub suricata_engine: Arc, + pub suricata_engine: Option>, shutdowns: SegQueue>, } @@ -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), diff --git a/net-guardia/src/core/mod.rs b/mantis/src/core/mod.rs similarity index 94% rename from net-guardia/src/core/mod.rs rename to mantis/src/core/mod.rs index 18d067e..992c3fb 100644 --- a/net-guardia/src/core/mod.rs +++ b/mantis/src/core/mod.rs @@ -1,3 +1,3 @@ -pub mod ebpf; -pub mod infrastructure; -pub mod system; +pub mod ebpf; +pub mod infrastructure; +pub mod system; diff --git a/net-guardia/src/core/system.rs b/mantis/src/core/system.rs similarity index 98% rename from net-guardia/src/core/system.rs rename to mantis/src/core/system.rs index 3cd61cc..6db03f2 100644 --- a/net-guardia/src/core/system.rs +++ b/mantis/src/core/system.rs @@ -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), 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), 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)?; diff --git a/net-guardia/src/detection/fusion.rs b/mantis/src/detection/fusion.rs similarity index 100% rename from net-guardia/src/detection/fusion.rs rename to mantis/src/detection/fusion.rs diff --git a/net-guardia/src/detection/ml/aggregator.rs b/mantis/src/detection/ml/aggregator.rs similarity index 90% rename from net-guardia/src/detection/ml/aggregator.rs rename to mantis/src/detection/ml/aggregator.rs index c4bcbce..d1ee27b 100644 --- a/net-guardia/src/detection/ml/aggregator.rs +++ b/mantis/src/detection/ml/aggregator.rs @@ -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; diff --git a/net-guardia/src/detection/ml/config_loader.rs b/mantis/src/detection/ml/config_loader.rs similarity index 100% rename from net-guardia/src/detection/ml/config_loader.rs rename to mantis/src/detection/ml/config_loader.rs diff --git a/net-guardia/src/detection/ml/engine.rs b/mantis/src/detection/ml/engine.rs similarity index 100% rename from net-guardia/src/detection/ml/engine.rs rename to mantis/src/detection/ml/engine.rs diff --git a/net-guardia/src/detection/ml/feature_extractor.rs b/mantis/src/detection/ml/feature_extractor.rs similarity index 100% rename from net-guardia/src/detection/ml/feature_extractor.rs rename to mantis/src/detection/ml/feature_extractor.rs diff --git a/net-guardia/src/detection/ml/flow_tracker.rs b/mantis/src/detection/ml/flow_tracker.rs similarity index 98% rename from net-guardia/src/detection/ml/flow_tracker.rs rename to mantis/src/detection/ml/flow_tracker.rs index e39ff9b..543c61d 100644 --- a/net-guardia/src/detection/ml/flow_tracker.rs +++ b/mantis/src/detection/ml/flow_tracker.rs @@ -222,8 +222,6 @@ impl FlowData { } } -/// 每個 thread 獨立擁有,不共享,無鎖。 -/// RSS 保證同一條 flow 永遠落在同一個 queue。 pub struct FlowTracker { flows: HashMap, 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 }); diff --git a/net-guardia/src/detection/ml/inference.rs b/mantis/src/detection/ml/inference.rs similarity index 100% rename from net-guardia/src/detection/ml/inference.rs rename to mantis/src/detection/ml/inference.rs diff --git a/net-guardia/src/detection/ml/mod.rs b/mantis/src/detection/ml/mod.rs similarity index 100% rename from net-guardia/src/detection/ml/mod.rs rename to mantis/src/detection/ml/mod.rs diff --git a/net-guardia/src/detection/ml/model_loader.rs b/mantis/src/detection/ml/model_loader.rs similarity index 100% rename from net-guardia/src/detection/ml/model_loader.rs rename to mantis/src/detection/ml/model_loader.rs diff --git a/net-guardia/src/detection/ml/traffic_logger.rs b/mantis/src/detection/ml/traffic_logger.rs similarity index 100% rename from net-guardia/src/detection/ml/traffic_logger.rs rename to mantis/src/detection/ml/traffic_logger.rs diff --git a/net-guardia/src/detection/mod.rs b/mantis/src/detection/mod.rs similarity index 100% rename from net-guardia/src/detection/mod.rs rename to mantis/src/detection/mod.rs diff --git a/mantis/src/detection/suricata/engine.rs b/mantis/src/detection/suricata/engine.rs new file mode 100644 index 0000000..e704022 --- /dev/null +++ b/mantis/src/detection/suricata/engine.rs @@ -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>, + child: std::sync::Mutex, +} + +impl SuricataEngine { + pub fn start( + config: &SuricataConfig, + rule_path: &Path, + eve_socket: &Path, + fusion: Arc, + ) -> Result, 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::>(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::() 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) { + 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 { + 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 { + 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 { + 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(); + } +} \ No newline at end of file diff --git a/net-guardia/src/detection/suricata/mod.rs b/mantis/src/detection/suricata/mod.rs similarity index 100% rename from net-guardia/src/detection/suricata/mod.rs rename to mantis/src/detection/suricata/mod.rs diff --git a/net-guardia/src/detection/suricata/output.rs b/mantis/src/detection/suricata/output.rs similarity index 89% rename from net-guardia/src/detection/suricata/output.rs rename to mantis/src/detection/suricata/output.rs index c5953f3..a3eb419 100644 --- a/net-guardia/src/detection/suricata/output.rs +++ b/mantis/src/detection/suricata/output.rs @@ -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) { let path = socket_path.to_owned(); diff --git a/net-guardia/src/main.rs b/mantis/src/main.rs similarity index 100% rename from net-guardia/src/main.rs rename to mantis/src/main.rs diff --git a/net-guardia/src/model/config.rs b/mantis/src/model/config.rs similarity index 53% rename from net-guardia/src/model/config.rs rename to mantis/src/model/config.rs index dbe0f09..fc489bf 100644 --- a/net-guardia/src/model/config.rs +++ b/mantis/src/model/config.rs @@ -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, + #[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, +} + +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, - /// 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, - /// 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, } fn default_fusion_mode() -> String { @@ -71,4 +73,4 @@ fn default_fusion_window_secs() -> u64 { fn default_ae_threshold_method() -> String { "95".to_string() -} +} \ No newline at end of file diff --git a/net-guardia/src/model/direction.rs b/mantis/src/model/direction.rs similarity index 100% rename from net-guardia/src/model/direction.rs rename to mantis/src/model/direction.rs diff --git a/net-guardia/src/model/error/ebpf.rs b/mantis/src/model/error/ebpf.rs similarity index 100% rename from net-guardia/src/model/error/ebpf.rs rename to mantis/src/model/error/ebpf.rs diff --git a/net-guardia/src/model/error/http.rs b/mantis/src/model/error/http.rs similarity index 100% rename from net-guardia/src/model/error/http.rs rename to mantis/src/model/error/http.rs diff --git a/net-guardia/src/model/error/io.rs b/mantis/src/model/error/io.rs similarity index 100% rename from net-guardia/src/model/error/io.rs rename to mantis/src/model/error/io.rs diff --git a/net-guardia/src/model/error/misc.rs b/mantis/src/model/error/misc.rs similarity index 100% rename from net-guardia/src/model/error/misc.rs rename to mantis/src/model/error/misc.rs diff --git a/net-guardia/src/model/error/ml.rs b/mantis/src/model/error/ml.rs similarity index 100% rename from net-guardia/src/model/error/ml.rs rename to mantis/src/model/error/ml.rs diff --git a/net-guardia/src/model/error/mod.rs b/mantis/src/model/error/mod.rs similarity index 100% rename from net-guardia/src/model/error/mod.rs rename to mantis/src/model/error/mod.rs diff --git a/net-guardia/src/model/error/suricata.rs b/mantis/src/model/error/suricata.rs similarity index 100% rename from net-guardia/src/model/error/suricata.rs rename to mantis/src/model/error/suricata.rs diff --git a/net-guardia/src/model/error/system.rs b/mantis/src/model/error/system.rs similarity index 100% rename from net-guardia/src/model/error/system.rs rename to mantis/src/model/error/system.rs diff --git a/net-guardia/src/model/geo_stats.rs b/mantis/src/model/geo_stats.rs similarity index 100% rename from net-guardia/src/model/geo_stats.rs rename to mantis/src/model/geo_stats.rs diff --git a/net-guardia/src/model/health.rs b/mantis/src/model/health.rs similarity index 100% rename from net-guardia/src/model/health.rs rename to mantis/src/model/health.rs diff --git a/net-guardia/src/model/ip_address.rs b/mantis/src/model/ip_address.rs similarity index 100% rename from net-guardia/src/model/ip_address.rs rename to mantis/src/model/ip_address.rs diff --git a/net-guardia/src/model/list_type.rs b/mantis/src/model/list_type.rs similarity index 100% rename from net-guardia/src/model/list_type.rs rename to mantis/src/model/list_type.rs diff --git a/net-guardia/src/model/log/ebpf.rs b/mantis/src/model/log/ebpf.rs similarity index 100% rename from net-guardia/src/model/log/ebpf.rs rename to mantis/src/model/log/ebpf.rs diff --git a/net-guardia/src/model/log/health.rs b/mantis/src/model/log/health.rs similarity index 100% rename from net-guardia/src/model/log/health.rs rename to mantis/src/model/log/health.rs diff --git a/net-guardia/src/model/log/http.rs b/mantis/src/model/log/http.rs similarity index 100% rename from net-guardia/src/model/log/http.rs rename to mantis/src/model/log/http.rs diff --git a/net-guardia/src/model/log/misc.rs b/mantis/src/model/log/misc.rs similarity index 100% rename from net-guardia/src/model/log/misc.rs rename to mantis/src/model/log/misc.rs diff --git a/net-guardia/src/model/log/ml.rs b/mantis/src/model/log/ml.rs similarity index 100% rename from net-guardia/src/model/log/ml.rs rename to mantis/src/model/log/ml.rs diff --git a/net-guardia/src/model/log/mod.rs b/mantis/src/model/log/mod.rs similarity index 100% rename from net-guardia/src/model/log/mod.rs rename to mantis/src/model/log/mod.rs diff --git a/net-guardia/src/model/log/suricata.rs b/mantis/src/model/log/suricata.rs similarity index 75% rename from net-guardia/src/model/log/suricata.rs rename to mantis/src/model/log/suricata.rs index 1f468b6..ebbd808 100644 --- a/net-guardia/src/model/log/suricata.rs +++ b/mantis/src/model/log/suricata.rs @@ -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, } -} +} \ No newline at end of file diff --git a/net-guardia/src/model/log/system.rs b/mantis/src/model/log/system.rs similarity index 100% rename from net-guardia/src/model/log/system.rs rename to mantis/src/model/log/system.rs diff --git a/net-guardia/src/model/ml_detection.rs b/mantis/src/model/ml_detection.rs similarity index 100% rename from net-guardia/src/model/ml_detection.rs rename to mantis/src/model/ml_detection.rs diff --git a/net-guardia/src/model/mod.rs b/mantis/src/model/mod.rs similarity index 100% rename from net-guardia/src/model/mod.rs rename to mantis/src/model/mod.rs diff --git a/net-guardia/src/model/rule_detection.rs b/mantis/src/model/rule_detection.rs similarity index 100% rename from net-guardia/src/model/rule_detection.rs rename to mantis/src/model/rule_detection.rs diff --git a/net-guardia/src/model/time_type.rs b/mantis/src/model/time_type.rs similarity index 100% rename from net-guardia/src/model/time_type.rs rename to mantis/src/model/time_type.rs diff --git a/net-guardia/src/utils/boot_time.rs b/mantis/src/utils/boot_time.rs similarity index 100% rename from net-guardia/src/utils/boot_time.rs rename to mantis/src/utils/boot_time.rs diff --git a/net-guardia/src/utils/cpu_affinity.rs b/mantis/src/utils/cpu_affinity.rs similarity index 100% rename from net-guardia/src/utils/cpu_affinity.rs rename to mantis/src/utils/cpu_affinity.rs diff --git a/net-guardia/src/utils/ip_address.rs b/mantis/src/utils/ip_address.rs similarity index 100% rename from net-guardia/src/utils/ip_address.rs rename to mantis/src/utils/ip_address.rs diff --git a/net-guardia/src/utils/logging.rs b/mantis/src/utils/logging.rs similarity index 97% rename from net-guardia/src/utils/logging.rs rename to mantis/src/utils/logging.rs index c755fb4..b88090c 100644 --- a/net-guardia/src/utils/logging.rs +++ b/mantis/src/utils/logging.rs @@ -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) diff --git a/net-guardia/src/utils/mod.rs b/mantis/src/utils/mod.rs similarity index 100% rename from net-guardia/src/utils/mod.rs rename to mantis/src/utils/mod.rs diff --git a/net-guardia/src/utils/packet_parser.rs b/mantis/src/utils/packet_parser.rs similarity index 100% rename from net-guardia/src/utils/packet_parser.rs rename to mantis/src/utils/packet_parser.rs diff --git a/net-guardia/src/utils/static_files.rs b/mantis/src/utils/static_files.rs similarity index 100% rename from net-guardia/src/utils/static_files.rs rename to mantis/src/utils/static_files.rs diff --git a/net-guardia/src/web/api/control/access_control.rs b/mantis/src/web/api/control/access_control.rs similarity index 100% rename from net-guardia/src/web/api/control/access_control.rs rename to mantis/src/web/api/control/access_control.rs diff --git a/net-guardia/src/web/api/control/mod.rs b/mantis/src/web/api/control/mod.rs similarity index 100% rename from net-guardia/src/web/api/control/mod.rs rename to mantis/src/web/api/control/mod.rs diff --git a/net-guardia/src/web/api/control/service.rs b/mantis/src/web/api/control/service.rs similarity index 100% rename from net-guardia/src/web/api/control/service.rs rename to mantis/src/web/api/control/service.rs diff --git a/net-guardia/src/web/api/control/statistics.rs b/mantis/src/web/api/control/statistics.rs similarity index 100% rename from net-guardia/src/web/api/control/statistics.rs rename to mantis/src/web/api/control/statistics.rs diff --git a/net-guardia/src/web/api/default.rs b/mantis/src/web/api/default.rs similarity index 100% rename from net-guardia/src/web/api/default.rs rename to mantis/src/web/api/default.rs diff --git a/net-guardia/src/web/api/detection_alert.rs b/mantis/src/web/api/detection_alert.rs similarity index 100% rename from net-guardia/src/web/api/detection_alert.rs rename to mantis/src/web/api/detection_alert.rs diff --git a/net-guardia/src/web/api/health.rs b/mantis/src/web/api/health.rs similarity index 100% rename from net-guardia/src/web/api/health.rs rename to mantis/src/web/api/health.rs diff --git a/net-guardia/src/web/api/misc.rs b/mantis/src/web/api/misc.rs similarity index 100% rename from net-guardia/src/web/api/misc.rs rename to mantis/src/web/api/misc.rs diff --git a/net-guardia/src/web/api/ml_alert.rs b/mantis/src/web/api/ml_alert.rs similarity index 100% rename from net-guardia/src/web/api/ml_alert.rs rename to mantis/src/web/api/ml_alert.rs diff --git a/net-guardia/src/web/api/mod.rs b/mantis/src/web/api/mod.rs similarity index 100% rename from net-guardia/src/web/api/mod.rs rename to mantis/src/web/api/mod.rs diff --git a/net-guardia/src/web/mod.rs b/mantis/src/web/mod.rs similarity index 100% rename from net-guardia/src/web/mod.rs rename to mantis/src/web/mod.rs diff --git a/net-guardia/src/web/websocket/alert_websocket.rs b/mantis/src/web/websocket/alert_websocket.rs similarity index 100% rename from net-guardia/src/web/websocket/alert_websocket.rs rename to mantis/src/web/websocket/alert_websocket.rs diff --git a/net-guardia/src/web/websocket/flow_websocket.rs b/mantis/src/web/websocket/flow_websocket.rs similarity index 100% rename from net-guardia/src/web/websocket/flow_websocket.rs rename to mantis/src/web/websocket/flow_websocket.rs diff --git a/net-guardia/src/web/websocket/health_websocket.rs b/mantis/src/web/websocket/health_websocket.rs similarity index 100% rename from net-guardia/src/web/websocket/health_websocket.rs rename to mantis/src/web/websocket/health_websocket.rs diff --git a/net-guardia/src/web/websocket/mod.rs b/mantis/src/web/websocket/mod.rs similarity index 100% rename from net-guardia/src/web/websocket/mod.rs rename to mantis/src/web/websocket/mod.rs diff --git a/net-guardia/static/artifacts/deep_autoencoder.onnx b/mantis/static/artifacts/deep_autoencoder.onnx similarity index 100% rename from net-guardia/static/artifacts/deep_autoencoder.onnx rename to mantis/static/artifacts/deep_autoencoder.onnx diff --git a/net-guardia/static/artifacts/full_config.json b/mantis/static/artifacts/full_config.json similarity index 100% rename from net-guardia/static/artifacts/full_config.json rename to mantis/static/artifacts/full_config.json diff --git a/net-guardia/static/artifacts/inference_config.json b/mantis/static/artifacts/inference_config.json similarity index 100% rename from net-guardia/static/artifacts/inference_config.json rename to mantis/static/artifacts/inference_config.json diff --git a/net-guardia/static/artifacts/info.txt b/mantis/static/artifacts/info.txt similarity index 100% rename from net-guardia/static/artifacts/info.txt rename to mantis/static/artifacts/info.txt diff --git a/net-guardia/static/db/info.txt b/mantis/static/db/info.txt similarity index 100% rename from net-guardia/static/db/info.txt rename to mantis/static/db/info.txt diff --git a/net-guardia/static/geo/info.txt b/mantis/static/geo/info.txt similarity index 100% rename from net-guardia/static/geo/info.txt rename to mantis/static/geo/info.txt diff --git a/mantis/static/rules/compromised-ips.txt b/mantis/static/rules/compromised-ips.txt new file mode 100644 index 0000000..a563700 --- /dev/null +++ b/mantis/static/rules/compromised-ips.txt @@ -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 diff --git a/net-guardia/static/rules/info.txt b/mantis/static/rules/info.txt similarity index 100% rename from net-guardia/static/rules/info.txt rename to mantis/static/rules/info.txt diff --git a/net-guardia/static/rules/suppress.conf b/mantis/static/rules/suppress.conf similarity index 100% rename from net-guardia/static/rules/suppress.conf rename to mantis/static/rules/suppress.conf diff --git a/net-guardia/static/rules/suricata.yaml b/mantis/static/rules/suricata.yaml similarity index 75% rename from net-guardia/static/rules/suricata.yaml rename to mantis/static/rules/suricata.yaml index 6306e8a..475cd66 100644 --- a/net-guardia/static/rules/suricata.yaml +++ b/mantis/static/rules/suricata.yaml @@ -30,12 +30,22 @@ vars: VXLAN_PORTS: 4789 TEREDO_PORTS: 3544 -default-rule-path: ./net-guardia/static/rules +default-rule-path: ./mantis/static/rules rule-files: - "*.rules" -threshold-file: ./net-guardia/static/rules/suppress.conf +threshold-file: ./mantis/static/rules/suppress.conf + +logging: + default-log-level: notice + outputs: + - console: + enabled: no + - file: + enabled: yes + level: info + filename: /tmp/suricata.log outputs: - eve-log: @@ -68,14 +78,20 @@ app-layer: enabled: yes af-packet: - - interface: ng-mirror-peer + - interface: mantis-peer use-mmap: yes tpacket-v3: yes ring-size: 2048 block-size: 131072 threading: - set-cpu-affinity: no + set-cpu-affinity: yes + cpu-affinity: + - management-cpu-set: + cpu: [ 0 ] + - worker-cpu-set: + cpu: [ "4-6" ] + mode: "balanced" legacy: uricontent: enabled \ No newline at end of file diff --git a/net-guardia/src/detection/suricata/engine.rs b/net-guardia/src/detection/suricata/engine.rs deleted file mode 100644 index 8708905..0000000 --- a/net-guardia/src/detection/suricata/engine.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::mem; -use std::path::PathBuf; -use std::process::{Child, Command}; -use std::sync::Arc; -use std::thread; - -use crossbeam::channel::{bounded, Sender}; -use macros::log; - -use crate::detection::fusion::FusionEngine; -use crate::model::error::suricata::SuricataError; -use crate::model::log::suricata::SuricataLog; - -use super::output; - -const MIRROR_IFACE: &str = "ng-mirror"; -const MIRROR_PEER: &str = "ng-mirror-peer"; - -/* Channel capacity matches the old libsuricata engine. */ -const CHANNEL_CAP: usize = 4096; - -pub struct SuricataEngine { - tx: Sender>, - child: std::sync::Mutex, -} - -impl SuricataEngine { - pub fn start( - yaml_path: PathBuf, - eve_socket: PathBuf, - fusion: Arc, - ) -> Result, 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 yaml = yaml_path.to_str() - .ok_or_else(|| SuricataError::InvalidPath { path: yaml_path.display().to_string() })?; - - let child = Command::new("suricata") - .args(["-c", yaml, "-i", MIRROR_PEER, "--runmode=workers"]) - .spawn() - .map_err(|e| SuricataError::ProcessSpawnFailed { reason: e.to_string() })?; - - let (tx, rx) = bounded::>(CHANNEL_CAP); - - /* Mirror worker: reads packet data from channel, sends via raw socket. */ - 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::() 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) { - match self.tx.try_send(data) { - Ok(()) => {} - Err(crossbeam::channel::TrySendError::Full(_)) => { - log!(SuricataLog::ChannelFull); - } - Err(crossbeam::channel::TrySendError::Disconnected(_)) => {} - } - } - - fn setup_veth() -> Result<(), SuricataError> { - /* Remove stale pair if it exists. */ - 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 { - 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 { - 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(); - } -} diff --git a/net-guardia/static/geo/GeoLite2-City.mmdb b/net-guardia/static/geo/GeoLite2-City.mmdb deleted file mode 100644 index 2df0b8b..0000000 Binary files a/net-guardia/static/geo/GeoLite2-City.mmdb and /dev/null differ