Mantis/CLAUDE.md
ParrotXray 5e146717d9
Fix/some issus (#14)
* wip

* feat: Add egress eBPF access control and fix Suricata HTTP port detection

* feat: adjust code with rustfmt

* feat: adjust code with rustfmt

* feat: Remove SERVICE stage from ingress eBPF pipeline
2026-05-23 16:35:27 +08:00

256 lines
10 KiB
Markdown

# 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/<domain>.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/<domain>.rs`.
```rust
loggable! {
MLLog {
#[error("ML artifacts loaded - {info}")]
ModelsLoaded { info: String } => tracing::Level::INFO,
}
}
```
## Conventions
- New error variant -> add to `src/model/error/<domain>.rs` using `traceable!`
- New log message -> add to `src/model/log/<domain>.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<Session>` 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://<host>:8080`
All routes that require auth expect `Authorization: Bearer <token>` 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":"<jwt>"}` |
| 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<AppState>` 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<AppState>,
) -> impl IntoResponse {
// user.0.role, user.0.username available
}
```
## Implementation Priority
See `TODO` for the full backlog.