* 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
10 KiB
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):
SKIP_EBPF_BUILD=1 cargo check --package mantis
Ignore these expected errors when SKIP_EBPF_BUILD is set:
environment variable ARTIFACTCS_PATH not definedenvironment variable CSV_RECORD_PATH not definedenvironment variable RULES_DB_PATH not definedcouldn't read .../mantis-ingresscouldn'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.
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.
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.
loggable! {
MLLog {
#[error("ML artifacts loaded - {info}")]
ModelsLoaded { info: String } => tracing::Level::INFO,
}
}
Conventions
- New error variant -> add to
src/model/error/<domain>.rsusingtraceable! - New log message -> add to
src/model/log/<domain>.rsusingloggable! - Never use
println!,eprintln!, ortracing::directly — always uselog!() - 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 modelinference_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
Pingframes withPong(handled automatically by most WS clients) - Send
Closeframe 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).
// 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.