# Mantis Network intrusion detection system combining eBPF packet capture with ML-based anomaly detection and Suricata/Snort rule matching. ## Language Convention - **Conversation**: Always use Traditional Chinese (繁體中文) - **Files, docs, comments, code**: Always use English - This applies to ALL generated content without exception ## Dependency Policy - ML inference: ort-tract (pure Rust, no native ORT binary) Do not switch to native ORT without explicit discussion. - Async runtime: tokio only, do not add async-std or smol. - Logging: macros::log! only, do not add log crate or use tracing:: directly. - Locking: std::sync::Mutex is preferred; parking_lot::Mutex where performance matters. - ort version is pinned to =2.0.0-rc.12, do not upgrade without checking ort-tract compatibility. ## Workspace Structure ``` 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 mantis-frontend/ - Next.js web UI ``` ## Build 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 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 .../mantis-ingress` - `couldn't read .../mantis-egress` ## mantis Source Layout ``` src/ ├── core/ │ ├── app_state.rs - AppState (Clone) shared across all axum handlers │ ├── ebpf/ - XDP/AF_XDP packet capture, access control │ └── infrastructure/- AppServices init, AppConfig, AppDb, GeoIP ├── detection/ │ ├── ml/ - ML inference pipeline │ │ ├── engine.rs - inference loop (tokio interval) │ │ ├── flow_tracker.rs - per-flow packet aggregation │ │ ├── feature_extractor.rs - flow -> feature vector │ │ ├── inference.rs - sliding window + autoencoder MSE │ │ ├── model_loader.rs - ort Session creation │ │ ├── config_loader.rs - inference_config.json │ │ ├── aggregator.rs - attack event dedup/aggregation │ │ └── traffic_logger.rs - CSV recording mode │ └── rule/ - Suricata/Snort rule matching (vectorscan) ├── model/ │ ├── error/ - one file per domain (ml, ebpf, http, auth, ...) │ ├── log/ - one file per domain (ml, ebpf, http, auth, ...) │ └── ml_detection.rs- shared ML types (FlowKey, DetectionResult, ...) ├── web/ │ ├── api/ - axum route handlers │ ├── middleware/ - auth.rs: AuthenticatedUser (FromRequestParts) │ └── websocket/ - alert, health, flow WebSocket handlers └── utils/ - packet parsing, logging setup, boot time ``` ## Macros (always use these) ### log! Logs an error or loggable value. Routes to tracing level automatically. ```rust use macros::log; log!(MLError::ModelLoadFailed { path }); log!(MLLog::InferenceCompleted { total_flows, anomaly, benign, duration_ms, throughput }); ``` ### traceable! Defines an error enum in `src/model/error/.rs`. ```rust traceable! { MLError { #[no_source] #[error("Failed to load ONNX model from: {path:?}")] ModelLoadFailed { path: PathBuf } => tracing::Level::ERROR, } } ``` ### loggable! Defines a log message enum in `src/model/log/.rs`. ```rust loggable! { MLLog { #[error("ML artifacts loaded - {info}")] ModelsLoaded { info: String } => tracing::Level::INFO, } } ``` ## Conventions - New error variant -> add to `src/model/error/.rs` using `traceable!` - New log message -> add to `src/model/log/.rs` using `loggable!` - Never use `println!`, `eprintln!`, or `tracing::` directly — always use `log!()` - Shared data types (structs, enums used across modules) go in `src/model/` - Comments in English only, no special characters ## ML Pipeline Inference runs on a tokio interval (default 5s): ``` eBPF packet -> FlowTracker (per-flow stats) -> FlowFeatures (extract + normalize ~80 features) -> Inference (sliding window buffer per src_ip) -> ort Session (LSTM autoencoder, MSE score) -> AttackAggregator -> MLAlert (WebSocket) ``` Inference engine: ort-tract (pure Rust, no native ORT binary required). `ort::set_api(ort_tract::api())` must be called before any Session is created. Session is wrapped in `Mutex` because `Session::run` requires `&mut self`. ## Config Runtime config: `config.toml` ML artifacts: `mantis/static/artifacts/` - `deep_autoencoder.onnx` - LSTM autoencoder model - `inference_config.json` - window size, feature names, scaler params, threshold Auth DB: `static/db/app.db` (SQLCipher-encrypted, path configurable via `[Config.auth].db_path`) ## HTTP API Base URL: `http://:8080` All routes that require auth expect `Authorization: Bearer ` header. Auth is disabled if `[Config.auth]` is absent from config.toml. ### Auth | Method | Path | Auth | Request body | Response | |--------|------|------|--------------|----------| | POST | `/auth/login` | No | `{"username":"…","password":"…"}` | `{"token":""}` | | GET | `/auth/me` | Bearer | — | `{"id":"…","username":"…","role":"…"}` | | POST | `/auth/logout` | Bearer | — | 200 OK (stateless, client discards token) | JWT claims: `{ sub, username, role, exp }`. Default TTL: 86400s. Default admin: username=`admin`, password=`admin` (seeded on first boot). ### eBPF Access Control — `/ebpf/access_control` | Method | Path | Body | Description | |--------|------|------|-------------| | GET | `/{nic}/ipv4/{flow}/{list_type}` | — | Get IPv4 list | | PUT | `/{nic}/ipv4/{flow}/{list_type}` | `SocketAddrV4` JSON | Add entry | | DELETE | `/{nic}/ipv4/{flow}/{list_type}` | `SocketAddrV4` JSON | Remove entry | | GET | `/{nic}/ipv6/{flow}/{list_type}` | — | Get IPv6 list | | PUT | `/{nic}/ipv6/{flow}/{list_type}` | `SocketAddrV6` JSON | Add entry | | DELETE | `/{nic}/ipv6/{flow}/{list_type}` | `SocketAddrV6` JSON | Remove entry | `nic`: `ingress` | `egress` — `flow`: `source` | `destination` — `list_type`: `whitelist` | `blacklist` Ingress access control targets the ingress eBPF (inbound packets from external network). Egress access control targets the egress eBPF (outbound packets from internal network). `source` matches the packet's src IP/port; `destination` matches dst IP/port. ### eBPF Service Control — `/ebpf/service` | Method | Path | Body | Description | |--------|------|------|-------------| | GET | `/ipv4/http_service` | — | List IPv4 HTTP service entries | | PUT | `/ipv4/http_service` | `[SocketAddrV4, [HttpMethod]]` | Add HTTP service | | DELETE | `/ipv4/http_service` | `[SocketAddrV4, [HttpMethod]]` | Remove HTTP service | | GET | `/ipv6/http_service` | — | List IPv6 HTTP service entries | | PUT | `/ipv6/http_service` | `[SocketAddrV6, [HttpMethod]]` | Add HTTP service | | DELETE | `/ipv6/http_service` | `[SocketAddrV6, [HttpMethod]]` | Remove HTTP service | | GET | `/ssh_white_list` | — | SSH whitelist enabled? | | POST | `/ssh_white_list/enable` | — | Enable SSH whitelist | | POST | `/ssh_white_list/disable` | — | Disable SSH whitelist | | GET/PUT/DELETE | `/ipv4/ssh_service` | `SocketAddrV4` | SSH service list | | GET/PUT/DELETE | `/ipv6/ssh_service` | `SocketAddrV6` | SSH service list | | GET/PUT/DELETE | `/ipv4/ssh_white_list` | `Ipv4Addr` | SSH whitelist | | GET/PUT/DELETE | `/ipv6/ssh_white_list` | `Ipv6Addr` | SSH whitelist | | GET/PUT/DELETE | `/ipv4/ssh_black_list` | `Ipv4Addr` | SSH blacklist | | GET/PUT/DELETE | `/ipv6/ssh_black_list` | `Ipv6Addr` | SSH blacklist | ### eBPF Statistics — `/ebpf/statistics` | Method | Path | Description | |--------|------|-------------| | GET | `/get/ipv4/{direction}/{flow_direction}/{time_type}` | IPv4 flow data snapshot | | GET | `/get/ipv6/{direction}/{flow_direction}/{time_type}` | IPv6 flow data snapshot | | GET (WS) | `/websocket/ipv4/{direction}/{flow_direction}/{time_type}` | IPv4 flow stream | | GET (WS) | `/websocket/ipv6/{direction}/{flow_direction}/{time_type}` | IPv6 flow stream | `direction`: `inbound` | `outbound` — `flow_direction`: `source` | `destination` `time_type`: `per_second` | `per_minute` | `per_hour` ### Health — `/health` | Method | Path | Description | |--------|------|-------------| | GET | `/metrics` | Current system metrics (CPU, memory, etc.) | | GET | `/status` | Boolean system health | | GET (WS) | `/websocket/metrics` | Live metrics stream | ### Detection — `/detection` | Method | Path | Description | |--------|------|-------------| | GET (WS) | `/websocket/alert` | Unified alert stream (`UnifiedAlert` JSON) | `UnifiedAlert` fields: `source`, `severity`, `src_ip`, `dst_ip`, `proto`, `timestamp`, `detail` ### Misc — `/misc` | Method | Path | Description | |--------|------|-------------| | GET | `/boot_time` | System boot timestamp | ### WebSocket Protocol All WebSocket endpoints push JSON on data events. Clients should: - Respond to `Ping` frames with `Pong` (handled automatically by most WS clients) - Send `Close` frame to disconnect gracefully ## HTTP Framework axum 0.8 + tower-http. State is passed via `AppState` (Clone) registered with `.with_state()`. New handlers: add `State(state): State` as parameter. Path params use `{param}` syntax. Auth-protected handlers: add `user: AuthenticatedUser` as parameter (auto-rejects invalid tokens). ```rust // Example: auth-protected handler async fn my_handler( user: AuthenticatedUser, State(state): State, ) -> impl IntoResponse { // user.0.role, user.0.username available } ``` ## Implementation Priority See `TODO` for the full backlog.