mirror of
https://github.com/ParrotXray/Mantis.git
synced 2026-08-23 16:10:27 +09:00
feat/account-api (#13)
* wip * feat: change ebpf build path * feat: change ebpf build path * feat: add SQLCipher account DB, Argon2 password hashing, and JWT auth * feat: add graceful shutdown on SIGINT * feat: adjust code with rustfmt * docs: edit README.md
This commit is contained in:
parent
46a69dd49c
commit
6eb64d36da
1101
Cargo.lock
generated
1101
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
44
README.md
44
README.md
@ -10,25 +10,39 @@
|
||||
- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection
|
||||
- **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards
|
||||
|
||||
## Functional Modules
|
||||
[//]: # (## Functional Modules)
|
||||
|
||||
### Resource Overview
|
||||

|
||||
- Real-time control system occupancy rate
|
||||
[//]: # ()
|
||||
[//]: # (### Resource Overview)
|
||||
|
||||
### Dashboard Overview
|
||||

|
||||
- Real-time network traffic monitoring and visualization
|
||||
- Recent traffic statistics and trend analysis
|
||||
[//]: # ()
|
||||
|
||||
### Detailed Traffic Statistics
|
||||

|
||||
- Detailed traffic usage information per IP address
|
||||
[//]: # (- Real-time control system occupancy rate)
|
||||
|
||||
### Network Access Control
|
||||

|
||||
- IPv4/IPv6 whitelist and blacklist management
|
||||
- Precise port-level access control
|
||||
[//]: # ()
|
||||
[//]: # (### Dashboard Overview)
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # (- Real-time network traffic monitoring and visualization)
|
||||
|
||||
[//]: # (- Recent traffic statistics and trend analysis)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### Detailed Traffic Statistics)
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # (- Detailed traffic usage information per IP address)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### Network Access Control)
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # (- IPv4/IPv6 whitelist and blacklist management)
|
||||
|
||||
[//]: # (- Precise port-level access control)
|
||||
|
||||
[//]: # (### AI Attack Detection)
|
||||
|
||||
|
||||
1
TODO
1
TODO
@ -4,6 +4,7 @@ Mantis TODO (updated 2026-05-21)
|
||||
|
||||
-- DONE (archived) ----------------------------------------
|
||||
|
||||
|
||||
[x] CSV rolling log with date-based filenames
|
||||
[x] Migrate inference engine to ort-tract (pure Rust)
|
||||
[x] Improve ML inference throughput under high load
|
||||
|
||||
@ -109,7 +109,6 @@ impl Event {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[repr(C, align(8))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IPv4Event {
|
||||
|
||||
@ -40,6 +40,13 @@ ml_cpu = 7
|
||||
|
||||
ae_threshold_method = "94"
|
||||
|
||||
# Auth system. Remove this entire section to disable auth.
|
||||
[Config.auth]
|
||||
jwt_secret = "change-me-jwt-secret-must-be-32-bytes-min"
|
||||
db_key = "change-me-db-key-must-be-32-bytes-min-x"
|
||||
token_ttl_secs = 86400
|
||||
default_admin_password = "admin"
|
||||
|
||||
# Suricata rule engine. Remove this entire section to disable.
|
||||
[Config.suricata]
|
||||
home_net = "140.130.34.0/24"
|
||||
|
||||
1
lib/ebpf/info.txt
Normal file
1
lib/ebpf/info.txt
Normal file
@ -0,0 +1 @@
|
||||
// this's ebpf folder
|
||||
@ -1,7 +1,7 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::{parse_macro_input, Expr, Token};
|
||||
use syn::{Expr, Token, parse_macro_input};
|
||||
|
||||
struct LogInput {
|
||||
error: Expr,
|
||||
@ -62,5 +62,5 @@ pub fn log_impl(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit f1fa091f4c1abc90049b32c36f2d339400090713
|
||||
Subproject commit 66ae9de967702ef02ab47baa9597ab8f16e3ab91
|
||||
@ -7,10 +7,9 @@ edition = "2024"
|
||||
common = { path = "../common", features = ["user"] }
|
||||
macros = { path = "../macros" }
|
||||
|
||||
actix = "0.13.5"
|
||||
actix-cors = "0.7.1"
|
||||
actix-web = "4.11.0"
|
||||
actix-ws = "0.4.0"
|
||||
axum = { version = "0.8", features = ["ws", "macros"] }
|
||||
tower = { version = "0.5", features = ["util"] }
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
aya = { workspace = true }
|
||||
aya-log = { workspace = true }
|
||||
network-types = { workspace = true }
|
||||
@ -25,7 +24,6 @@ serde_json = "1.0.143"
|
||||
sysinfo = "0.38.2"
|
||||
thiserror = "2.0.3"
|
||||
tokio = { version = "1.40.0", features = ["full", "macros"] }
|
||||
tokio-tungstenite = "0.28.0"
|
||||
toml = "1.0.3"
|
||||
tracing = "0.1.41"
|
||||
tracing-appender = "0.2.3"
|
||||
@ -37,6 +35,10 @@ lru = "0.16.2"
|
||||
futures = "0.3.31"
|
||||
tract-onnx = "0.22.1"
|
||||
chrono = "0.4"
|
||||
rusqlite = { version = "0.31", features = ["bundled-sqlcipher-vendored-openssl"] }
|
||||
argon2 = "0.5"
|
||||
jsonwebtoken = "9"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
ort-tract = { version = "0.3.0+0.22", optional = true }
|
||||
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray"] }
|
||||
|
||||
@ -8,12 +8,18 @@ use std::time::SystemTime;
|
||||
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target};
|
||||
|
||||
fn main() {
|
||||
let suricata_eve_socket = PathBuf::from("/").join("tmp").join("suricata-alerts.sock");
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let artifact_dir = manifest_dir.join("static").join("artifacts");
|
||||
let rule_dir = manifest_dir.join("static").join("rules");
|
||||
let suricata_eve_socket = PathBuf::from("/tmp").join("suricata-alerts.sock");
|
||||
let db_dir = manifest_dir.join("static").join("db");
|
||||
let csv_dir = manifest_dir.parent().unwrap().join("records");
|
||||
let onnxruntime_dir = manifest_dir.parent().unwrap().join("onnxruntime").join("libonnxruntime.so");
|
||||
|
||||
let lib_dir = manifest_dir.parent().unwrap().join("lib");
|
||||
let onnxruntime_dir = lib_dir.join("onnxruntime").join("libonnxruntime.so");
|
||||
let ingress_edpf_dir = lib_dir.join("ebpf").join("mantis-ingress");
|
||||
let egress_edpf_dir = lib_dir.join("ebpf").join("mantis-egress");
|
||||
|
||||
let static_web = manifest_dir.join("static").join("web");
|
||||
let project_name = manifest_dir.file_name().unwrap().to_string_lossy().into_owned();
|
||||
@ -25,7 +31,10 @@ fn main() {
|
||||
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());
|
||||
println!("cargo:rustc-env=INGRESS_PATH={}", ingress_edpf_dir.display());
|
||||
println!("cargo:rustc-env=EGRESS_PATH={}", egress_edpf_dir.display());
|
||||
println!("cargo:rustc-env=RULE_PATH={}", rule_dir.display());
|
||||
println!("cargo:rustc-env=DB_PATH={}", db_dir.display());
|
||||
println!("cargo:rustc-env=RULE_EVE_PATH={}", suricata_eve_socket.display());
|
||||
|
||||
for item in &[
|
||||
@ -42,21 +51,19 @@ fn main() {
|
||||
}
|
||||
|
||||
if env::var_os("SKIP_EBPF_BUILD").is_some() {
|
||||
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
for name in &["mantis-ingress", "mantis-egress"] {
|
||||
let path = out.join(name);
|
||||
for path in &[&ingress_edpf_dir, &egress_edpf_dir] {
|
||||
if !path.exists() {
|
||||
fs::write(&path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
|
||||
fs::write(path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
build_ingress_ebpf();
|
||||
build_egress_ebpf();
|
||||
build_ingress_ebpf(&ingress_edpf_dir);
|
||||
build_egress_ebpf(&egress_edpf_dir);
|
||||
build_frontend(&frontend_dir, &static_web);
|
||||
}
|
||||
|
||||
fn build_ingress_ebpf() {
|
||||
fn build_ingress_ebpf(dst: &PathBuf) {
|
||||
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
|
||||
let ebpf_package = packages
|
||||
.into_iter()
|
||||
@ -155,14 +162,12 @@ fn build_ingress_ebpf() {
|
||||
|
||||
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}"));
|
||||
for (_name, binary) in executables {
|
||||
let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_egress_ebpf() {
|
||||
fn build_egress_ebpf(dst: &PathBuf) {
|
||||
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
|
||||
let ebpf_package = packages
|
||||
.into_iter()
|
||||
@ -262,10 +267,8 @@ fn build_egress_ebpf() {
|
||||
|
||||
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}"));
|
||||
for (_name, binary) in executables {
|
||||
let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
mantis/src/core/app_state.rs
Normal file
22
mantis/src/core/app_state.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::ebpf::access_control::AccessControl;
|
||||
use crate::core::ebpf::service::Service;
|
||||
use crate::core::ebpf::statistics::Statistics;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::app_db::AppDB;
|
||||
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::detection::ml::config_loader::InferenceConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub app_config: Arc<AppConfig>,
|
||||
pub inference_config: Arc<InferenceConfig>,
|
||||
pub access_control: Arc<AccessControl>,
|
||||
pub service: Arc<Service>,
|
||||
pub statistics: Arc<Statistics>,
|
||||
pub health: Arc<SystemHealth>,
|
||||
pub detection_alert: Arc<DetectionAlert>,
|
||||
pub app_db: Option<Arc<AppDB>>,
|
||||
}
|
||||
@ -8,8 +8,8 @@ use common::model::ip_address::{IPv4, IPv6, Port};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::model::direction::FlowDirection;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::ip_address::NativeConvert;
|
||||
use crate::model::list_type::ListType;
|
||||
use crate::utils::ip_address::convert_ports_to_vec;
|
||||
|
||||
@ -17,8 +17,8 @@ use crate::core::ebpf::xsk_manager::XskManager;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::detection::ml::engine::Engine;
|
||||
use crate::detection::suricata::SuricataEngine;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::system::SystemError;
|
||||
|
||||
pub struct EbpfServices {
|
||||
pub xsk_manager: Arc<XskManager>,
|
||||
@ -29,11 +29,7 @@ pub struct EbpfServices {
|
||||
}
|
||||
|
||||
impl EbpfServices {
|
||||
pub fn new(
|
||||
app_config: Arc<AppConfig>,
|
||||
ingress_ebpf: &mut Ebpf,
|
||||
egress_ebpf: &mut Ebpf,
|
||||
) -> Result<Self, Error> {
|
||||
pub fn new(app_config: Arc<AppConfig>, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result<Self, Error> {
|
||||
let xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
|
||||
let access_control = AccessControl::new(ingress_ebpf)?;
|
||||
let service = Service::new(ingress_ebpf)?;
|
||||
@ -70,4 +66,4 @@ impl EbpfServices {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,8 +8,8 @@ use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6};
|
||||
use common::model::placeholder::PlaceHolder;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::ip_address::NativeConvert;
|
||||
|
||||
pub struct Service {
|
||||
|
||||
@ -160,11 +160,13 @@ impl Statistics {
|
||||
let map = self
|
||||
.ipv4_maps
|
||||
.get(&(direction, flow_direction, time_type))
|
||||
.ok_or_else(|| EbpfError::FlowMapKeyMissing(
|
||||
format!("{:?}", direction),
|
||||
format!("{:?}", flow_direction),
|
||||
format!("{:?}", time_type),
|
||||
))?;
|
||||
.ok_or_else(|| {
|
||||
EbpfError::FlowMapKeyMissing(
|
||||
format!("{:?}", direction),
|
||||
format!("{:?}", flow_direction),
|
||||
format!("{:?}", time_type),
|
||||
)
|
||||
})?;
|
||||
let flow_data = map.write().await.get_map();
|
||||
|
||||
if let Some(ref geo_ip) = self.geo_ip {
|
||||
@ -197,11 +199,13 @@ impl Statistics {
|
||||
let map = self
|
||||
.ipv6_maps
|
||||
.get(&(direction, flow_direction, time_type))
|
||||
.ok_or_else(|| EbpfError::FlowMapKeyMissing(
|
||||
format!("{:?}", direction),
|
||||
format!("{:?}", flow_direction),
|
||||
format!("{:?}", time_type),
|
||||
))?;
|
||||
.ok_or_else(|| {
|
||||
EbpfError::FlowMapKeyMissing(
|
||||
format!("{:?}", direction),
|
||||
format!("{:?}", flow_direction),
|
||||
format!("{:?}", time_type),
|
||||
)
|
||||
})?;
|
||||
let flow_data = map.write().await.get_map();
|
||||
|
||||
if let Some(ref geo_ip) = self.geo_ip {
|
||||
|
||||
@ -6,9 +6,9 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use aya::maps::{MapData, XskMap};
|
||||
use aya::Ebpf;
|
||||
use crossbeam::channel::{bounded, Receiver, Sender};
|
||||
use aya::maps::{MapData, XskMap};
|
||||
use crossbeam::channel::{Receiver, Sender, bounded};
|
||||
use crossbeam::queue::SegQueue;
|
||||
use macros::log;
|
||||
use parking_lot::Mutex;
|
||||
@ -21,9 +21,9 @@ use crate::detection::ml::engine::Engine;
|
||||
use crate::detection::suricata::SuricataEngine;
|
||||
use crate::model::config::Config;
|
||||
use crate::model::direction::Direction;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::ebpf::EbpfLog;
|
||||
use crate::utils::cpu_affinity::set_cpu_affinity;
|
||||
|
||||
@ -210,7 +210,6 @@ impl XskPair {
|
||||
set_cpu_affinity((start + self.queue_id % num_cores) as usize);
|
||||
}
|
||||
|
||||
|
||||
let mut shutdown_rx = Some(shutdown_rx);
|
||||
let mut idle_count: u32 = 0;
|
||||
let mut last_cleanup = std::time::Instant::now();
|
||||
@ -286,10 +285,7 @@ impl XskPair {
|
||||
Ok(nb_completed)
|
||||
}
|
||||
|
||||
fn process_rx_queue(
|
||||
&mut self,
|
||||
forward_tx: &Sender<Vec<u8>>,
|
||||
) -> Result<usize, EbpfError> {
|
||||
fn process_rx_queue(&mut self, forward_tx: &Sender<Vec<u8>>) -> Result<usize, EbpfError> {
|
||||
let mut rx_descs = vec![FrameDesc::default(); 64];
|
||||
let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
|
||||
|
||||
@ -394,4 +390,4 @@ impl XskPair {
|
||||
|
||||
Ok(nb_submitted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ use std::fs;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::model::config::{Config, ConfigTable};
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::system::SystemError;
|
||||
|
||||
pub struct AppConfig {
|
||||
pub config: Config,
|
||||
|
||||
115
mantis/src/core/infrastructure/app_db.rs
Normal file
115
mantis/src/core/infrastructure/app_db.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use argon2::Argon2;
|
||||
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng};
|
||||
use macros::log;
|
||||
use rusqlite::{Connection, params};
|
||||
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::log::auth::AuthLog;
|
||||
|
||||
pub struct Account {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
pub struct AppDB {
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl AppDB {
|
||||
pub fn open(path: impl AsRef<Path>, key: &str) -> Result<Self, Error> {
|
||||
let path = path.as_ref();
|
||||
|
||||
if let Some(parent) = Path::new(path).parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
}
|
||||
|
||||
let conn = Connection::open(path).map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
|
||||
// Must be the first statement on the connection to unlock the encrypted DB.
|
||||
conn.execute_batch(&format!("PRAGMA key = '{}';", key.replace('\'', "''")))
|
||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
|
||||
conn.execute_batch(
|
||||
"PRAGMA foreign_keys = ON;
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'viewer',
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
account_id TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
|
||||
);",
|
||||
)
|
||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
|
||||
log!(AuthLog::DbInitialized {
|
||||
path: path.to_string_lossy().to_string()
|
||||
});
|
||||
Ok(Self { conn: Mutex::new(conn) })
|
||||
}
|
||||
|
||||
pub fn ensure_default_admin(&self, default_password: &str) -> Result<(), Error> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM accounts", [], |row| row.get(0))
|
||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
|
||||
if count == 0 {
|
||||
let hash = hash_password(default_password)?;
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
conn.execute(
|
||||
"INSERT INTO accounts (id, username, password_hash, role, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params!["admin", "admin", hash, "admin", now],
|
||||
)
|
||||
.map_err(|e| AuthError::DBError { msg: e.to_string() })?;
|
||||
log!(AuthLog::DefaultAdminCreated);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_account_by_username(&self, username: &str) -> Result<Option<Account>, Error> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let result = conn.query_row(
|
||||
"SELECT id, username, password_hash, role FROM accounts WHERE username = ?1",
|
||||
params![username],
|
||||
|row| {
|
||||
Ok(Account {
|
||||
id: row.get(0)?,
|
||||
username: row.get(1)?,
|
||||
password_hash: row.get(2)?,
|
||||
role: row.get(3)?,
|
||||
})
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Ok(account) => Ok(Some(account)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(AuthError::DBError { msg: e.to_string() }.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_password(password: &str) -> Result<String, Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
Argon2::default()
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.map(|h| h.to_string())
|
||||
.map_err(|_| AuthError::HashError.into())
|
||||
}
|
||||
|
||||
pub fn verify_password(password: &str, stored_hash: &str) -> bool {
|
||||
PasswordHash::new(stored_hash)
|
||||
.map(|h| Argon2::default().verify_password(password.as_bytes(), &h).is_ok())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
use std::net::IpAddr;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use maxminddb::{geoip2, MaxMindDbError, Reader};
|
||||
use tokio::sync::{RwLock, Semaphore};
|
||||
use lru::LruCache;
|
||||
use std::num::NonZeroUsize;
|
||||
use maxminddb::{MaxMindDbError, Reader, geoip2};
|
||||
use tokio::sync::{RwLock, Semaphore};
|
||||
use tokio::task;
|
||||
|
||||
use crate::model::geo_stats::GeoLocation;
|
||||
@ -25,13 +25,10 @@ impl GeoIpService {
|
||||
Self::with_cache_size(db_path, 10000)
|
||||
}
|
||||
|
||||
pub fn with_cache_size<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
cache_size: usize,
|
||||
) -> Result<Self, MaxMindDbError> {
|
||||
pub fn with_cache_size<P: AsRef<Path>>(db_path: P, cache_size: usize) -> Result<Self, MaxMindDbError> {
|
||||
let reader = Reader::open_readfile(db_path)?;
|
||||
let cache_capacity = NonZeroUsize::new(cache_size)
|
||||
.unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero"));
|
||||
let cache_capacity =
|
||||
NonZeroUsize::new(cache_size).unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero"));
|
||||
|
||||
Ok(Self {
|
||||
reader: Arc::new(reader),
|
||||
@ -59,7 +56,11 @@ impl GeoIpService {
|
||||
}
|
||||
}
|
||||
|
||||
let permit = self.lookup_sem.clone().acquire_owned().await
|
||||
let permit = self
|
||||
.lookup_sem
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|_| MaxMindDbError::InvalidDatabase {
|
||||
message: "GeoIP semaphore closed".to_string(),
|
||||
offset: None,
|
||||
@ -70,11 +71,11 @@ impl GeoIpService {
|
||||
let _permit = permit;
|
||||
Self::lookup_from_db_blocking(&reader, ip)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| MaxMindDbError::InvalidDatabase {
|
||||
message: format!("Task join error: {}", e),
|
||||
offset: None,
|
||||
})??;
|
||||
.await
|
||||
.map_err(|e| MaxMindDbError::InvalidDatabase {
|
||||
message: format!("Task join error: {}", e),
|
||||
offset: None,
|
||||
})??;
|
||||
|
||||
{
|
||||
let mut cache = self.cache.write().await;
|
||||
@ -84,22 +85,16 @@ impl GeoIpService {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn lookup_from_db_blocking(
|
||||
reader: &Reader<Vec<u8>>,
|
||||
ip: IpAddr,
|
||||
) -> Result<Option<GeoLocation>, MaxMindDbError> {
|
||||
fn lookup_from_db_blocking(reader: &Reader<Vec<u8>>, ip: IpAddr) -> Result<Option<GeoLocation>, MaxMindDbError> {
|
||||
let lookup_result = reader.lookup(ip)?;
|
||||
let city_option: Option<geoip2::City> = lookup_result.decode()?;
|
||||
|
||||
Ok(city_option.map(|city| {
|
||||
let country_name = city.country.names.english
|
||||
.map(|s| s.to_string());
|
||||
let country_name = city.country.names.english.map(|s| s.to_string());
|
||||
|
||||
let country_code = city.country.iso_code
|
||||
.map(|s| s.to_string());
|
||||
let country_code = city.country.iso_code.map(|s| s.to_string());
|
||||
|
||||
let city_name = city.city.names.english
|
||||
.map(|s| s.to_string());
|
||||
let city_name = city.city.names.english.map(|s| s.to_string());
|
||||
|
||||
let latitude = city.location.latitude.or(Some(0.0));
|
||||
let longitude = city.location.longitude.or(Some(0.0));
|
||||
@ -120,4 +115,4 @@ impl GeoIpService {
|
||||
let cache = self.cache.read().await;
|
||||
(cache.len(), cache.cap().get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use sysinfo::{Components, Networks, System};
|
||||
use tokio::sync::{broadcast, oneshot, RwLock};
|
||||
use tokio::time::interval;
|
||||
use macros::log;
|
||||
use sysinfo::{Components, Networks, System};
|
||||
use tokio::sync::{RwLock, broadcast, oneshot};
|
||||
use tokio::time::interval;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::model::log::health::Health;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::health::{
|
||||
ConfiguredNetworkStats,
|
||||
CpuCoreInfo,
|
||||
CpuDetails,
|
||||
LoadAverage,
|
||||
MemoryUsage,
|
||||
NetworkStats,
|
||||
SystemHealthMetrics,
|
||||
SystemHealthStatus,
|
||||
SystemInfo
|
||||
ConfiguredNetworkStats, CpuCoreInfo, CpuDetails, LoadAverage, MemoryUsage, NetworkStats, SystemHealthMetrics,
|
||||
SystemHealthStatus, SystemInfo,
|
||||
};
|
||||
use crate::model::log::health::Health;
|
||||
|
||||
pub struct SystemHealth {
|
||||
system: RwLock<System>,
|
||||
@ -30,7 +23,6 @@ pub struct SystemHealth {
|
||||
egress_interface: String,
|
||||
}
|
||||
|
||||
|
||||
impl SystemHealth {
|
||||
pub fn new(config: Arc<AppConfig>) -> Result<Self, Error> {
|
||||
let (broadcast_tx, _) = broadcast::channel(100);
|
||||
@ -125,11 +117,7 @@ impl SystemHealth {
|
||||
swap_used: system.used_swap(),
|
||||
};
|
||||
|
||||
let network_stats = Self::collect_configured_network_stats(
|
||||
networks,
|
||||
ingress_interface,
|
||||
egress_interface,
|
||||
);
|
||||
let network_stats = Self::collect_configured_network_stats(networks, ingress_interface, egress_interface);
|
||||
|
||||
let load_average = System::load_average();
|
||||
let load_average = if load_average.one != 0.0 || load_average.five != 0.0 || load_average.fifteen != 0.0 {
|
||||
@ -229,16 +217,19 @@ impl SystemHealth {
|
||||
let egress = create_network_stats(egress_interface);
|
||||
|
||||
if ingress.is_none() {
|
||||
log!(Health::InterfaceNotFound("Ingress".to_string(), ingress_interface.to_string()));
|
||||
log!(Health::InterfaceNotFound(
|
||||
"Ingress".to_string(),
|
||||
ingress_interface.to_string()
|
||||
));
|
||||
}
|
||||
if egress.is_none() {
|
||||
log!(Health::InterfaceNotFound("Egress".to_string(), egress_interface.to_string()));
|
||||
log!(Health::InterfaceNotFound(
|
||||
"Egress".to_string(),
|
||||
egress_interface.to_string()
|
||||
));
|
||||
}
|
||||
|
||||
ConfiguredNetworkStats {
|
||||
ingress,
|
||||
egress,
|
||||
}
|
||||
ConfiguredNetworkStats { ingress, egress }
|
||||
}
|
||||
|
||||
pub async fn get_current_metrics(&self) -> SystemHealthMetrics {
|
||||
@ -290,22 +281,17 @@ impl SystemHealth {
|
||||
metrics.memory_usage.usage_percent
|
||||
));
|
||||
} else if metrics.memory_usage.usage_percent > 80.0 {
|
||||
status.warnings.push(format!(
|
||||
"High memory usage: {:.1}%",
|
||||
metrics.memory_usage.usage_percent
|
||||
));
|
||||
status
|
||||
.warnings
|
||||
.push(format!("High memory usage: {:.1}%", metrics.memory_usage.usage_percent));
|
||||
}
|
||||
|
||||
if let Some(temp) = metrics.temperature {
|
||||
if temp > 80.0 {
|
||||
status.overall_healthy = false;
|
||||
status
|
||||
.issues
|
||||
.push(format!("High CPU temperature: {:.1}°C", temp));
|
||||
status.issues.push(format!("High CPU temperature: {:.1}°C", temp));
|
||||
} else if temp > 70.0 {
|
||||
status
|
||||
.warnings
|
||||
.push(format!("Elevated CPU temperature: {:.1}°C", temp));
|
||||
status.warnings.push(format!("Elevated CPU temperature: {:.1}°C", temp));
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,4 +306,4 @@ impl SystemHealth {
|
||||
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
pub mod app_config;
|
||||
pub mod app_db;
|
||||
pub mod detection_alert;
|
||||
pub mod health;
|
||||
pub mod geoip;
|
||||
pub mod health;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@ -13,6 +14,7 @@ use macros::log;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::app_db::AppDB;
|
||||
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::detection::fusion::{FusionEngine, FusionMode};
|
||||
@ -22,8 +24,8 @@ use crate::detection::ml::feature_extractor::FlowFeatures;
|
||||
use crate::detection::ml::model_loader::MLModels;
|
||||
use crate::detection::ml::traffic_logger::TrafficLogger;
|
||||
use crate::detection::suricata::SuricataEngine;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::system::SystemError;
|
||||
use crate::model::log::system::SystemLog;
|
||||
|
||||
@ -34,6 +36,7 @@ pub struct AppServices {
|
||||
pub ml_models: Arc<MLModels>,
|
||||
pub ml_engine: Arc<Engine>,
|
||||
pub suricata_engine: Option<Arc<SuricataEngine>>,
|
||||
pub app_db: Option<Arc<AppDB>>,
|
||||
shutdowns: SegQueue<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
@ -53,12 +56,21 @@ impl AppServices {
|
||||
let traffic_logger = if app_config.traffic_logging_mode {
|
||||
let dir = PathBuf::from(env!("CSV_RECORD_PATH"));
|
||||
let basename = app_config.traffic_log_csv_path.trim_end_matches(".csv").to_string();
|
||||
let mut header = vec!["Source IP".to_string(), "Destination IP".to_string(), "Timestamp".to_string()];
|
||||
let mut header = vec![
|
||||
"Source IP".to_string(),
|
||||
"Destination IP".to_string(),
|
||||
"Timestamp".to_string(),
|
||||
];
|
||||
header.extend(FlowFeatures::all_feature_names_owned());
|
||||
header.push("Label".to_string());
|
||||
let logger = TrafficLogger::new(&dir, &basename, header)
|
||||
.map_err(|e| MiscError::TrafficLogCreateError(dir.display().to_string(), e.to_string()))?;
|
||||
log!(SystemLog::TrafficLoggingEnabled(format!("{}/{}-{}.csv", dir.display(), basename, Local::now().format("%Y-%m-%d"))));
|
||||
log!(SystemLog::TrafficLoggingEnabled(format!(
|
||||
"{}/{}-{}.csv",
|
||||
dir.display(),
|
||||
basename,
|
||||
Local::now().format("%Y-%m-%d")
|
||||
)));
|
||||
Some(Arc::new(logger))
|
||||
} else {
|
||||
None
|
||||
@ -80,7 +92,21 @@ impl AppServices {
|
||||
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())?)
|
||||
Some(SuricataEngine::start(
|
||||
sc,
|
||||
&rule_path,
|
||||
&eve_socket,
|
||||
fusion_engine.clone(),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let app_db = if let Some(ref auth) = app_config.auth {
|
||||
let db_path = PathBuf::from(env!("DB_PATH")).join("app.db");
|
||||
let db = AppDB::open(db_path, &auth.db_key)?;
|
||||
db.ensure_default_admin(&auth.default_admin_password)?;
|
||||
Some(Arc::new(db))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@ -92,6 +118,7 @@ impl AppServices {
|
||||
ml_models,
|
||||
ml_engine,
|
||||
suricata_engine,
|
||||
app_db,
|
||||
shutdowns: SegQueue::new(),
|
||||
})
|
||||
}
|
||||
@ -116,4 +143,4 @@ impl AppServices {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod app_state;
|
||||
pub mod ebpf;
|
||||
pub mod infrastructure;
|
||||
pub mod system;
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::route;
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use axum::Router;
|
||||
use aya::Ebpf;
|
||||
use aya::maps::{MapData, ProgramArray};
|
||||
use aya::programs::{Xdp, XdpFlags};
|
||||
use aya::Ebpf;
|
||||
use aya_log::EbpfLogger;
|
||||
use common::define::program_array::*;
|
||||
use macros::log;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::CorsLayer;
|
||||
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::core::ebpf::EbpfServices;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::infrastructure::AppServices;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::detection::ml::config_loader::InferenceConfig;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::log::ml::MLLog;
|
||||
use crate::model::log::system::SystemLog;
|
||||
use crate::utils::logging::Logging;
|
||||
use crate::web::api::{control, default, detection_alert, health, misc};
|
||||
use crate::web::api::default::default_route;
|
||||
use crate::web::api::{auth, control, detection_alert, health, misc};
|
||||
|
||||
pub struct System {
|
||||
pub app_config: Arc<AppConfig>,
|
||||
@ -41,7 +44,10 @@ impl System {
|
||||
let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?;
|
||||
let app_config = Arc::new(AppConfig::new()?);
|
||||
|
||||
let inference_config = Arc::new(InferenceConfig::load_file(&app_config.models_config_name, &app_config.ae_threshold_method)?);
|
||||
let inference_config = Arc::new(InferenceConfig::load_file(
|
||||
&app_config.models_config_name,
|
||||
&app_config.ae_threshold_method,
|
||||
)?);
|
||||
|
||||
let ebpf_services = Arc::new(EbpfServices::new(
|
||||
app_config.clone(),
|
||||
@ -82,7 +88,9 @@ impl System {
|
||||
log!(SystemLog::InitializeComplete);
|
||||
self.attach_ebpf()?;
|
||||
|
||||
ebpf_services.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone()).await?;
|
||||
ebpf_services
|
||||
.run(app_services.ml_engine.clone(), app_services.suricata_engine.clone())
|
||||
.await?;
|
||||
app_services.run().await?;
|
||||
self.run_http_server().await?;
|
||||
Ok(())
|
||||
@ -134,49 +142,45 @@ impl System {
|
||||
}
|
||||
|
||||
async fn run_http_server(&self) -> Result<(), Error> {
|
||||
let app_config = self.app_config.clone();
|
||||
let inference_config = self.inference_config.clone();
|
||||
let access_control = self.ebpf_services.access_control.clone();
|
||||
let service = self.ebpf_services.service.clone();
|
||||
let statistics = self.ebpf_services.statistics.clone();
|
||||
let health = self.app_services.health.clone();
|
||||
let detection_alert = self.app_services.detection_alert.clone();
|
||||
let state = AppState {
|
||||
app_config: self.app_config.clone(),
|
||||
inference_config: self.inference_config.clone(),
|
||||
access_control: self.ebpf_services.access_control.clone(),
|
||||
service: self.ebpf_services.service.clone(),
|
||||
statistics: self.ebpf_services.statistics.clone(),
|
||||
health: self.app_services.health.clone(),
|
||||
detection_alert: self.app_services.detection_alert.clone(),
|
||||
app_db: self.app_services.app_db.clone(),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/ebpf", control::router())
|
||||
.nest("/detection", detection_alert::router())
|
||||
.nest("/health", health::router())
|
||||
.nest("/misc", misc::router())
|
||||
.nest("/auth", auth::router())
|
||||
.fallback(default_route)
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
let port = self.app_config.http_server_bind_port;
|
||||
HttpServer::new(move || {
|
||||
let cors = actix_cors::Cors::default()
|
||||
.allow_any_origin()
|
||||
.allow_any_method()
|
||||
.allow_any_header()
|
||||
.max_age(3600);
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.app_data(web::Data::from(app_config.clone()))
|
||||
.app_data(web::Data::from(inference_config.clone()))
|
||||
.app_data(web::Data::from(access_control.clone()))
|
||||
.app_data(web::Data::from(service.clone()))
|
||||
.app_data(web::Data::from(statistics.clone()))
|
||||
.app_data(web::Data::from(health.clone()))
|
||||
.app_data(web::Data::from(detection_alert.clone()))
|
||||
.service(control::initialize())
|
||||
.service(detection_alert::initialize())
|
||||
.service(health::initialize())
|
||||
.service(misc::initialize())
|
||||
.default_service(route().to(default::default_route))
|
||||
})
|
||||
.bind(format!("0.0.0.0:{}", port))
|
||||
.map_err(HttpError::BindPortError)?
|
||||
.run()
|
||||
.await
|
||||
.map_err(HttpError::ServerPanic)?;
|
||||
let listener = TcpListener::bind(format!("0.0.0.0:{}", port))
|
||||
.await
|
||||
.map_err(HttpError::BindPortError)?;
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(async {
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
})
|
||||
.await
|
||||
.map_err(HttpError::ServerPanic)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
|
||||
let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/mantis-ingress"
|
||||
)))
|
||||
.map_err(EbpfError::EbpfNotFound)?;
|
||||
let mut ingress_ebpf =
|
||||
Ebpf::load(aya::include_bytes_aligned!(env!("INGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;
|
||||
let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?;
|
||||
Self::load_program(
|
||||
@ -197,11 +201,8 @@ impl System {
|
||||
}
|
||||
|
||||
fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
|
||||
let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/mantis-egress"
|
||||
)))
|
||||
.map_err(EbpfError::EbpfNotFound)?;
|
||||
let mut egress_ebpf =
|
||||
Ebpf::load(aya::include_bytes_aligned!(env!("EGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;
|
||||
let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?;
|
||||
let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?;
|
||||
Self::load_program(&mut egress_ebpf, &mut program_array, "statistics", egress::STATISTICS)?;
|
||||
|
||||
@ -14,7 +14,11 @@ pub enum FusionMode {
|
||||
|
||||
impl FusionMode {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
if s.eq_ignore_ascii_case("and") { FusionMode::And } else { FusionMode::Or }
|
||||
if s.eq_ignore_ascii_case("and") {
|
||||
FusionMode::And
|
||||
} else {
|
||||
FusionMode::Or
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +64,14 @@ impl FusionEngine {
|
||||
return;
|
||||
}
|
||||
self.alert.broadcast(UnifiedAlert::from_ml(result));
|
||||
map.insert(key, FusionState { ml: Some(result.clone()), rule: None, created_at: now });
|
||||
map.insert(
|
||||
key,
|
||||
FusionState {
|
||||
ml: Some(result.clone()),
|
||||
rule: None,
|
||||
created_at: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
FusionMode::And => {
|
||||
let maybe_rule = {
|
||||
@ -100,7 +111,14 @@ impl FusionEngine {
|
||||
return;
|
||||
}
|
||||
self.alert.broadcast(UnifiedAlert::from_rule(m));
|
||||
map.insert(key, FusionState { ml: None, rule: Some(m.clone()), created_at: now });
|
||||
map.insert(
|
||||
key,
|
||||
FusionState {
|
||||
ml: None,
|
||||
rule: Some(m.clone()),
|
||||
created_at: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
FusionMode::And => {
|
||||
let maybe_ml = {
|
||||
|
||||
@ -5,7 +5,7 @@ use crate::model::ml_detection::FlowKey;
|
||||
|
||||
// L2 thresholds: anomalous flows per src_ip within the aggregation window
|
||||
const FLOOD_THRESHOLD: usize = 10; // total anomalous flows -> FLOOD
|
||||
const SCAN_THRESHOLD: usize = 5; // distinct dst_ports -> SCAN
|
||||
const SCAN_THRESHOLD: usize = 5; // distinct dst_ports -> SCAN
|
||||
|
||||
struct SrcIpState {
|
||||
events: Vec<(Instant, u16)>, // (time, dst_port)
|
||||
@ -39,8 +39,7 @@ impl AttackAggregator {
|
||||
detections.push((now, score));
|
||||
|
||||
if detections.len() >= self.min_detections {
|
||||
let avg_score: f32 =
|
||||
detections.iter().map(|(_, s)| s).sum::<f32>() / detections.len() as f32;
|
||||
let avg_score: f32 = detections.iter().map(|(_, s)| s).sum::<f32>() / detections.len() as f32;
|
||||
|
||||
return avg_score > threshold * self.alert_threshold_multiplier;
|
||||
}
|
||||
@ -52,10 +51,13 @@ impl AttackAggregator {
|
||||
let now = Instant::now();
|
||||
let window = self.window_duration;
|
||||
|
||||
let state = self.src_ip_states.entry(src_ip.to_string()).or_insert_with(|| SrcIpState {
|
||||
events: Vec::new(),
|
||||
last_alert: None,
|
||||
});
|
||||
let state = self
|
||||
.src_ip_states
|
||||
.entry(src_ip.to_string())
|
||||
.or_insert_with(|| SrcIpState {
|
||||
events: Vec::new(),
|
||||
last_alert: None,
|
||||
});
|
||||
|
||||
state.events.retain(|(t, _)| now.duration_since(*t) < window);
|
||||
state.events.push((now, dst_port));
|
||||
@ -89,7 +91,9 @@ impl AttackAggregator {
|
||||
!detections.is_empty()
|
||||
});
|
||||
self.src_ip_states.retain(|_, state| {
|
||||
state.events.retain(|(t, _)| now.duration_since(*t) < self.window_duration);
|
||||
state
|
||||
.events
|
||||
.retain(|(t, _)| now.duration_since(*t) < self.window_duration);
|
||||
!state.events.is_empty()
|
||||
});
|
||||
}
|
||||
@ -97,4 +101,4 @@ impl AttackAggregator {
|
||||
pub fn tracked_flows(&self) -> usize {
|
||||
self.detections.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use macros::log;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::ml_detection::ClipParams;
|
||||
|
||||
@ -25,15 +26,18 @@ pub struct InferenceConfig {
|
||||
impl InferenceConfig {
|
||||
pub fn load_file(file: &str, method: &str) -> Result<Self, MLError> {
|
||||
let path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(file);
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?;
|
||||
let mut config: InferenceConfig = serde_json::from_str(&content)
|
||||
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
||||
let content = fs::read_to_string(&path).map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?;
|
||||
let mut config: InferenceConfig =
|
||||
serde_json::from_str(&content).map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
|
||||
|
||||
// Validate the method exists at startup — fail fast rather than at inference time.
|
||||
if !config.ae_thresholds.contains_key(method) {
|
||||
log!(MLError::ThresholdMethodNotFound { method: method.to_string() });
|
||||
return Err(MLError::ThresholdMethodNotFound { method: method.to_string() });
|
||||
log!(MLError::ThresholdMethodNotFound {
|
||||
method: method.to_string()
|
||||
});
|
||||
return Err(MLError::ThresholdMethodNotFound {
|
||||
method: method.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
config.ae_threshold_method = method.to_string();
|
||||
@ -43,4 +47,4 @@ impl InferenceConfig {
|
||||
pub fn num_ae_features(&self) -> usize {
|
||||
self.ae_feature_names.len()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ use super::flow_tracker::FlowTracker;
|
||||
use super::inference::Inference;
|
||||
use super::model_loader::MLModels;
|
||||
use super::traffic_logger::TrafficLogger;
|
||||
|
||||
use crate::detection::fusion::FusionEngine;
|
||||
use crate::model::error::ml::MLError;
|
||||
use crate::model::log::ml::MLLog;
|
||||
@ -81,9 +80,9 @@ impl Engine {
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut shutdown_rx => break,
|
||||
_ = ticker.tick() => {}
|
||||
}
|
||||
_ = &mut shutdown_rx => break,
|
||||
_ = ticker.tick() => {}
|
||||
}
|
||||
|
||||
let (total_flows, flows, active_ips) = {
|
||||
let Ok(mut t) = self.tracker.lock() else {
|
||||
@ -97,10 +96,8 @@ impl Engine {
|
||||
// (which is always empty). This preserves per-src_ip inference buffers
|
||||
// across consecutive cycles so the LSTM window can fill up over time.
|
||||
// A src_ip absent from this cycle loses its buffer on the next cleanup.
|
||||
let active_ips: std::collections::HashSet<String> = flows
|
||||
.iter()
|
||||
.map(|f| f.flow_key.src_ip.clone())
|
||||
.collect();
|
||||
let active_ips: std::collections::HashSet<String> =
|
||||
flows.iter().map(|f| f.flow_key.src_ip.clone()).collect();
|
||||
(total_flows, flows, active_ips)
|
||||
};
|
||||
|
||||
@ -122,7 +119,9 @@ impl Engine {
|
||||
|
||||
let mut batch = flows[..flows.len().min(self.batch_size)].to_vec();
|
||||
batch.sort_by(|a, b| {
|
||||
a.flow_key.src_ip.cmp(&b.flow_key.src_ip)
|
||||
a.flow_key
|
||||
.src_ip
|
||||
.cmp(&b.flow_key.src_ip)
|
||||
.then_with(|| a.start_time_us.cmp(&b.start_time_us))
|
||||
});
|
||||
|
||||
@ -131,7 +130,9 @@ impl Engine {
|
||||
let pipeline = Arc::clone(&self.inference_pipeline);
|
||||
let ml_cpu = self.ml_cpu;
|
||||
let mut handle = tokio::task::spawn_blocking(move || {
|
||||
let cpu = ml_cpu.map(|c| c as usize).unwrap_or_else(|| num_cpus().saturating_sub(1));
|
||||
let cpu = ml_cpu
|
||||
.map(|c| c as usize)
|
||||
.unwrap_or_else(|| num_cpus().saturating_sub(1));
|
||||
set_cpu_affinity(cpu);
|
||||
pipeline.infer_batch(&batch)
|
||||
});
|
||||
@ -166,11 +167,8 @@ impl Engine {
|
||||
for result in &results {
|
||||
if result.is_attack {
|
||||
// L1: full 5-tuple aggregation for persistent same-port attacks
|
||||
let should_alert = aggregator.should_alert(
|
||||
&result.flow_key_raw,
|
||||
result.ae_score,
|
||||
result.threshold,
|
||||
);
|
||||
let should_alert =
|
||||
aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold);
|
||||
if should_alert {
|
||||
log!(MLLog::ThreatDetected(
|
||||
format!("{:?}", result.direction),
|
||||
@ -185,10 +183,9 @@ impl Engine {
|
||||
|
||||
// L2: src_ip-level scan/flood detection (skipped if L1 already fired)
|
||||
if !alerted_src_ips.contains(&result.flow_key_raw.src_ip) {
|
||||
if let Some(attack_type) = aggregator.should_alert_src_ip(
|
||||
&result.flow_key_raw.src_ip,
|
||||
result.flow_key_raw.dst_port,
|
||||
) {
|
||||
if let Some(attack_type) = aggregator
|
||||
.should_alert_src_ip(&result.flow_key_raw.src_ip, result.flow_key_raw.dst_port)
|
||||
{
|
||||
let mut l2_result = result.clone();
|
||||
l2_result.attack_type = Some(attack_type.to_string());
|
||||
log!(MLLog::ThreatDetected(
|
||||
@ -235,4 +232,4 @@ impl Engine {
|
||||
};
|
||||
EngineStats { active_flows }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use chrono::{Utc, TimeZone};
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
use super::flow_tracker::FlowData;
|
||||
use crate::model::ml_detection::{ClipParams, PacketData};
|
||||
@ -67,7 +68,9 @@ impl FlowFeatures {
|
||||
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
|
||||
|
||||
let all_lengths: Vec<f64> = flow
|
||||
.fwd_packets.iter().chain(flow.bwd_packets.iter())
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.chain(flow.bwd_packets.iter())
|
||||
.map(|p| p.payload_length as f64)
|
||||
.collect();
|
||||
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths);
|
||||
@ -75,11 +78,7 @@ impl FlowFeatures {
|
||||
let fwd_bulk = &flow.fwd_bulk_state;
|
||||
let bwd_bulk = &flow.bwd_bulk_state;
|
||||
|
||||
let fwd_header_sizes: Vec<f64> = flow
|
||||
.fwd_packets
|
||||
.iter()
|
||||
.map(|p| p.header_length as f64)
|
||||
.collect();
|
||||
let fwd_header_sizes: Vec<f64> = flow.fwd_packets.iter().map(|p| p.header_length as f64).collect();
|
||||
|
||||
let (active_max, active_min, active_mean, active_std) =
|
||||
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
|
||||
@ -145,14 +144,22 @@ impl FlowFeatures {
|
||||
"Avg Fwd Segment Size" | "avg_fwd_seg_size" => safe_div(flow.fwd_total_bytes as f64, fwd_count),
|
||||
"Avg Bwd Segment Size" | "avg_bwd_seg_size" => safe_div(flow.bwd_total_bytes as f64, bwd_count),
|
||||
"Fwd Header Length.1" | "fwd_header_length_1" => flow.fwd_header_bytes as f64,
|
||||
"Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64),
|
||||
"Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64),
|
||||
"Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => {
|
||||
safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64)
|
||||
}
|
||||
"Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => {
|
||||
safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64)
|
||||
}
|
||||
"Fwd Avg Bulk Rate" | "fwd_avg_bulk_rate" => safe_div(
|
||||
fwd_bulk.total_bytes as f64,
|
||||
fwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
),
|
||||
"Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64),
|
||||
"Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64),
|
||||
"Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => {
|
||||
safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64)
|
||||
}
|
||||
"Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => {
|
||||
safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64)
|
||||
}
|
||||
"Bwd Avg Bulk Rate" | "bwd_avg_bulk_rate" => safe_div(
|
||||
bwd_bulk.total_bytes as f64,
|
||||
bwd_bulk.total_duration_us as f64 / 1_000_000.0,
|
||||
@ -358,4 +365,4 @@ fn compute_flow_iats(fwd_packets: &[PacketData], bwd_packets: &[PacketData]) ->
|
||||
.windows(2)
|
||||
.map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,13 +103,27 @@ impl FlowData {
|
||||
self.bwd_fin_seen = true;
|
||||
}
|
||||
}
|
||||
if packet.tcp_flags().syn { self.syn_count += 1; }
|
||||
if packet.tcp_flags().rst { self.rst_count += 1; }
|
||||
if packet.tcp_flags().psh { self.psh_count += 1; }
|
||||
if packet.tcp_flags().ack { self.ack_count += 1; }
|
||||
if packet.tcp_flags().urg { self.urg_count += 1; }
|
||||
if packet.tcp_flags().cwr { self.cwe_count += 1; }
|
||||
if packet.tcp_flags().ece { self.ece_count += 1; }
|
||||
if packet.tcp_flags().syn {
|
||||
self.syn_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().rst {
|
||||
self.rst_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().psh {
|
||||
self.psh_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().ack {
|
||||
self.ack_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().urg {
|
||||
self.urg_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().cwr {
|
||||
self.cwe_count += 1;
|
||||
}
|
||||
if packet.tcp_flags().ece {
|
||||
self.ece_count += 1;
|
||||
}
|
||||
|
||||
let iat = packet.timestamp_us().saturating_sub(self.last_packet_time);
|
||||
const IDLE_THRESHOLD_US: u64 = 1_000_000;
|
||||
@ -166,9 +180,7 @@ impl FlowData {
|
||||
|
||||
if packet.payload_length > 0 {
|
||||
// Idle break: discard helper, start fresh
|
||||
if bulk_state.in_bulk
|
||||
&& packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US
|
||||
{
|
||||
if bulk_state.in_bulk && packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US {
|
||||
bulk_state.in_bulk = false;
|
||||
bulk_state.last_bulk_bytes = 0;
|
||||
bulk_state.last_bulk_packets = 0;
|
||||
@ -193,14 +205,12 @@ impl FlowData {
|
||||
bulk_state.bulk_count += 1;
|
||||
bulk_state.total_packets += 4;
|
||||
bulk_state.total_bytes += bulk_state.last_bulk_bytes;
|
||||
bulk_state.total_duration_us += packet.timestamp_us
|
||||
.saturating_sub(bulk_state.last_bulk_start_us);
|
||||
bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(bulk_state.last_bulk_start_us);
|
||||
} else if bulk_state.last_bulk_packets > 4 {
|
||||
// Each subsequent packet adds incrementally
|
||||
bulk_state.total_packets += 1;
|
||||
bulk_state.total_bytes += packet.length as u64;
|
||||
bulk_state.total_duration_us += packet.timestamp_us
|
||||
.saturating_sub(prev_us);
|
||||
bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(prev_us);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -279,7 +289,8 @@ impl FlowTracker {
|
||||
|
||||
let initiator_direction = if is_forward { direction } else { direction.flip() };
|
||||
|
||||
let flow = self.flows
|
||||
let flow = self
|
||||
.flows
|
||||
.entry(actual_key.clone())
|
||||
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
|
||||
|
||||
@ -305,10 +316,8 @@ impl FlowTracker {
|
||||
.duration_since(time::UNIX_EPOCH)
|
||||
.map(|d| d.as_micros() as u64)
|
||||
.unwrap_or(0);
|
||||
self.flows.retain(|_, flow| {
|
||||
!flow.is_finished()
|
||||
&& now.saturating_sub(flow.last_time_us) < max_age_us
|
||||
});
|
||||
self.flows
|
||||
.retain(|_, flow| !flow.is_finished() && now.saturating_sub(flow.last_time_us) < max_age_us);
|
||||
}
|
||||
|
||||
pub fn flow_count(&self) -> usize {
|
||||
@ -357,4 +366,4 @@ fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16)
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
@ -90,10 +90,7 @@ impl Inference {
|
||||
}
|
||||
|
||||
let t2 = Instant::now();
|
||||
let ae_input = Array3::from_shape_fn(
|
||||
(1, window_size, feat_len),
|
||||
|(_, t, f)| sequence[t][f],
|
||||
);
|
||||
let ae_input = Array3::from_shape_fn((1, window_size, feat_len), |(_, t, f)| sequence[t][f]);
|
||||
|
||||
let t3 = Instant::now();
|
||||
let ae_score = match self.run_autoencoder(&ae_input) {
|
||||
@ -114,14 +111,19 @@ impl Inference {
|
||||
));
|
||||
|
||||
let pad = window_size - buf_len_snapshot;
|
||||
let rows = sequence.iter().enumerate().map(|(t, row)| {
|
||||
if t < pad {
|
||||
format!(" t{t:02}: [pad]")
|
||||
} else {
|
||||
let vals = row.iter().map(|v| format!("{v:7.3}")).collect::<Vec<_>>().join(" ");
|
||||
format!(" t{t:02}: [{vals}]")
|
||||
}
|
||||
}).collect::<Vec<_>>().join("\n");
|
||||
let rows = sequence
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(t, row)| {
|
||||
if t < pad {
|
||||
format!(" t{t:02}: [pad]")
|
||||
} else {
|
||||
let vals = row.iter().map(|v| format!("{v:7.3}")).collect::<Vec<_>>().join(" ");
|
||||
format!(" t{t:02}: [{vals}]")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
log!(MLLog::WindowDebug(
|
||||
flow.flow_key.src_ip.clone(),
|
||||
pad,
|
||||
@ -180,4 +182,4 @@ impl Inference {
|
||||
|
||||
Ok(mse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
pub mod model_loader;
|
||||
pub mod config_loader;
|
||||
pub mod flow_tracker;
|
||||
pub mod feature_extractor;
|
||||
pub mod inference;
|
||||
pub mod engine;
|
||||
pub mod aggregator;
|
||||
pub mod config_loader;
|
||||
pub mod engine;
|
||||
pub mod feature_extractor;
|
||||
pub mod flow_tracker;
|
||||
pub mod inference;
|
||||
pub mod model_loader;
|
||||
pub mod traffic_logger;
|
||||
|
||||
@ -18,7 +18,8 @@ impl MLModels {
|
||||
{
|
||||
log!(MLLog::BackendNativeOrt);
|
||||
ort::init_from(PathBuf::from(env!("ONNXRUNTIME_PATH")))
|
||||
.map_err(|_| MLError::InitializeFailed)?.commit();
|
||||
.map_err(|_| MLError::InitializeFailed)?
|
||||
.commit();
|
||||
}
|
||||
#[cfg(feature = "tract-backend")]
|
||||
{
|
||||
@ -35,9 +36,13 @@ impl MLModels {
|
||||
let model_path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(model_name);
|
||||
|
||||
Session::builder()
|
||||
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?
|
||||
.map_err(|_| MLError::ModelLoadFailed {
|
||||
path: model_path.clone(),
|
||||
})?
|
||||
.with_optimization_level(GraphOptimizationLevel::All)
|
||||
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?
|
||||
.map_err(|_| MLError::ModelLoadFailed {
|
||||
path: model_path.clone(),
|
||||
})?
|
||||
.commit_from_file(&model_path)
|
||||
.map_err(|_| MLError::ModelLoadFailed { path: model_path })
|
||||
}
|
||||
@ -55,4 +60,4 @@ impl MLModels {
|
||||
_ => "unknown model".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::{PathBuf, Path};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Local;
|
||||
use crossbeam::channel::{bounded, Sender, TrySendError, RecvTimeoutError};
|
||||
use crossbeam::channel::{RecvTimeoutError, Sender, TrySendError, bounded};
|
||||
use macros::log;
|
||||
|
||||
use crate::model::error::ml::MLError;
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
pub mod fusion;
|
||||
pub mod ml;
|
||||
pub mod suricata;
|
||||
pub mod suricata;
|
||||
|
||||
@ -6,16 +6,15 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossbeam::channel::{bounded, Sender};
|
||||
use crossbeam::channel::{Sender, bounded};
|
||||
use macros::log;
|
||||
|
||||
use super::output;
|
||||
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";
|
||||
@ -42,10 +41,12 @@ impl SuricataEngine {
|
||||
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 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)?;
|
||||
@ -73,7 +74,9 @@ impl SuricataEngine {
|
||||
.spawn(|| {
|
||||
let log_path = Path::new(SURICATA_LOG);
|
||||
for _ in 0..100 {
|
||||
if log_path.exists() { break; }
|
||||
if log_path.exists() {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
let file = match std::fs::File::open(log_path) {
|
||||
@ -96,17 +99,20 @@ impl SuricataEngine {
|
||||
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; }
|
||||
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; }
|
||||
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 });
|
||||
@ -144,7 +150,9 @@ impl SuricataEngine {
|
||||
sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be();
|
||||
sll.sll_ifindex = ifindex as i32;
|
||||
|
||||
log!(SuricataLog::MirrorReady { iface: MIRROR_IFACE.into() });
|
||||
log!(SuricataLog::MirrorReady {
|
||||
iface: MIRROR_IFACE.into()
|
||||
});
|
||||
|
||||
while let Ok(data) = rx.recv() {
|
||||
unsafe {
|
||||
@ -166,7 +174,10 @@ impl SuricataEngine {
|
||||
|
||||
log!(SuricataLog::Initialized);
|
||||
|
||||
Ok(Arc::new(Self { tx, child: std::sync::Mutex::new(child) }))
|
||||
Ok(Arc::new(Self {
|
||||
tx,
|
||||
child: std::sync::Mutex::new(child),
|
||||
}))
|
||||
}
|
||||
|
||||
/* Non-blocking: drops silently when the channel is full under load. */
|
||||
@ -187,25 +198,31 @@ impl SuricataEngine {
|
||||
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(),
|
||||
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:
|
||||
Some([start, end]) => format!(
|
||||
r#" - worker-cpu-set:
|
||||
cpu: [ "{start}-{end}" ]
|
||||
mode: "balanced""#),
|
||||
mode: "balanced""#
|
||||
),
|
||||
None => String::new(),
|
||||
};
|
||||
format!(r#"threading:
|
||||
format!(
|
||||
r#"threading:
|
||||
set-cpu-affinity: yes
|
||||
cpu-affinity:
|
||||
- management-cpu-set:
|
||||
cpu: [ {mgmt_cpu} ]
|
||||
{worker_block}"#)
|
||||
{worker_block}"#
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
format!(r#"%YAML 1.1
|
||||
format!(
|
||||
r#"%YAML 1.1
|
||||
---
|
||||
vars:
|
||||
address-groups:
|
||||
@ -298,23 +315,21 @@ legacy:
|
||||
|
||||
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,
|
||||
home_net = config.home_net,
|
||||
rule_path = rule_path,
|
||||
eve_socket = eve_socket,
|
||||
suppress_path = suppress_path,
|
||||
log_path = SURICATA_LOG,
|
||||
iface = MIRROR_PEER,
|
||||
threads = config.af_packet_threads,
|
||||
ring_size = config.af_packet_ring_size,
|
||||
block_size = config.af_packet_block_size,
|
||||
threading = threading,
|
||||
)
|
||||
}
|
||||
|
||||
fn yaml_to_memfd(yaml: &str) -> Result<i32, SuricataError> {
|
||||
let fd = unsafe {
|
||||
libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0)
|
||||
};
|
||||
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 {
|
||||
@ -322,9 +337,7 @@ host-mode: sniffer-only
|
||||
});
|
||||
}
|
||||
let bytes = yaml.as_bytes();
|
||||
let written = unsafe {
|
||||
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len())
|
||||
};
|
||||
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() };
|
||||
@ -351,7 +364,10 @@ host-mode: sniffer-only
|
||||
.map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?;
|
||||
}
|
||||
|
||||
log!(SuricataLog::VethCreated { iface: MIRROR_IFACE.into(), peer: MIRROR_PEER.into() });
|
||||
log!(SuricataLog::VethCreated {
|
||||
iface: MIRROR_IFACE.into(),
|
||||
peer: MIRROR_PEER.into()
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -366,13 +382,7 @@ host-mode: sniffer-only
|
||||
}
|
||||
|
||||
fn open_raw_socket() -> Result<i32, SuricataError> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_PACKET,
|
||||
libc::SOCK_RAW,
|
||||
(libc::ETH_P_ALL as u16).to_be() as i32,
|
||||
)
|
||||
};
|
||||
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 {
|
||||
@ -390,4 +400,4 @@ impl Drop for SuricataEngine {
|
||||
}
|
||||
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
mod output;
|
||||
pub mod engine;
|
||||
mod output;
|
||||
pub use engine::SuricataEngine;
|
||||
|
||||
@ -78,9 +78,7 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc<FusionE
|
||||
if event.event_type != "alert" {
|
||||
continue;
|
||||
}
|
||||
let (Some(alert), Some(src_ip), Some(dst_ip)) =
|
||||
(event.alert, event.src_ip, event.dest_ip)
|
||||
else {
|
||||
let (Some(alert), Some(src_ip), Some(dst_ip)) = (event.alert, event.src_ip, event.dest_ip) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -103,4 +101,4 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc<FusionE
|
||||
|
||||
fusion.record_rule(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
mod core;
|
||||
mod detection;
|
||||
mod model;
|
||||
mod utils;
|
||||
mod web;
|
||||
mod detection;
|
||||
|
||||
use crate::core::system::System;
|
||||
use crate::model::error::Error;
|
||||
|
||||
#[actix_web::main]
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
let mut system = System::new().await?;
|
||||
system.run().await?;
|
||||
system.terminate().await?;
|
||||
|
||||
|
||||
drop(system);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,9 +21,15 @@ pub struct SuricataConfig {
|
||||
pub suppress: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_af_threads() -> String { "auto".to_string() }
|
||||
fn default_af_ring_size() -> u32 { 2048 }
|
||||
fn default_af_block_size() -> u32 { 131072 }
|
||||
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 {
|
||||
@ -61,6 +67,25 @@ pub struct Config {
|
||||
|
||||
/// Suricata rule engine config. If absent, the rule engine is disabled.
|
||||
pub suricata: Option<SuricataConfig>,
|
||||
/// Auth system config. If absent, auth is disabled and all routes are public.
|
||||
pub auth: Option<AuthConfig>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct AuthConfig {
|
||||
pub jwt_secret: String,
|
||||
pub db_key: String,
|
||||
#[serde(default = "default_token_ttl")]
|
||||
pub token_ttl_secs: u64,
|
||||
#[serde(default = "default_admin_password")]
|
||||
pub default_admin_password: String,
|
||||
}
|
||||
|
||||
fn default_token_ttl() -> u64 {
|
||||
86400
|
||||
}
|
||||
fn default_admin_password() -> String {
|
||||
"admin".to_string()
|
||||
}
|
||||
|
||||
fn default_fusion_mode() -> String {
|
||||
@ -73,4 +98,4 @@ fn default_fusion_window_secs() -> u64 {
|
||||
|
||||
fn default_ae_threshold_method() -> String {
|
||||
"95".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
21
mantis/src/model/error/auth.rs
Normal file
21
mantis/src/model/error/auth.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use macros::traceable;
|
||||
|
||||
traceable! {
|
||||
AuthError {
|
||||
#[no_source]
|
||||
#[error("Database error: {msg}")]
|
||||
DBError { msg: String } => tracing::Level::ERROR,
|
||||
|
||||
#[no_source]
|
||||
#[error("Invalid credentials")]
|
||||
InvalidCredentials => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Token expired or invalid")]
|
||||
InvalidToken => tracing::Level::WARN,
|
||||
|
||||
#[no_source]
|
||||
#[error("Password hashing failed")]
|
||||
HashError => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
@ -70,4 +70,3 @@ traceable! {
|
||||
FlowMapKeyMissing { direction: String, flow_direction: String, time_type: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@ traceable! {
|
||||
#[error("Http Server panic")]
|
||||
ServerPanic => tracing::Level::ERROR,
|
||||
|
||||
#[error("WebSocket error")]
|
||||
WebSocketError => tracing::Level::ERROR,
|
||||
#[no_source]
|
||||
#[error("WebSocket error: {msg}")]
|
||||
WebSocketError { msg: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,4 +26,4 @@ traceable! {
|
||||
#[error("Failed to create traffic log file '{path}': {reason}")]
|
||||
TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,4 +48,4 @@ traceable! {
|
||||
#[error("Failed to write CSV row: {reason}")]
|
||||
TrafficLogWriteFailed { reason: String } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
pub mod auth;
|
||||
pub mod ebpf;
|
||||
pub mod http;
|
||||
pub mod io;
|
||||
pub mod misc;
|
||||
pub mod ml;
|
||||
pub mod system;
|
||||
pub mod suricata;
|
||||
pub mod system;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::model::error::auth::AuthError;
|
||||
use crate::model::error::ebpf::EbpfError;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::io::IOError;
|
||||
@ -18,6 +20,8 @@ use crate::model::error::system::SystemError;
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Auth(AuthError),
|
||||
#[error("{0}")]
|
||||
Ebpf(EbpfError),
|
||||
#[error("{0}")]
|
||||
@ -34,6 +38,12 @@ pub enum Error {
|
||||
System(SystemError),
|
||||
}
|
||||
|
||||
impl From<AuthError> for Error {
|
||||
fn from(error: AuthError) -> Self {
|
||||
Self::Auth(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EbpfError> for Error {
|
||||
fn from(error: EbpfError) -> Self {
|
||||
Self::Ebpf(error)
|
||||
@ -74,4 +84,4 @@ impl From<SuricataError> for Error {
|
||||
fn from(error: SuricataError) -> Self {
|
||||
Self::Suricata(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use common::model::flow_stats::FlowStats;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FlowStatsWithGeo {
|
||||
|
||||
@ -37,17 +37,11 @@ impl NativeConvert for AddrPortV4 {
|
||||
type Native = SocketAddrV4;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
SocketAddrV4::new(
|
||||
Ipv4Addr::from(u32::from_be(self.ip())),
|
||||
self.port()
|
||||
)
|
||||
SocketAddrV4::new(Ipv4Addr::from(u32::from_be(self.ip())), self.port())
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
AddrPortV4::new(
|
||||
native.ip().to_bits().to_be(),
|
||||
native.port()
|
||||
)
|
||||
AddrPortV4::new(native.ip().to_bits().to_be(), native.port())
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,18 +49,10 @@ impl NativeConvert for AddrPortV6 {
|
||||
type Native = SocketAddrV6;
|
||||
|
||||
fn into_native(self) -> Self::Native {
|
||||
SocketAddrV6::new(
|
||||
Ipv6Addr::from(u128::from_be(self.ip())),
|
||||
self.port(),
|
||||
0,
|
||||
0
|
||||
)
|
||||
SocketAddrV6::new(Ipv6Addr::from(u128::from_be(self.ip())), self.port(), 0, 0)
|
||||
}
|
||||
|
||||
fn from_native(native: Self::Native) -> Self {
|
||||
AddrPortV6::new(
|
||||
native.ip().to_bits().to_be(),
|
||||
native.port()
|
||||
)
|
||||
AddrPortV6::new(native.ip().to_bits().to_be(), native.port())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
mantis/src/model/log/auth.rs
Normal file
18
mantis/src/model/log/auth.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use macros::loggable;
|
||||
use tracing;
|
||||
|
||||
loggable! {
|
||||
AuthLog {
|
||||
#[error("Auth DB initialized at {path}")]
|
||||
DbInitialized { path: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Default admin account created")]
|
||||
DefaultAdminCreated => tracing::Level::INFO,
|
||||
|
||||
#[error("Login successful for user: {username}")]
|
||||
LoginSuccess { username: String } => tracing::Level::INFO,
|
||||
|
||||
#[error("Login failed for user: {username}")]
|
||||
LoginFailed { username: String } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,7 @@ loggable! {
|
||||
|
||||
#[error("No frames available for TX")]
|
||||
NoFramesAvailable => tracing::Level::WARN,
|
||||
|
||||
|
||||
#[error("TX wakeup failed: {error}")]
|
||||
TXWakeupFailed { error: String } => tracing::Level::WARN,
|
||||
|
||||
@ -51,4 +51,4 @@ loggable! {
|
||||
#[error("Fill queue incomplete: produced {produced}, expected {expected}")]
|
||||
FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,4 +79,4 @@ loggable! {
|
||||
WindowDebug { src: String, pad: usize, window_size: usize, ae_score: f32, rows: String } => tracing::Level::DEBUG,
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
pub mod auth;
|
||||
pub mod ebpf;
|
||||
pub mod http;
|
||||
pub mod ml;
|
||||
pub mod system;
|
||||
pub mod misc;
|
||||
pub mod health;
|
||||
pub mod http;
|
||||
pub mod misc;
|
||||
pub mod ml;
|
||||
pub mod suricata;
|
||||
pub mod system;
|
||||
|
||||
@ -33,4 +33,4 @@ loggable! {
|
||||
#[error("[suricata] {line}")]
|
||||
ProcessError { line: String } => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,4 +28,4 @@ loggable! {
|
||||
TrafficLoggingEnabled { path: String } => tracing::Level::INFO,
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,4 +236,4 @@ impl UnifiedAlert {
|
||||
rule_msg: Some(m.msg.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,10 +2,10 @@ pub mod config;
|
||||
pub mod direction;
|
||||
pub mod error;
|
||||
pub mod geo_stats;
|
||||
pub mod health;
|
||||
pub mod ip_address;
|
||||
pub mod list_type;
|
||||
pub mod log;
|
||||
pub mod time_type;
|
||||
pub mod ml_detection;
|
||||
pub mod health;
|
||||
pub mod rule_detection;
|
||||
pub mod time_type;
|
||||
|
||||
@ -13,12 +13,7 @@ pub fn convert_ports_to_vec(ports: [u16; MAX_RULES_PORT]) -> Vec<Port> {
|
||||
|
||||
pub fn is_private_ip(ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => {
|
||||
v4.is_private()
|
||||
|| v4.is_loopback()
|
||||
|| v4.is_link_local()
|
||||
|| v4.is_broadcast()
|
||||
}
|
||||
IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local() || v4.is_broadcast(),
|
||||
IpAddr::V6(v6) => {
|
||||
v6.is_loopback()
|
||||
|| v6.is_unique_local() // fc00::/7
|
||||
@ -26,4 +21,4 @@ pub fn is_private_ip(ip: &IpAddr) -> bool {
|
||||
|| v6.is_multicast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
use std::fs;
|
||||
|
||||
use tracing::Level;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
use crate::model::error::io::IOError;
|
||||
use crate::model::error::Error;
|
||||
use crate::model::error::io::IOError;
|
||||
|
||||
pub struct Logging;
|
||||
|
||||
impl Logging {
|
||||
pub fn initialize() -> Result<(), Error> {
|
||||
let log_directory = "logs";
|
||||
fs::create_dir_all(log_directory)
|
||||
.map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
||||
fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
|
||||
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "Mantis");
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
pub mod logging;
|
||||
pub mod static_files;
|
||||
pub mod boot_time;
|
||||
pub mod packet_parser;
|
||||
pub mod cpu_affinity;
|
||||
pub mod logging;
|
||||
pub mod packet_parser;
|
||||
pub mod static_files;
|
||||
|
||||
pub mod ip_address;
|
||||
pub mod ip_address;
|
||||
|
||||
@ -184,4 +184,4 @@ pub fn format_ipv6(addr: u128) -> String {
|
||||
bytes[14],
|
||||
bytes[15]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
105
mantis/src/web/api/auth.rs
Normal file
105
mantis/src/web/api/auth.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{EncodingKey, Header, encode};
|
||||
use macros::log;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::core::infrastructure::app_db::verify_password;
|
||||
use crate::model::log::auth::AuthLog;
|
||||
use crate::web::middleware::auth::{AuthenticatedUser, Claims};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/login", post(login))
|
||||
.route("/me", get(me))
|
||||
.route("/logout", post(logout))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LoginRequest {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoginResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MeResponse {
|
||||
id: String,
|
||||
username: String,
|
||||
role: String,
|
||||
}
|
||||
|
||||
async fn login(State(state): State<AppState>, Json(body): Json<LoginRequest>) -> impl IntoResponse {
|
||||
let auth_cfg = match state.app_config.auth.as_ref() {
|
||||
Some(c) => c,
|
||||
None => return (StatusCode::NOT_IMPLEMENTED, "Auth not configured").into_response(),
|
||||
};
|
||||
let db = match &state.app_db {
|
||||
Some(db) => db,
|
||||
None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not available").into_response(),
|
||||
};
|
||||
|
||||
let account = match db.find_account_by_username(&body.username) {
|
||||
Ok(Some(a)) => a,
|
||||
Ok(None) => {
|
||||
log!(AuthLog::LoginFailed {
|
||||
username: body.username.clone()
|
||||
});
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
||||
}
|
||||
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Database error").into_response(),
|
||||
};
|
||||
|
||||
if !verify_password(&body.password, &account.password_hash) {
|
||||
log!(AuthLog::LoginFailed {
|
||||
username: body.username.clone()
|
||||
});
|
||||
return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response();
|
||||
}
|
||||
|
||||
let exp = (Utc::now().timestamp() as usize) + (auth_cfg.token_ttl_secs as usize);
|
||||
let claims = Claims {
|
||||
sub: account.id.clone(),
|
||||
username: account.username.clone(),
|
||||
role: account.role.clone(),
|
||||
exp,
|
||||
};
|
||||
|
||||
match encode(
|
||||
&Header::default(),
|
||||
&claims,
|
||||
&EncodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()),
|
||||
) {
|
||||
Ok(token) => {
|
||||
log!(AuthLog::LoginSuccess {
|
||||
username: account.username
|
||||
});
|
||||
Json(LoginResponse { token }).into_response()
|
||||
}
|
||||
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Token generation failed").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn me(user: AuthenticatedUser) -> impl IntoResponse {
|
||||
Json(MeResponse {
|
||||
id: user.0.sub,
|
||||
username: user.0.username,
|
||||
role: user.0.role,
|
||||
})
|
||||
}
|
||||
|
||||
async fn logout(_user: AuthenticatedUser) -> impl IntoResponse {
|
||||
StatusCode::OK
|
||||
}
|
||||
@ -1,93 +1,89 @@
|
||||
use std::net::{SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use actix_web::{delete, get, put, web, HttpResponse, Responder, Scope};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{delete, get, put};
|
||||
use axum::{Json, Router};
|
||||
|
||||
use crate::core::ebpf::access_control::AccessControl;
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::model::direction::FlowDirection;
|
||||
use crate::model::list_type::ListType;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/access_control")
|
||||
.service(get_ipv4_list)
|
||||
.service(get_ipv6_list)
|
||||
.service(add_ipv4_list)
|
||||
.service(add_ipv6_list)
|
||||
.service(remove_ipv4_list)
|
||||
.service(remove_ipv6_list)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/ipv4/{direction}/{list_type}",
|
||||
get(get_ipv4_list).put(add_ipv4_list).delete(remove_ipv4_list),
|
||||
)
|
||||
.route(
|
||||
"/ipv6/{direction}/{list_type}",
|
||||
get(get_ipv6_list).put(add_ipv6_list).delete(remove_ipv6_list),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/ipv4/{direction}/{list_type}")]
|
||||
async fn get_ipv4_list(
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let (direction, list_type) = path.into_inner();
|
||||
let list = access_control.get_ipv4_list(direction, list_type).await;
|
||||
HttpResponse::Ok().json(list)
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
Json(state.access_control.get_ipv4_list(direction, list_type).await)
|
||||
}
|
||||
|
||||
#[get("/ipv6/{direction}/{list_type}")]
|
||||
async fn get_ipv6_list(
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let (direction, list_type) = path.into_inner();
|
||||
let list = access_control.get_ipv6_list(direction, list_type).await;
|
||||
HttpResponse::Ok().json(list)
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
Json(state.access_control.get_ipv6_list(direction, list_type).await)
|
||||
}
|
||||
|
||||
#[put("/ipv4/{direction}/{list_type}")]
|
||||
async fn add_ipv4_list(
|
||||
address: web::Json<SocketAddrV4>,
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let address = address.into_inner();
|
||||
let (direction, list_type) = path.into_inner();
|
||||
match access_control.add_ipv4_list(direction, list_type, address).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
Json(address): Json<SocketAddrV4>,
|
||||
) -> impl IntoResponse {
|
||||
match state.access_control.add_ipv4_list(direction, list_type, address).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/ipv6/{direction}/{list_type}")]
|
||||
async fn add_ipv6_list(
|
||||
address: web::Json<SocketAddrV6>,
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let address = address.into_inner();
|
||||
let (direction, list_type) = path.into_inner();
|
||||
match access_control.add_ipv6_list(direction, list_type, address).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
Json(address): Json<SocketAddrV6>,
|
||||
) -> impl IntoResponse {
|
||||
match state.access_control.add_ipv6_list(direction, list_type, address).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv4/{direction}/{list_type}")]
|
||||
async fn remove_ipv4_list(
|
||||
address: web::Json<SocketAddrV4>,
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let address = address.into_inner();
|
||||
let (direction, list_type) = path.into_inner();
|
||||
match access_control.remove_ipv4_list(direction, list_type, address).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
Json(address): Json<SocketAddrV4>,
|
||||
) -> impl IntoResponse {
|
||||
match state
|
||||
.access_control
|
||||
.remove_ipv4_list(direction, list_type, address)
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv6/{direction}/{list_type}")]
|
||||
async fn remove_ipv6_list(
|
||||
address: web::Json<SocketAddrV6>,
|
||||
path: web::Path<(FlowDirection, ListType)>,
|
||||
access_control: web::Data<AccessControl>,
|
||||
) -> impl Responder {
|
||||
let address = address.into_inner();
|
||||
let (direction, list_type) = path.into_inner();
|
||||
match access_control.remove_ipv6_list(direction, list_type, address).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
Path((direction, list_type)): Path<(FlowDirection, ListType)>,
|
||||
State(state): State<AppState>,
|
||||
Json(address): Json<SocketAddrV6>,
|
||||
) -> impl IntoResponse {
|
||||
match state
|
||||
.access_control
|
||||
.remove_ipv6_list(direction, list_type, address)
|
||||
.await
|
||||
{
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@ pub mod access_control;
|
||||
pub mod service;
|
||||
pub mod statistics;
|
||||
|
||||
use actix_web::{web, Scope};
|
||||
use axum::Router;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/ebpf")
|
||||
.service(access_control::initialize())
|
||||
.service(service::initialize())
|
||||
.service(statistics::initialize())
|
||||
use crate::core::app_state::AppState;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest("/access_control", access_control::router())
|
||||
.nest("/service", service::router())
|
||||
.nest("/statistics", statistics::router())
|
||||
}
|
||||
|
||||
@ -1,251 +1,243 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
|
||||
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse, Responder, Scope};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::{delete, get, post, put};
|
||||
use axum::{Json, Router};
|
||||
use common::model::http_method::HttpMethod;
|
||||
|
||||
use crate::core::ebpf::service::Service;
|
||||
use crate::core::app_state::AppState;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/service")
|
||||
.service(get_ipv4_http_service)
|
||||
.service(get_ipv6_http_service)
|
||||
.service(add_ipv4_http_service)
|
||||
.service(add_ipv6_http_service)
|
||||
.service(remove_ipv4_http_service)
|
||||
.service(remove_ipv6_http_service)
|
||||
.service(is_ssh_white_list_enable)
|
||||
.service(enable_ssh_white_list)
|
||||
.service(disable_ssh_white_list)
|
||||
.service(get_ipv4_ssh_service)
|
||||
.service(get_ipv6_ssh_service)
|
||||
.service(add_ipv4_ssh_service)
|
||||
.service(add_ipv6_ssh_service)
|
||||
.service(remove_ipv4_ssh_service)
|
||||
.service(remove_ipv6_ssh_service)
|
||||
.service(get_ipv4_ssh_white_list)
|
||||
.service(get_ipv6_ssh_white_list)
|
||||
.service(add_ipv4_ssh_white_list)
|
||||
.service(add_ipv6_ssh_white_list)
|
||||
.service(remove_ipv4_ssh_white_list)
|
||||
.service(remove_ipv6_ssh_white_list)
|
||||
.service(get_ipv4_ssh_black_list)
|
||||
.service(get_ipv6_ssh_black_list)
|
||||
.service(add_ipv4_ssh_black_list)
|
||||
.service(add_ipv6_ssh_black_list)
|
||||
.service(remove_ipv4_ssh_black_list)
|
||||
.service(remove_ipv6_ssh_black_list)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
"/ipv4/http_service",
|
||||
get(get_ipv4_http_service)
|
||||
.put(add_ipv4_http_service)
|
||||
.delete(remove_ipv4_http_service),
|
||||
)
|
||||
.route(
|
||||
"/ipv6/http_service",
|
||||
get(get_ipv6_http_service)
|
||||
.put(add_ipv6_http_service)
|
||||
.delete(remove_ipv6_http_service),
|
||||
)
|
||||
.route("/ssh_white_list", get(is_ssh_white_list_enable))
|
||||
.route("/ssh_white_list/enable", post(enable_ssh_white_list))
|
||||
.route("/ssh_white_list/disable", post(disable_ssh_white_list))
|
||||
.route(
|
||||
"/ipv4/ssh_service",
|
||||
get(get_ipv4_ssh_service)
|
||||
.put(add_ipv4_ssh_service)
|
||||
.delete(remove_ipv4_ssh_service),
|
||||
)
|
||||
.route(
|
||||
"/ipv6/ssh_service",
|
||||
get(get_ipv6_ssh_service)
|
||||
.put(add_ipv6_ssh_service)
|
||||
.delete(remove_ipv6_ssh_service),
|
||||
)
|
||||
.route(
|
||||
"/ipv4/ssh_white_list",
|
||||
get(get_ipv4_ssh_white_list)
|
||||
.put(add_ipv4_ssh_white_list)
|
||||
.delete(remove_ipv4_ssh_white_list),
|
||||
)
|
||||
.route(
|
||||
"/ipv6/ssh_white_list",
|
||||
get(get_ipv6_ssh_white_list)
|
||||
.put(add_ipv6_ssh_white_list)
|
||||
.delete(remove_ipv6_ssh_white_list),
|
||||
)
|
||||
.route(
|
||||
"/ipv4/ssh_black_list",
|
||||
get(get_ipv4_ssh_black_list)
|
||||
.put(add_ipv4_ssh_black_list)
|
||||
.delete(remove_ipv4_ssh_black_list),
|
||||
)
|
||||
.route(
|
||||
"/ipv6/ssh_black_list",
|
||||
get(get_ipv6_ssh_black_list)
|
||||
.put(add_ipv6_ssh_black_list)
|
||||
.delete(remove_ipv6_ssh_black_list),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/ipv4/http_service")]
|
||||
async fn get_ipv4_http_service(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv4_http_service().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv4_http_service(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv4_http_service().await)
|
||||
}
|
||||
|
||||
#[get("/ipv6/http_service")]
|
||||
async fn get_ipv6_http_service(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv6_http_service().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv6_http_service(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv6_http_service().await)
|
||||
}
|
||||
|
||||
#[put("/ipv4/http_service")]
|
||||
async fn add_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<Service>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.add_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
) -> impl IntoResponse {
|
||||
let (addr, methods) = payload;
|
||||
match state.service.add_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/ipv6/http_service")]
|
||||
async fn add_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<Service>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.add_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
) -> impl IntoResponse {
|
||||
let (addr, methods) = payload;
|
||||
match state.service.add_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv4/http_service")]
|
||||
async fn remove_ipv4_http_service(
|
||||
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
service: web::Data<Service>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.remove_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<(SocketAddrV4, Vec<HttpMethod>)>,
|
||||
) -> impl IntoResponse {
|
||||
let (addr, methods) = payload;
|
||||
match state.service.remove_ipv4_http_service(addr, methods).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv6/http_service")]
|
||||
async fn remove_ipv6_http_service(
|
||||
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
service: web::Data<Service>,
|
||||
) -> impl Responder {
|
||||
let (addr, methods) = payload.into_inner();
|
||||
match service.remove_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<(SocketAddrV6, Vec<HttpMethod>)>,
|
||||
) -> impl IntoResponse {
|
||||
let (addr, methods) = payload;
|
||||
match state.service.remove_ipv6_http_service(addr, methods).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ssh_white_list")]
|
||||
async fn is_ssh_white_list_enable(service: web::Data<Service>) -> impl Responder {
|
||||
let enabled = service.is_ssh_white_list_enable().await;
|
||||
HttpResponse::Ok().json(enabled)
|
||||
async fn is_ssh_white_list_enable(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.is_ssh_white_list_enable().await)
|
||||
}
|
||||
|
||||
#[post("/ssh_white_list/enable")]
|
||||
async fn enable_ssh_white_list(service: web::Data<Service>) -> impl Responder {
|
||||
match service.enable_ssh_white_list().await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn enable_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
match state.service.enable_ssh_white_list().await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/ssh_white_list/disable")]
|
||||
async fn disable_ssh_white_list(service: web::Data<Service>) -> impl Responder {
|
||||
match service.disable_ssh_white_list().await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn disable_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
match state.service.disable_ssh_white_list().await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ipv4/ssh_service")]
|
||||
async fn get_ipv4_ssh_service(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_service().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv4_ssh_service(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv4_ssh_service().await)
|
||||
}
|
||||
|
||||
#[get("/ipv6/ssh_service")]
|
||||
async fn get_ipv6_ssh_service(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_service().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv6_ssh_service(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv6_ssh_service().await)
|
||||
}
|
||||
|
||||
#[put("/ipv4/ssh_service")]
|
||||
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv4_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv4_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV4>) -> impl IntoResponse {
|
||||
match state.service.add_ipv4_ssh_service(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/ipv6/ssh_service")]
|
||||
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv6_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv6_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV6>) -> impl IntoResponse {
|
||||
match state.service.add_ipv6_ssh_service(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv4/ssh_service")]
|
||||
async fn remove_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv4_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV4>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv4_ssh_service(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv6/ssh_service")]
|
||||
async fn remove_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_service(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv6_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV6>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv6_ssh_service(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ipv4/ssh_white_list")]
|
||||
async fn get_ipv4_ssh_white_list(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_white_list().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv4_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv4_ssh_white_list().await)
|
||||
}
|
||||
|
||||
#[get("/ipv6/ssh_white_list")]
|
||||
async fn get_ipv6_ssh_white_list(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_white_list().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv6_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv6_ssh_white_list().await)
|
||||
}
|
||||
|
||||
#[put("/ipv4/ssh_white_list")]
|
||||
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv4_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
|
||||
match state.service.add_ipv4_ssh_white_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/ipv6/ssh_white_list")]
|
||||
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv6_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
|
||||
match state.service.add_ipv6_ssh_white_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv4/ssh_white_list")]
|
||||
async fn remove_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv4_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv4_ssh_white_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv6/ssh_white_list")]
|
||||
async fn remove_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv6_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv6_ssh_white_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/ipv4/ssh_black_list")]
|
||||
async fn get_ipv4_ssh_black_list(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv4_ssh_black_list().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv4_ssh_black_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv4_ssh_black_list().await)
|
||||
}
|
||||
|
||||
#[get("/ipv6/ssh_black_list")]
|
||||
async fn get_ipv6_ssh_black_list(service: web::Data<Service>) -> impl Responder {
|
||||
let list = service.get_ipv6_ssh_black_list().await;
|
||||
HttpResponse::Ok().json(web::Json(list))
|
||||
async fn get_ipv6_ssh_black_list(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.service.get_ipv6_ssh_black_list().await)
|
||||
}
|
||||
|
||||
#[put("/ipv4/ssh_black_list")]
|
||||
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv4_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
|
||||
match state.service.add_ipv4_ssh_black_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/ipv6/ssh_black_list")]
|
||||
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn add_ipv6_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
|
||||
match state.service.add_ipv6_ssh_black_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv4/ssh_black_list")]
|
||||
async fn remove_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv4_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv4_ssh_black_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[delete("/ipv6/ssh_black_list")]
|
||||
async fn remove_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder {
|
||||
match service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
async fn remove_ipv6_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
|
||||
match state.service.remove_ipv6_ssh_black_list(addr).await {
|
||||
Ok(_) => StatusCode::OK.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,76 +1,76 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use axum::extract::ws::WebSocketUpgrade;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::{Json, Router};
|
||||
use macros::log;
|
||||
|
||||
use crate::core::ebpf::statistics::Statistics;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::model::direction::{Direction, FlowDirection};
|
||||
use crate::model::time_type::TimeType;
|
||||
use crate::web::websocket::flow_websocket;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/statistics")
|
||||
.service(get_ipv4_flow)
|
||||
.service(get_ipv6_flow)
|
||||
.service(websocket_ipv4)
|
||||
.service(websocket_ipv6)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/get/ipv4/{direction}/{flow_direction}/{time_type}", get(get_ipv4_flow))
|
||||
.route("/get/ipv6/{direction}/{flow_direction}/{time_type}", get(get_ipv6_flow))
|
||||
.route(
|
||||
"/websocket/ipv4/{direction}/{flow_direction}/{time_type}",
|
||||
get(websocket_ipv4),
|
||||
)
|
||||
.route(
|
||||
"/websocket/ipv6/{direction}/{flow_direction}/{time_type}",
|
||||
get(websocket_ipv6),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/get/ipv4/{direction}/{flow_direction}/{time_type}")]
|
||||
async fn get_ipv4_flow(
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
statistics: web::Data<Arc<Statistics>>,
|
||||
) -> impl Responder {
|
||||
let (direction, flow_direction, time_type) = path.into_inner();
|
||||
match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)),
|
||||
Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
match state
|
||||
.statistics
|
||||
.get_ipv4_flow_data(direction, flow_direction, time_type)
|
||||
.await
|
||||
{
|
||||
Ok(data) => Json(data).into_response(),
|
||||
Err(e) => {
|
||||
log!(e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/get/ipv6/{direction}/{flow_direction}/{time_type}")]
|
||||
async fn get_ipv6_flow(
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
statistics: web::Data<Arc<Statistics>>,
|
||||
) -> impl Responder {
|
||||
let (direction, flow_direction, time_type) = path.into_inner();
|
||||
match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)),
|
||||
Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
match state
|
||||
.statistics
|
||||
.get_ipv6_flow_data(direction, flow_direction, time_type)
|
||||
.await
|
||||
{
|
||||
Ok(data) => Json(data).into_response(),
|
||||
Err(e) => {
|
||||
log!(e);
|
||||
HttpResponse::InternalServerError().finish()
|
||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")]
|
||||
async fn websocket_ipv4(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
statistics: web::Data<Statistics>,
|
||||
) -> impl Responder {
|
||||
match flow_websocket::websocket_ipv4_flow(req, stream, path, app_config, statistics).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)),
|
||||
}
|
||||
Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
|
||||
State(state): State<AppState>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| flow_websocket::handle_ipv4_flow(socket, state, direction, flow_direction, time_type))
|
||||
}
|
||||
|
||||
#[get("/websocket/ipv6/{direction}/{flow_direction}/{time_type}")]
|
||||
async fn websocket_ipv6(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
statistics: web::Data<Statistics>,
|
||||
) -> impl Responder {
|
||||
match flow_websocket::websocket_ipv6_flow(req, stream, path, app_config, statistics).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)),
|
||||
}
|
||||
Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
|
||||
State(state): State<AppState>,
|
||||
ws: WebSocketUpgrade,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(move |socket| flow_websocket::handle_ipv6_flow(socket, state, direction, flow_direction, time_type))
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder};
|
||||
use mime_guess::from_path;
|
||||
use axum::http::{StatusCode, Uri};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
||||
use crate::utils::static_files::StaticFiles;
|
||||
|
||||
pub async fn default_route(req: HttpRequest) -> impl Responder {
|
||||
let request_path = req.path();
|
||||
pub async fn default_route(uri: Uri) -> Response {
|
||||
let request_path = uri.path();
|
||||
|
||||
let file_system_path = if request_path == "/" {
|
||||
"web/index.html".to_string()
|
||||
@ -13,30 +13,31 @@ pub async fn default_route(req: HttpRequest) -> impl Responder {
|
||||
};
|
||||
|
||||
if let Some(content) = StaticFiles::get(&file_system_path) {
|
||||
let mime_type = from_path(&file_system_path).first_or_octet_stream();
|
||||
return HttpResponse::Ok()
|
||||
.content_type(mime_type.as_ref())
|
||||
.body(content.data.into_owned());
|
||||
let mime_type = mime_guess::from_path(&file_system_path).first_or_octet_stream();
|
||||
return (
|
||||
[("content-type", mime_type.as_ref().to_string())],
|
||||
content.data.into_owned(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let html_path = format!("{}.html", file_system_path);
|
||||
if let Some(content) = StaticFiles::get(&html_path) {
|
||||
return HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(content.data.into_owned());
|
||||
return ([("content-type", "text/html")], content.data.into_owned()).into_response();
|
||||
}
|
||||
|
||||
let index_path = format!("{}/index.html", file_system_path);
|
||||
if let Some(content) = StaticFiles::get(&index_path) {
|
||||
return HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(content.data.into_owned());
|
||||
return ([("content-type", "text/html")], content.data.into_owned()).into_response();
|
||||
}
|
||||
|
||||
match StaticFiles::get("web/404.html") {
|
||||
Some(page) => HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body(page.data.into_owned()),
|
||||
None => HttpResponse::NotFound().body("404 Not Found"),
|
||||
Some(page) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
[("content-type", "text/html")],
|
||||
page.data.into_owned(),
|
||||
)
|
||||
.into_response(),
|
||||
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,15 @@
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use axum::extract::State;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::web::websocket::alert_websocket;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/detection")
|
||||
.service(websocket_alert)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new().route("/websocket/alert", get(websocket_alert))
|
||||
}
|
||||
|
||||
#[get("/websocket/alert")]
|
||||
async fn websocket_alert(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
da: web::Data<DetectionAlert>,
|
||||
) -> impl Responder {
|
||||
match alert_websocket::websocket_alert(req, stream, da).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err))
|
||||
}
|
||||
}
|
||||
async fn websocket_alert(ws: axum::extract::ws::WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
|
||||
let rx = state.detection_alert.subscribe();
|
||||
ws.on_upgrade(|socket| alert_websocket::handle_alert(socket, rx))
|
||||
}
|
||||
|
||||
@ -1,35 +1,30 @@
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope};
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::web::websocket::health_websocket;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/health")
|
||||
.service(get_current_metrics)
|
||||
.service(get_health_status)
|
||||
.service(websocket_metrics)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/metrics", get(get_current_metrics))
|
||||
.route("/status", get(get_health_status))
|
||||
.route("/websocket/metrics", get(websocket_metrics))
|
||||
}
|
||||
|
||||
#[get("/metrics")]
|
||||
async fn get_current_metrics(health: web::Data<SystemHealth>) -> impl Responder {
|
||||
let metrics = health.get_current_metrics().await;
|
||||
HttpResponse::Ok().json(metrics)
|
||||
async fn get_current_metrics(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.health.get_current_metrics().await)
|
||||
}
|
||||
|
||||
#[get("/status")]
|
||||
async fn get_health_status(health: web::Data<SystemHealth>) -> impl Responder {
|
||||
let status = health.is_system_healthy().await;
|
||||
HttpResponse::Ok().json(status)
|
||||
async fn get_health_status(State(state): State<AppState>) -> impl IntoResponse {
|
||||
Json(state.health.is_system_healthy().await)
|
||||
}
|
||||
|
||||
#[get("/websocket/metrics")]
|
||||
async fn websocket_metrics(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
health: web::Data<SystemHealth>,
|
||||
) -> impl Responder {
|
||||
match health_websocket::websocket_system_health(req, stream, health).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)),
|
||||
}
|
||||
}
|
||||
ws: axum::extract::ws::WebSocketUpgrade,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let rx = state.health.subscribe_to_metrics();
|
||||
ws.on_upgrade(|socket| health_websocket::handle_health(socket, rx))
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
use actix_web::{get, web, HttpResponse, Responder, Scope};
|
||||
use axum::Json;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::utils::boot_time::boot_time;
|
||||
|
||||
pub fn initialize() -> Scope {
|
||||
web::scope("/misc")
|
||||
.service(get_boot_time)
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new().route("/boot_time", get(get_boot_time))
|
||||
}
|
||||
|
||||
#[get("/boot_time")]
|
||||
async fn get_boot_time() -> impl Responder {
|
||||
let boot_time = boot_time();
|
||||
HttpResponse::Ok().json(boot_time)
|
||||
async fn get_boot_time() -> impl IntoResponse {
|
||||
Json(boot_time())
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod control;
|
||||
pub mod default;
|
||||
pub mod detection_alert;
|
||||
|
||||
43
mantis/src/web/middleware/auth.rs
Normal file
43
mantis/src/web/middleware/auth.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use axum::extract::FromRequestParts;
|
||||
use axum::http::{StatusCode, request::Parts};
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::app_state::AppState;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Claims {
|
||||
pub sub: String,
|
||||
pub username: String,
|
||||
pub role: String,
|
||||
pub exp: usize,
|
||||
}
|
||||
|
||||
pub struct AuthenticatedUser(pub Claims);
|
||||
|
||||
impl FromRequestParts<AppState> for AuthenticatedUser {
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
|
||||
let auth_cfg = state
|
||||
.app_config
|
||||
.auth
|
||||
.as_ref()
|
||||
.ok_or((StatusCode::NOT_IMPLEMENTED, "Auth not configured"))?;
|
||||
|
||||
let token = parts
|
||||
.headers
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
|
||||
|
||||
decode::<Claims>(
|
||||
token,
|
||||
&DecodingKey::from_secret(auth_cfg.jwt_secret.as_bytes()),
|
||||
&Validation::new(Algorithm::HS256),
|
||||
)
|
||||
.map(|d| AuthenticatedUser(d.claims))
|
||||
.map_err(|_| (StatusCode::UNAUTHORIZED, "Unauthorized"))
|
||||
}
|
||||
}
|
||||
1
mantis/src/web/middleware/mod.rs
Normal file
1
mantis/src/web/middleware/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod auth;
|
||||
@ -1,2 +1,3 @@
|
||||
pub mod api;
|
||||
pub mod middleware;
|
||||
pub mod websocket;
|
||||
|
||||
@ -1,91 +1,47 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use actix_ws::{handle, Message, MessageStream, Session};
|
||||
use futures_util::StreamExt;
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::core::infrastructure::detection_alert::DetectionAlert;
|
||||
use crate::model::ml_detection::UnifiedAlert;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::log::http::HttpLog;
|
||||
use crate::model::ml_detection::UnifiedAlert;
|
||||
|
||||
pub async fn websocket_alert(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
da: web::Data<DetectionAlert>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
pub async fn handle_alert(socket: WebSocket, mut broadcast_rx: broadcast::Receiver<UnifiedAlert>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
let broadcast_rx = da.subscribe();
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
handle_alert_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_alert_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<UnifiedAlert>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(alert) => {
|
||||
if !send_alert(&mut session, &alert).await {
|
||||
break;
|
||||
}
|
||||
msg = receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if sender.send(Message::Pong(data)).await.is_err() { break; }
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLaged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Err(e)) => {
|
||||
log!(HttpError::WebSocketError { msg: e.to_string() });
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_alert(session: &mut Session, alert: &UnifiedAlert) -> bool {
|
||||
match serde_json::to_string(alert) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
result = broadcast_rx.recv() => {
|
||||
match result {
|
||||
Ok(alert) => {
|
||||
match serde_json::to_string(&alert) {
|
||||
Ok(json) => {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() { break; }
|
||||
}
|
||||
Err(e) => { log!(MiscError::SerializeError(e)); }
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
log!(HttpLog::WebSocketLaged { skipped: n });
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,192 +1,96 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use actix_ws::{handle, Message, MessageStream, Session};
|
||||
use futures_util::StreamExt;
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use macros::log;
|
||||
use tokio::time::{interval, Duration};
|
||||
use tokio::time::{Duration, interval};
|
||||
|
||||
use crate::core::ebpf::statistics::Statistics;
|
||||
use crate::core::infrastructure::app_config::AppConfig;
|
||||
use crate::core::app_state::AppState;
|
||||
use crate::model::direction::{Direction, FlowDirection};
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::time_type::TimeType;
|
||||
|
||||
pub async fn websocket_ipv4_flow(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
statistics: web::Data<Statistics>,
|
||||
) -> Result<HttpResponse> {
|
||||
let app_config = app_config.into_inner();
|
||||
let statistics = statistics.into_inner();
|
||||
let (direction, flow_direction, time_type) = path.into_inner();
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
handle_ipv4_flow_connection(
|
||||
app_config,
|
||||
statistics,
|
||||
session,
|
||||
msg_stream,
|
||||
direction,
|
||||
flow_direction,
|
||||
time_type,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn websocket_ipv6_flow(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
path: web::Path<(Direction, FlowDirection, TimeType)>,
|
||||
app_config: web::Data<AppConfig>,
|
||||
statistics: web::Data<Statistics>,
|
||||
) -> Result<HttpResponse> {
|
||||
let app_config = app_config.into_inner();
|
||||
let statistics = statistics.into_inner();
|
||||
let (direction, flow_direction, time_type) = path.into_inner();
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
handle_ipv6_flow_connection(
|
||||
app_config,
|
||||
statistics,
|
||||
session,
|
||||
msg_stream,
|
||||
direction,
|
||||
flow_direction,
|
||||
time_type,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_ipv4_flow_connection(
|
||||
app_config: Arc<AppConfig>,
|
||||
statistics: Arc<Statistics>,
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
pub async fn handle_ipv4_flow(
|
||||
socket: WebSocket,
|
||||
state: AppState,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) {
|
||||
let config = app_config.config.clone();
|
||||
let refresh_interval = Duration::from_secs(config.refresh_interval);
|
||||
let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
let mut data_interval = interval(refresh_interval);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
msg = receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if sender.send(Message::Pong(data)).await.is_err() { break; }
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Err(e)) => {
|
||||
log!(HttpError::WebSocketError { msg: e.to_string() });
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ = data_interval.tick() => {
|
||||
if !send_ipv4_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await {
|
||||
break;
|
||||
match state.statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(data) => {
|
||||
match serde_json::to_string(&data) {
|
||||
Ok(json) => {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() { break; }
|
||||
}
|
||||
Err(e) => { log!(MiscError::SerializeError(e)); }
|
||||
}
|
||||
}
|
||||
Err(e) => { log!(e); break; }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_ipv6_flow_connection(
|
||||
app_config: Arc<AppConfig>,
|
||||
statistics: Arc<Statistics>,
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
pub async fn handle_ipv6_flow(
|
||||
socket: WebSocket,
|
||||
state: AppState,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) {
|
||||
let config = app_config.config.clone();
|
||||
let refresh_interval = Duration::from_secs(config.refresh_interval);
|
||||
let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
let mut data_interval = interval(refresh_interval);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
msg = receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if sender.send(Message::Pong(data)).await.is_err() { break; }
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Err(e)) => {
|
||||
log!(HttpError::WebSocketError { msg: e.to_string() });
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
_ = data_interval.tick() => {
|
||||
if !send_ipv6_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await {
|
||||
break;
|
||||
match state.statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(data) => {
|
||||
match serde_json::to_string(&data) {
|
||||
Ok(json) => {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() { break; }
|
||||
}
|
||||
Err(e) => { log!(MiscError::SerializeError(e)); }
|
||||
}
|
||||
}
|
||||
Err(e) => { log!(e); break; }
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_ipv4_flow_data(
|
||||
statistics: &Arc<Statistics>,
|
||||
session: &mut Session,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) -> bool {
|
||||
let flow_data = match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => { log!(e); return false; }
|
||||
};
|
||||
match serde_json::to_string(&flow_data) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_ipv6_flow_data(
|
||||
statistics: &Arc<Statistics>,
|
||||
session: &mut Session,
|
||||
direction: Direction,
|
||||
flow_direction: FlowDirection,
|
||||
time_type: TimeType,
|
||||
) -> bool {
|
||||
let flow_data = match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
|
||||
Ok(data) => data,
|
||||
Err(e) => { log!(e); return false; }
|
||||
};
|
||||
match serde_json::to_string(&flow_data) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,91 +1,47 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse, Result};
|
||||
use actix_ws::{handle, Message, MessageStream, Session};
|
||||
use futures_util::StreamExt;
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use macros::log;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::core::infrastructure::health::SystemHealth;
|
||||
use crate::model::error::http::HttpError;
|
||||
use crate::model::error::misc::MiscError;
|
||||
use crate::model::log::http::HttpLog;
|
||||
use crate::model::health::SystemHealthMetrics;
|
||||
use crate::model::log::http::HttpLog;
|
||||
|
||||
pub async fn websocket_system_health(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
health: web::Data<SystemHealth>,
|
||||
) -> Result<HttpResponse> {
|
||||
let (response, session, msg_stream) = handle(&req, body)?;
|
||||
pub async fn handle_health(socket: WebSocket, mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>) {
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
let broadcast_rx = health.subscribe_to_metrics();
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
handle_health_connection(session, msg_stream, broadcast_rx).await;
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn handle_health_connection(
|
||||
mut session: Session,
|
||||
mut msg_stream: MessageStream,
|
||||
mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg_result = msg_stream.next() => {
|
||||
if !handle_client_message(&mut session, msg_result).await {
|
||||
break;
|
||||
}
|
||||
},
|
||||
broadcast_result = broadcast_rx.recv() => {
|
||||
match broadcast_result {
|
||||
Ok(metrics) => {
|
||||
if !send_metrics(&mut session, &metrics).await {
|
||||
break;
|
||||
}
|
||||
msg = receiver.next() => {
|
||||
match msg {
|
||||
Some(Ok(Message::Ping(data))) => {
|
||||
if sender.send(Message::Pong(data)).await.is_err() { break; }
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
log!(HttpLog::WebSocketLaged(skipped));
|
||||
continue;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
Some(Ok(Message::Close(_))) | None => break,
|
||||
Some(Err(e)) => {
|
||||
log!(HttpError::WebSocketError { msg: e.to_string() });
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
result = broadcast_rx.recv() => {
|
||||
match result {
|
||||
Ok(metrics) => {
|
||||
match serde_json::to_string(&metrics) {
|
||||
Ok(json) => {
|
||||
if sender.send(Message::Text(json.into())).await.is_err() { break; }
|
||||
}
|
||||
Err(e) => { log!(MiscError::SerializeError(e)); }
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||
log!(HttpLog::WebSocketLaged { skipped: n });
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = session.close(None).await;
|
||||
}
|
||||
|
||||
async fn handle_client_message(
|
||||
session: &mut Session,
|
||||
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
|
||||
) -> bool {
|
||||
match msg_result {
|
||||
Some(Ok(Message::Text(_))) => true,
|
||||
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
|
||||
Some(Ok(Message::Close(reason))) => {
|
||||
let _ = (session.clone()).close(reason).await;
|
||||
false
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
log!(HttpError::WebSocketError(err));
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_metrics(session: &mut Session, metrics: &SystemHealthMetrics) -> bool {
|
||||
match serde_json::to_string(metrics) {
|
||||
Ok(json) => session.text(json).await.is_ok(),
|
||||
Err(err) => {
|
||||
log!(MiscError::SerializeError(err));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
pub mod alert_websocket;
|
||||
pub mod flow_websocket;
|
||||
pub mod health_websocket;
|
||||
pub mod alert_websocket;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user