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:
ParrotXray 2026-05-23 11:29:23 +08:00 committed by GitHub
parent 46a69dd49c
commit 6eb64d36da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 1888 additions and 1566 deletions

1101
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,25 +10,39 @@
- **Deep Learning Models** - Identifies and predicts potential network attacks with intelligent threat detection - **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 - **Hardware Integration** - Designed to work with Intel i350 T2 and similar enterprise-grade network interface cards
## Functional Modules [//]: # (## Functional Modules)
### Resource Overview [//]: # ()
![Home](.github/images/index.png) [//]: # (### Resource Overview)
- Real-time control system occupancy rate
### Dashboard Overview [//]: # (![Home](.github/images/index.png))
![Dashboard](.github/images/dashboard.png)
- Real-time network traffic monitoring and visualization
- Recent traffic statistics and trend analysis
### Detailed Traffic Statistics [//]: # (- Real-time control system occupancy rate)
![Statistics](.github/images/statistics.png)
- Detailed traffic usage information per IP address
### Network Access Control [//]: # ()
![accessControl](.github/images/accessControl.png) [//]: # (### Dashboard Overview)
- IPv4/IPv6 whitelist and blacklist management
- Precise port-level access control [//]: # (![Dashboard](.github/images/dashboard.png))
[//]: # (- Real-time network traffic monitoring and visualization)
[//]: # (- Recent traffic statistics and trend analysis)
[//]: # ()
[//]: # (### Detailed Traffic Statistics)
[//]: # (![Statistics](.github/images/statistics.png))
[//]: # (- Detailed traffic usage information per IP address)
[//]: # ()
[//]: # (### Network Access Control)
[//]: # (![accessControl](.github/images/accessControl.png))
[//]: # (- IPv4/IPv6 whitelist and blacklist management)
[//]: # (- Precise port-level access control)
[//]: # (### AI Attack Detection) [//]: # (### AI Attack Detection)

1
TODO
View File

@ -4,6 +4,7 @@ Mantis TODO (updated 2026-05-21)
-- DONE (archived) ---------------------------------------- -- DONE (archived) ----------------------------------------
[x] CSV rolling log with date-based filenames [x] CSV rolling log with date-based filenames
[x] Migrate inference engine to ort-tract (pure Rust) [x] Migrate inference engine to ort-tract (pure Rust)
[x] Improve ML inference throughput under high load [x] Improve ML inference throughput under high load

View File

@ -109,7 +109,6 @@ impl Event {
} }
} }
#[repr(C, align(8))] #[repr(C, align(8))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct IPv4Event { pub struct IPv4Event {

View File

@ -40,6 +40,13 @@ ml_cpu = 7
ae_threshold_method = "94" 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. # Suricata rule engine. Remove this entire section to disable.
[Config.suricata] [Config.suricata]
home_net = "140.130.34.0/24" home_net = "140.130.34.0/24"

1
lib/ebpf/info.txt Normal file
View File

@ -0,0 +1 @@
// this's ebpf folder

View File

@ -1,7 +1,7 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
use syn::parse::{Parse, ParseStream}; use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Expr, Token}; use syn::{Expr, Token, parse_macro_input};
struct LogInput { struct LogInput {
error: Expr, error: Expr,
@ -62,5 +62,5 @@ pub fn log_impl(input: TokenStream) -> TokenStream {
} }
} }
} }
.into() .into()
} }

@ -1 +1 @@
Subproject commit f1fa091f4c1abc90049b32c36f2d339400090713 Subproject commit 66ae9de967702ef02ab47baa9597ab8f16e3ab91

View File

@ -7,10 +7,9 @@ edition = "2024"
common = { path = "../common", features = ["user"] } common = { path = "../common", features = ["user"] }
macros = { path = "../macros" } macros = { path = "../macros" }
actix = "0.13.5" axum = { version = "0.8", features = ["ws", "macros"] }
actix-cors = "0.7.1" tower = { version = "0.5", features = ["util"] }
actix-web = "4.11.0" tower-http = { version = "0.6", features = ["cors"] }
actix-ws = "0.4.0"
aya = { workspace = true } aya = { workspace = true }
aya-log = { workspace = true } aya-log = { workspace = true }
network-types = { workspace = true } network-types = { workspace = true }
@ -25,7 +24,6 @@ serde_json = "1.0.143"
sysinfo = "0.38.2" sysinfo = "0.38.2"
thiserror = "2.0.3" thiserror = "2.0.3"
tokio = { version = "1.40.0", features = ["full", "macros"] } tokio = { version = "1.40.0", features = ["full", "macros"] }
tokio-tungstenite = "0.28.0"
toml = "1.0.3" toml = "1.0.3"
tracing = "0.1.41" tracing = "0.1.41"
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
@ -37,6 +35,10 @@ lru = "0.16.2"
futures = "0.3.31" futures = "0.3.31"
tract-onnx = "0.22.1" tract-onnx = "0.22.1"
chrono = "0.4" 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-tract = { version = "0.3.0+0.22", optional = true }
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray"] } ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "ndarray"] }

View File

@ -8,12 +8,18 @@ use std::time::SystemTime;
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target}; use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target};
fn main() { 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 manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let artifact_dir = manifest_dir.join("static").join("artifacts"); let artifact_dir = manifest_dir.join("static").join("artifacts");
let rule_dir = manifest_dir.join("static").join("rules"); 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 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 static_web = manifest_dir.join("static").join("web");
let project_name = manifest_dir.file_name().unwrap().to_string_lossy().into_owned(); 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=ARTIFACTCS_PATH={}", artifact_dir.display());
println!("cargo:rustc-env=CSV_RECORD_PATH={}", csv_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=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=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()); println!("cargo:rustc-env=RULE_EVE_PATH={}", suricata_eve_socket.display());
for item in &[ for item in &[
@ -42,21 +51,19 @@ fn main() {
} }
if env::var_os("SKIP_EBPF_BUILD").is_some() { if env::var_os("SKIP_EBPF_BUILD").is_some() {
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); for path in &[&ingress_edpf_dir, &egress_edpf_dir] {
for name in &["mantis-ingress", "mantis-egress"] {
let path = out.join(name);
if !path.exists() { 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; return;
} }
build_ingress_ebpf(); build_ingress_ebpf(&ingress_edpf_dir);
build_egress_ebpf(); build_egress_ebpf(&egress_edpf_dir);
build_frontend(&frontend_dir, &static_web); 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 Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages let ebpf_package = packages
.into_iter() .into_iter()
@ -155,14 +162,12 @@ fn build_ingress_ebpf() {
stderr.join().map_err(std::panic::resume_unwind).unwrap(); stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables { 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}"));
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 Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages let ebpf_package = packages
.into_iter() .into_iter()
@ -262,10 +267,8 @@ fn build_egress_ebpf() {
stderr.join().map_err(std::panic::resume_unwind).unwrap(); stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables { 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}"));
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
} }
} }

View 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>>,
}

View File

@ -8,8 +8,8 @@ use common::model::ip_address::{IPv4, IPv6, Port};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::model::direction::FlowDirection; use crate::model::direction::FlowDirection;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::ip_address::NativeConvert; use crate::model::ip_address::NativeConvert;
use crate::model::list_type::ListType; use crate::model::list_type::ListType;
use crate::utils::ip_address::convert_ports_to_vec; use crate::utils::ip_address::convert_ports_to_vec;

View File

@ -17,8 +17,8 @@ use crate::core::ebpf::xsk_manager::XskManager;
use crate::core::infrastructure::app_config::AppConfig; use crate::core::infrastructure::app_config::AppConfig;
use crate::detection::ml::engine::Engine; use crate::detection::ml::engine::Engine;
use crate::detection::suricata::SuricataEngine; use crate::detection::suricata::SuricataEngine;
use crate::model::error::system::SystemError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::system::SystemError;
pub struct EbpfServices { pub struct EbpfServices {
pub xsk_manager: Arc<XskManager>, pub xsk_manager: Arc<XskManager>,
@ -29,11 +29,7 @@ pub struct EbpfServices {
} }
impl EbpfServices { impl EbpfServices {
pub fn new( pub fn new(app_config: Arc<AppConfig>, ingress_ebpf: &mut Ebpf, egress_ebpf: &mut Ebpf) -> Result<Self, Error> {
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 xsk_manager = XskManager::new(app_config.clone(), ingress_ebpf, egress_ebpf)?;
let access_control = AccessControl::new(ingress_ebpf)?; let access_control = AccessControl::new(ingress_ebpf)?;
let service = Service::new(ingress_ebpf)?; let service = Service::new(ingress_ebpf)?;
@ -70,4 +66,4 @@ impl EbpfServices {
} }
} }
} }
} }

View File

@ -8,8 +8,8 @@ use common::model::ip_address::{AddrPortV4, AddrPortV6, IPv4, IPv6};
use common::model::placeholder::PlaceHolder; use common::model::placeholder::PlaceHolder;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::ip_address::NativeConvert; use crate::model::ip_address::NativeConvert;
pub struct Service { pub struct Service {

View File

@ -160,11 +160,13 @@ impl Statistics {
let map = self let map = self
.ipv4_maps .ipv4_maps
.get(&(direction, flow_direction, time_type)) .get(&(direction, flow_direction, time_type))
.ok_or_else(|| EbpfError::FlowMapKeyMissing( .ok_or_else(|| {
format!("{:?}", direction), EbpfError::FlowMapKeyMissing(
format!("{:?}", flow_direction), format!("{:?}", direction),
format!("{:?}", time_type), format!("{:?}", flow_direction),
))?; format!("{:?}", time_type),
)
})?;
let flow_data = map.write().await.get_map(); let flow_data = map.write().await.get_map();
if let Some(ref geo_ip) = self.geo_ip { if let Some(ref geo_ip) = self.geo_ip {
@ -197,11 +199,13 @@ impl Statistics {
let map = self let map = self
.ipv6_maps .ipv6_maps
.get(&(direction, flow_direction, time_type)) .get(&(direction, flow_direction, time_type))
.ok_or_else(|| EbpfError::FlowMapKeyMissing( .ok_or_else(|| {
format!("{:?}", direction), EbpfError::FlowMapKeyMissing(
format!("{:?}", flow_direction), format!("{:?}", direction),
format!("{:?}", time_type), format!("{:?}", flow_direction),
))?; format!("{:?}", time_type),
)
})?;
let flow_data = map.write().await.get_map(); let flow_data = map.write().await.get_map();
if let Some(ref geo_ip) = self.geo_ip { if let Some(ref geo_ip) = self.geo_ip {

View File

@ -6,9 +6,9 @@ use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use aya::maps::{MapData, XskMap};
use aya::Ebpf; 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 crossbeam::queue::SegQueue;
use macros::log; use macros::log;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -21,9 +21,9 @@ use crate::detection::ml::engine::Engine;
use crate::detection::suricata::SuricataEngine; use crate::detection::suricata::SuricataEngine;
use crate::model::config::Config; use crate::model::config::Config;
use crate::model::direction::Direction; use crate::model::direction::Direction;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError; use crate::model::error::ebpf::EbpfError;
use crate::model::error::system::SystemError; use crate::model::error::system::SystemError;
use crate::model::error::Error;
use crate::model::log::ebpf::EbpfLog; use crate::model::log::ebpf::EbpfLog;
use crate::utils::cpu_affinity::set_cpu_affinity; 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); set_cpu_affinity((start + self.queue_id % num_cores) as usize);
} }
let mut shutdown_rx = Some(shutdown_rx); let mut shutdown_rx = Some(shutdown_rx);
let mut idle_count: u32 = 0; let mut idle_count: u32 = 0;
let mut last_cleanup = std::time::Instant::now(); let mut last_cleanup = std::time::Instant::now();
@ -286,10 +285,7 @@ impl XskPair {
Ok(nb_completed) Ok(nb_completed)
} }
fn process_rx_queue( fn process_rx_queue(&mut self, forward_tx: &Sender<Vec<u8>>) -> Result<usize, EbpfError> {
&mut self,
forward_tx: &Sender<Vec<u8>>,
) -> Result<usize, EbpfError> {
let mut rx_descs = vec![FrameDesc::default(); 64]; let mut rx_descs = vec![FrameDesc::default(); 64];
let rx_count = unsafe { self.rx.consume(&mut rx_descs) }; let rx_count = unsafe { self.rx.consume(&mut rx_descs) };
@ -394,4 +390,4 @@ impl XskPair {
Ok(nb_submitted) Ok(nb_submitted)
} }
} }

View File

@ -2,8 +2,8 @@ use std::fs;
use std::ops::Deref; use std::ops::Deref;
use crate::model::config::{Config, ConfigTable}; use crate::model::config::{Config, ConfigTable};
use crate::model::error::system::SystemError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::system::SystemError;
pub struct AppConfig { pub struct AppConfig {
pub config: Config, pub config: Config,

View 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)
}

View File

@ -1,11 +1,11 @@
use std::net::IpAddr; use std::net::IpAddr;
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use maxminddb::{geoip2, MaxMindDbError, Reader};
use tokio::sync::{RwLock, Semaphore};
use lru::LruCache; use lru::LruCache;
use std::num::NonZeroUsize; use maxminddb::{MaxMindDbError, Reader, geoip2};
use tokio::sync::{RwLock, Semaphore};
use tokio::task; use tokio::task;
use crate::model::geo_stats::GeoLocation; use crate::model::geo_stats::GeoLocation;
@ -25,13 +25,10 @@ impl GeoIpService {
Self::with_cache_size(db_path, 10000) Self::with_cache_size(db_path, 10000)
} }
pub fn with_cache_size<P: AsRef<Path>>( pub fn with_cache_size<P: AsRef<Path>>(db_path: P, cache_size: usize) -> Result<Self, MaxMindDbError> {
db_path: P,
cache_size: usize,
) -> Result<Self, MaxMindDbError> {
let reader = Reader::open_readfile(db_path)?; let reader = Reader::open_readfile(db_path)?;
let cache_capacity = NonZeroUsize::new(cache_size) let cache_capacity =
.unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero")); NonZeroUsize::new(cache_size).unwrap_or_else(|| NonZeroUsize::new(10000).expect("10000 is non-zero"));
Ok(Self { Ok(Self {
reader: Arc::new(reader), 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 { .map_err(|_| MaxMindDbError::InvalidDatabase {
message: "GeoIP semaphore closed".to_string(), message: "GeoIP semaphore closed".to_string(),
offset: None, offset: None,
@ -70,11 +71,11 @@ impl GeoIpService {
let _permit = permit; let _permit = permit;
Self::lookup_from_db_blocking(&reader, ip) Self::lookup_from_db_blocking(&reader, ip)
}) })
.await .await
.map_err(|e| MaxMindDbError::InvalidDatabase { .map_err(|e| MaxMindDbError::InvalidDatabase {
message: format!("Task join error: {}", e), message: format!("Task join error: {}", e),
offset: None, offset: None,
})??; })??;
{ {
let mut cache = self.cache.write().await; let mut cache = self.cache.write().await;
@ -84,22 +85,16 @@ impl GeoIpService {
Ok(result) Ok(result)
} }
fn lookup_from_db_blocking( fn lookup_from_db_blocking(reader: &Reader<Vec<u8>>, ip: IpAddr) -> Result<Option<GeoLocation>, MaxMindDbError> {
reader: &Reader<Vec<u8>>,
ip: IpAddr,
) -> Result<Option<GeoLocation>, MaxMindDbError> {
let lookup_result = reader.lookup(ip)?; let lookup_result = reader.lookup(ip)?;
let city_option: Option<geoip2::City> = lookup_result.decode()?; let city_option: Option<geoip2::City> = lookup_result.decode()?;
Ok(city_option.map(|city| { Ok(city_option.map(|city| {
let country_name = city.country.names.english let country_name = city.country.names.english.map(|s| s.to_string());
.map(|s| s.to_string());
let country_code = city.country.iso_code let country_code = city.country.iso_code.map(|s| s.to_string());
.map(|s| s.to_string());
let city_name = city.city.names.english let city_name = city.city.names.english.map(|s| s.to_string());
.map(|s| s.to_string());
let latitude = city.location.latitude.or(Some(0.0)); let latitude = city.location.latitude.or(Some(0.0));
let longitude = city.location.longitude.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; let cache = self.cache.read().await;
(cache.len(), cache.cap().get()) (cache.len(), cache.cap().get())
} }
} }

View File

@ -1,25 +1,18 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use sysinfo::{Components, Networks, System};
use tokio::sync::{broadcast, oneshot, RwLock};
use tokio::time::interval;
use macros::log; 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::core::infrastructure::app_config::AppConfig;
use crate::model::log::health::Health;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::health::{ use crate::model::health::{
ConfiguredNetworkStats, ConfiguredNetworkStats, CpuCoreInfo, CpuDetails, LoadAverage, MemoryUsage, NetworkStats, SystemHealthMetrics,
CpuCoreInfo, SystemHealthStatus, SystemInfo,
CpuDetails,
LoadAverage,
MemoryUsage,
NetworkStats,
SystemHealthMetrics,
SystemHealthStatus,
SystemInfo
}; };
use crate::model::log::health::Health;
pub struct SystemHealth { pub struct SystemHealth {
system: RwLock<System>, system: RwLock<System>,
@ -30,7 +23,6 @@ pub struct SystemHealth {
egress_interface: String, egress_interface: String,
} }
impl SystemHealth { impl SystemHealth {
pub fn new(config: Arc<AppConfig>) -> Result<Self, Error> { pub fn new(config: Arc<AppConfig>) -> Result<Self, Error> {
let (broadcast_tx, _) = broadcast::channel(100); let (broadcast_tx, _) = broadcast::channel(100);
@ -125,11 +117,7 @@ impl SystemHealth {
swap_used: system.used_swap(), swap_used: system.used_swap(),
}; };
let network_stats = Self::collect_configured_network_stats( let network_stats = Self::collect_configured_network_stats(networks, ingress_interface, egress_interface);
networks,
ingress_interface,
egress_interface,
);
let load_average = System::load_average(); 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 { 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); let egress = create_network_stats(egress_interface);
if ingress.is_none() { 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() { 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 { ConfiguredNetworkStats { ingress, egress }
ingress,
egress,
}
} }
pub async fn get_current_metrics(&self) -> SystemHealthMetrics { pub async fn get_current_metrics(&self) -> SystemHealthMetrics {
@ -290,22 +281,17 @@ impl SystemHealth {
metrics.memory_usage.usage_percent metrics.memory_usage.usage_percent
)); ));
} else if metrics.memory_usage.usage_percent > 80.0 { } else if metrics.memory_usage.usage_percent > 80.0 {
status.warnings.push(format!( status
"High memory usage: {:.1}%", .warnings
metrics.memory_usage.usage_percent .push(format!("High memory usage: {:.1}%", metrics.memory_usage.usage_percent));
));
} }
if let Some(temp) = metrics.temperature { if let Some(temp) = metrics.temperature {
if temp > 80.0 { if temp > 80.0 {
status.overall_healthy = false; status.overall_healthy = false;
status status.issues.push(format!("High CPU temperature: {:.1}°C", temp));
.issues
.push(format!("High CPU temperature: {:.1}°C", temp));
} else if temp > 70.0 { } else if temp > 70.0 {
status status.warnings.push(format!("Elevated CPU temperature: {:.1}°C", temp));
.warnings
.push(format!("Elevated CPU temperature: {:.1}°C", temp));
} }
} }
@ -320,4 +306,4 @@ impl SystemHealth {
status status
} }
} }

View File

@ -1,7 +1,8 @@
pub mod app_config; pub mod app_config;
pub mod app_db;
pub mod detection_alert; pub mod detection_alert;
pub mod health;
pub mod geoip; pub mod geoip;
pub mod health;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -13,6 +14,7 @@ use macros::log;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use crate::core::infrastructure::app_config::AppConfig; 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::detection_alert::DetectionAlert;
use crate::core::infrastructure::health::SystemHealth; use crate::core::infrastructure::health::SystemHealth;
use crate::detection::fusion::{FusionEngine, FusionMode}; 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::model_loader::MLModels;
use crate::detection::ml::traffic_logger::TrafficLogger; use crate::detection::ml::traffic_logger::TrafficLogger;
use crate::detection::suricata::SuricataEngine; use crate::detection::suricata::SuricataEngine;
use crate::model::error::misc::MiscError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError; use crate::model::error::system::SystemError;
use crate::model::log::system::SystemLog; use crate::model::log::system::SystemLog;
@ -34,6 +36,7 @@ pub struct AppServices {
pub ml_models: Arc<MLModels>, pub ml_models: Arc<MLModels>,
pub ml_engine: Arc<Engine>, pub ml_engine: Arc<Engine>,
pub suricata_engine: Option<Arc<SuricataEngine>>, pub suricata_engine: Option<Arc<SuricataEngine>>,
pub app_db: Option<Arc<AppDB>>,
shutdowns: SegQueue<oneshot::Sender<()>>, shutdowns: SegQueue<oneshot::Sender<()>>,
} }
@ -53,12 +56,21 @@ impl AppServices {
let traffic_logger = if app_config.traffic_logging_mode { let traffic_logger = if app_config.traffic_logging_mode {
let dir = PathBuf::from(env!("CSV_RECORD_PATH")); let dir = PathBuf::from(env!("CSV_RECORD_PATH"));
let basename = app_config.traffic_log_csv_path.trim_end_matches(".csv").to_string(); 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.extend(FlowFeatures::all_feature_names_owned());
header.push("Label".to_string()); header.push("Label".to_string());
let logger = TrafficLogger::new(&dir, &basename, header) let logger = TrafficLogger::new(&dir, &basename, header)
.map_err(|e| MiscError::TrafficLogCreateError(dir.display().to_string(), e.to_string()))?; .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)) Some(Arc::new(logger))
} else { } else {
None None
@ -80,7 +92,21 @@ impl AppServices {
let suricata_engine = if let Some(ref sc) = app_config.suricata { let suricata_engine = if let Some(ref sc) = app_config.suricata {
let rule_path = PathBuf::from(env!("RULE_PATH")); let rule_path = PathBuf::from(env!("RULE_PATH"));
let eve_socket = PathBuf::from(env!("RULE_EVE_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 { } else {
None None
}; };
@ -92,6 +118,7 @@ impl AppServices {
ml_models, ml_models,
ml_engine, ml_engine,
suricata_engine, suricata_engine,
app_db,
shutdowns: SegQueue::new(), shutdowns: SegQueue::new(),
}) })
} }
@ -116,4 +143,4 @@ impl AppServices {
} }
} }
} }
} }

View File

@ -1,3 +1,4 @@
pub mod app_state;
pub mod ebpf; pub mod ebpf;
pub mod infrastructure; pub mod infrastructure;
pub mod system; pub mod system;

View File

@ -1,26 +1,29 @@
use std::sync::Arc; use std::sync::Arc;
use actix_web::web::route; use axum::Router;
use actix_web::{web, App, HttpServer}; use aya::Ebpf;
use aya::maps::{MapData, ProgramArray}; use aya::maps::{MapData, ProgramArray};
use aya::programs::{Xdp, XdpFlags}; use aya::programs::{Xdp, XdpFlags};
use aya::Ebpf;
use aya_log::EbpfLogger; use aya_log::EbpfLogger;
use common::define::program_array::*; use common::define::program_array::*;
use macros::log; 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::ebpf::EbpfServices;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::AppServices; use crate::core::infrastructure::AppServices;
use crate::core::infrastructure::app_config::AppConfig;
use crate::detection::ml::config_loader::InferenceConfig; use crate::detection::ml::config_loader::InferenceConfig;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError; use crate::model::error::ebpf::EbpfError;
use crate::model::error::http::HttpError; use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError; use crate::model::error::misc::MiscError;
use crate::model::error::Error;
use crate::model::log::ml::MLLog; use crate::model::log::ml::MLLog;
use crate::model::log::system::SystemLog; use crate::model::log::system::SystemLog;
use crate::utils::logging::Logging; 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 struct System {
pub app_config: Arc<AppConfig>, pub app_config: Arc<AppConfig>,
@ -41,7 +44,10 @@ impl System {
let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?; let (mut egress_ebpf, egress_program_array) = System::get_egress_ebpf()?;
let app_config = Arc::new(AppConfig::new()?); 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( let ebpf_services = Arc::new(EbpfServices::new(
app_config.clone(), app_config.clone(),
@ -82,7 +88,9 @@ impl System {
log!(SystemLog::InitializeComplete); log!(SystemLog::InitializeComplete);
self.attach_ebpf()?; 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?; app_services.run().await?;
self.run_http_server().await?; self.run_http_server().await?;
Ok(()) Ok(())
@ -134,49 +142,45 @@ impl System {
} }
async fn run_http_server(&self) -> Result<(), Error> { async fn run_http_server(&self) -> Result<(), Error> {
let app_config = self.app_config.clone(); let state = AppState {
let inference_config = self.inference_config.clone(); app_config: self.app_config.clone(),
let access_control = self.ebpf_services.access_control.clone(); inference_config: self.inference_config.clone(),
let service = self.ebpf_services.service.clone(); access_control: self.ebpf_services.access_control.clone(),
let statistics = self.ebpf_services.statistics.clone(); service: self.ebpf_services.service.clone(),
let health = self.app_services.health.clone(); statistics: self.ebpf_services.statistics.clone(),
let detection_alert = self.app_services.detection_alert.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; let port = self.app_config.http_server_bind_port;
HttpServer::new(move || { let listener = TcpListener::bind(format!("0.0.0.0:{}", port))
let cors = actix_cors::Cors::default() .await
.allow_any_origin() .map_err(HttpError::BindPortError)?;
.allow_any_method()
.allow_any_header() axum::serve(listener, app)
.max_age(3600); .with_graceful_shutdown(async {
App::new() tokio::signal::ctrl_c().await.ok();
.wrap(cors) })
.app_data(web::Data::from(app_config.clone())) .await
.app_data(web::Data::from(inference_config.clone())) .map_err(HttpError::ServerPanic)?;
.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)?;
Ok(()) Ok(())
} }
fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> { fn get_ingress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
let mut ingress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( let mut ingress_ebpf =
env!("OUT_DIR"), Ebpf::load(aya::include_bytes_aligned!(env!("INGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;
"/mantis-ingress"
)))
.map_err(EbpfError::EbpfNotFound)?;
let program_array = ingress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; 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)?; let mut program_array = ProgramArray::try_from(program_array).map_err(EbpfError::MapOperationError)?;
Self::load_program( Self::load_program(
@ -197,11 +201,8 @@ impl System {
} }
fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> { fn get_egress_ebpf() -> Result<(Ebpf, ProgramArray<MapData>), Error> {
let mut egress_ebpf = Ebpf::load(aya::include_bytes_aligned!(concat!( let mut egress_ebpf =
env!("OUT_DIR"), Ebpf::load(aya::include_bytes_aligned!(env!("EGRESS_PATH"))).map_err(EbpfError::EbpfNotFound)?;
"/mantis-egress"
)))
.map_err(EbpfError::EbpfNotFound)?;
let program_array = egress_ebpf.take_map("PROGRAM_ARRAY").ok_or(EbpfError::MapNotFound)?; 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)?; 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)?; Self::load_program(&mut egress_ebpf, &mut program_array, "statistics", egress::STATISTICS)?;

View File

@ -14,7 +14,11 @@ pub enum FusionMode {
impl FusionMode { impl FusionMode {
pub fn from_str(s: &str) -> Self { 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; return;
} }
self.alert.broadcast(UnifiedAlert::from_ml(result)); 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 => { FusionMode::And => {
let maybe_rule = { let maybe_rule = {
@ -100,7 +111,14 @@ impl FusionEngine {
return; return;
} }
self.alert.broadcast(UnifiedAlert::from_rule(m)); 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 => { FusionMode::And => {
let maybe_ml = { let maybe_ml = {

View File

@ -5,7 +5,7 @@ use crate::model::ml_detection::FlowKey;
// L2 thresholds: anomalous flows per src_ip within the aggregation window // L2 thresholds: anomalous flows per src_ip within the aggregation window
const FLOOD_THRESHOLD: usize = 10; // total anomalous flows -> FLOOD 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 { struct SrcIpState {
events: Vec<(Instant, u16)>, // (time, dst_port) events: Vec<(Instant, u16)>, // (time, dst_port)
@ -39,8 +39,7 @@ impl AttackAggregator {
detections.push((now, score)); detections.push((now, score));
if detections.len() >= self.min_detections { if detections.len() >= self.min_detections {
let avg_score: f32 = let avg_score: f32 = detections.iter().map(|(_, s)| s).sum::<f32>() / detections.len() as f32;
detections.iter().map(|(_, s)| s).sum::<f32>() / detections.len() as f32;
return avg_score > threshold * self.alert_threshold_multiplier; return avg_score > threshold * self.alert_threshold_multiplier;
} }
@ -52,10 +51,13 @@ impl AttackAggregator {
let now = Instant::now(); let now = Instant::now();
let window = self.window_duration; let window = self.window_duration;
let state = self.src_ip_states.entry(src_ip.to_string()).or_insert_with(|| SrcIpState { let state = self
events: Vec::new(), .src_ip_states
last_alert: None, .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.retain(|(t, _)| now.duration_since(*t) < window);
state.events.push((now, dst_port)); state.events.push((now, dst_port));
@ -89,7 +91,9 @@ impl AttackAggregator {
!detections.is_empty() !detections.is_empty()
}); });
self.src_ip_states.retain(|_, state| { 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() !state.events.is_empty()
}); });
} }
@ -97,4 +101,4 @@ impl AttackAggregator {
pub fn tracked_flows(&self) -> usize { pub fn tracked_flows(&self) -> usize {
self.detections.len() self.detections.len()
} }
} }

View File

@ -1,9 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use macros::log; use macros::log;
use serde::{Deserialize, Serialize};
use crate::model::error::ml::MLError; use crate::model::error::ml::MLError;
use crate::model::ml_detection::ClipParams; use crate::model::ml_detection::ClipParams;
@ -25,15 +26,18 @@ pub struct InferenceConfig {
impl InferenceConfig { impl InferenceConfig {
pub fn load_file(file: &str, method: &str) -> Result<Self, MLError> { pub fn load_file(file: &str, method: &str) -> Result<Self, MLError> {
let path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(file); let path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(file);
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?;
.map_err(|_| MLError::ConfigLoadFailed { path: path.clone() })?; let mut config: InferenceConfig =
let mut config: InferenceConfig = serde_json::from_str(&content) serde_json::from_str(&content).map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
.map_err(|e| MLError::ConfigParseFailed { reason: e.to_string() })?;
// Validate the method exists at startup — fail fast rather than at inference time. // Validate the method exists at startup — fail fast rather than at inference time.
if !config.ae_thresholds.contains_key(method) { if !config.ae_thresholds.contains_key(method) {
log!(MLError::ThresholdMethodNotFound { method: method.to_string() }); log!(MLError::ThresholdMethodNotFound {
return Err(MLError::ThresholdMethodNotFound { method: method.to_string() }); method: method.to_string()
});
return Err(MLError::ThresholdMethodNotFound {
method: method.to_string(),
});
} }
config.ae_threshold_method = method.to_string(); config.ae_threshold_method = method.to_string();
@ -43,4 +47,4 @@ impl InferenceConfig {
pub fn num_ae_features(&self) -> usize { pub fn num_ae_features(&self) -> usize {
self.ae_feature_names.len() self.ae_feature_names.len()
} }
} }

View File

@ -12,7 +12,6 @@ use super::flow_tracker::FlowTracker;
use super::inference::Inference; use super::inference::Inference;
use super::model_loader::MLModels; use super::model_loader::MLModels;
use super::traffic_logger::TrafficLogger; use super::traffic_logger::TrafficLogger;
use crate::detection::fusion::FusionEngine; use crate::detection::fusion::FusionEngine;
use crate::model::error::ml::MLError; use crate::model::error::ml::MLError;
use crate::model::log::ml::MLLog; use crate::model::log::ml::MLLog;
@ -81,9 +80,9 @@ impl Engine {
loop { loop {
tokio::select! { tokio::select! {
_ = &mut shutdown_rx => break, _ = &mut shutdown_rx => break,
_ = ticker.tick() => {} _ = ticker.tick() => {}
} }
let (total_flows, flows, active_ips) = { let (total_flows, flows, active_ips) = {
let Ok(mut t) = self.tracker.lock() else { 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 // (which is always empty). This preserves per-src_ip inference buffers
// across consecutive cycles so the LSTM window can fill up over time. // 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. // A src_ip absent from this cycle loses its buffer on the next cleanup.
let active_ips: std::collections::HashSet<String> = flows let active_ips: std::collections::HashSet<String> =
.iter() flows.iter().map(|f| f.flow_key.src_ip.clone()).collect();
.map(|f| f.flow_key.src_ip.clone())
.collect();
(total_flows, flows, active_ips) (total_flows, flows, active_ips)
}; };
@ -122,7 +119,9 @@ impl Engine {
let mut batch = flows[..flows.len().min(self.batch_size)].to_vec(); let mut batch = flows[..flows.len().min(self.batch_size)].to_vec();
batch.sort_by(|a, b| { 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)) .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 pipeline = Arc::clone(&self.inference_pipeline);
let ml_cpu = self.ml_cpu; let ml_cpu = self.ml_cpu;
let mut handle = tokio::task::spawn_blocking(move || { 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); set_cpu_affinity(cpu);
pipeline.infer_batch(&batch) pipeline.infer_batch(&batch)
}); });
@ -166,11 +167,8 @@ impl Engine {
for result in &results { for result in &results {
if result.is_attack { if result.is_attack {
// L1: full 5-tuple aggregation for persistent same-port attacks // L1: full 5-tuple aggregation for persistent same-port attacks
let should_alert = aggregator.should_alert( let should_alert =
&result.flow_key_raw, aggregator.should_alert(&result.flow_key_raw, result.ae_score, result.threshold);
result.ae_score,
result.threshold,
);
if should_alert { if should_alert {
log!(MLLog::ThreatDetected( log!(MLLog::ThreatDetected(
format!("{:?}", result.direction), format!("{:?}", result.direction),
@ -185,10 +183,9 @@ impl Engine {
// L2: src_ip-level scan/flood detection (skipped if L1 already fired) // L2: src_ip-level scan/flood detection (skipped if L1 already fired)
if !alerted_src_ips.contains(&result.flow_key_raw.src_ip) { if !alerted_src_ips.contains(&result.flow_key_raw.src_ip) {
if let Some(attack_type) = aggregator.should_alert_src_ip( if let Some(attack_type) = aggregator
&result.flow_key_raw.src_ip, .should_alert_src_ip(&result.flow_key_raw.src_ip, result.flow_key_raw.dst_port)
result.flow_key_raw.dst_port, {
) {
let mut l2_result = result.clone(); let mut l2_result = result.clone();
l2_result.attack_type = Some(attack_type.to_string()); l2_result.attack_type = Some(attack_type.to_string());
log!(MLLog::ThreatDetected( log!(MLLog::ThreatDetected(
@ -235,4 +232,4 @@ impl Engine {
}; };
EngineStats { active_flows } EngineStats { active_flows }
} }
} }

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::{Utc, TimeZone};
use chrono::{TimeZone, Utc};
use super::flow_tracker::FlowData; use super::flow_tracker::FlowData;
use crate::model::ml_detection::{ClipParams, PacketData}; 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 bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
let all_lengths: Vec<f64> = flow 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) .map(|p| p.payload_length as f64)
.collect(); .collect();
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths); 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 fwd_bulk = &flow.fwd_bulk_state;
let bwd_bulk = &flow.bwd_bulk_state; let bwd_bulk = &flow.bwd_bulk_state;
let fwd_header_sizes: Vec<f64> = flow let fwd_header_sizes: Vec<f64> = flow.fwd_packets.iter().map(|p| p.header_length as f64).collect();
.fwd_packets
.iter()
.map(|p| p.header_length as f64)
.collect();
let (active_max, active_min, active_mean, active_std) = let (active_max, active_min, active_mean, active_std) =
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>()); 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 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), "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 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 Bytes/Bulk" | "fwd_avg_bytes_bulk" => {
"Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64), 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 Avg Bulk Rate" | "fwd_avg_bulk_rate" => safe_div(
fwd_bulk.total_bytes as f64, fwd_bulk.total_bytes as f64,
fwd_bulk.total_duration_us as f64 / 1_000_000.0, 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 Bytes/Bulk" | "bwd_avg_bytes_bulk" => {
"Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64), 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 Avg Bulk Rate" | "bwd_avg_bulk_rate" => safe_div(
bwd_bulk.total_bytes as f64, bwd_bulk.total_bytes as f64,
bwd_bulk.total_duration_us as f64 / 1_000_000.0, 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) .windows(2)
.map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64) .map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64)
.collect() .collect()
} }

View File

@ -103,13 +103,27 @@ impl FlowData {
self.bwd_fin_seen = true; self.bwd_fin_seen = true;
} }
} }
if packet.tcp_flags().syn { self.syn_count += 1; } if packet.tcp_flags().syn {
if packet.tcp_flags().rst { self.rst_count += 1; } self.syn_count += 1;
if packet.tcp_flags().psh { self.psh_count += 1; } }
if packet.tcp_flags().ack { self.ack_count += 1; } if packet.tcp_flags().rst {
if packet.tcp_flags().urg { self.urg_count += 1; } self.rst_count += 1;
if packet.tcp_flags().cwr { self.cwe_count += 1; } }
if packet.tcp_flags().ece { self.ece_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); let iat = packet.timestamp_us().saturating_sub(self.last_packet_time);
const IDLE_THRESHOLD_US: u64 = 1_000_000; const IDLE_THRESHOLD_US: u64 = 1_000_000;
@ -166,9 +180,7 @@ impl FlowData {
if packet.payload_length > 0 { if packet.payload_length > 0 {
// Idle break: discard helper, start fresh // Idle break: discard helper, start fresh
if bulk_state.in_bulk if bulk_state.in_bulk && packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US {
&& packet.timestamp_us.saturating_sub(bulk_state.last_bulk_packet_us) > BULK_IDLE_US
{
bulk_state.in_bulk = false; bulk_state.in_bulk = false;
bulk_state.last_bulk_bytes = 0; bulk_state.last_bulk_bytes = 0;
bulk_state.last_bulk_packets = 0; bulk_state.last_bulk_packets = 0;
@ -193,14 +205,12 @@ impl FlowData {
bulk_state.bulk_count += 1; bulk_state.bulk_count += 1;
bulk_state.total_packets += 4; bulk_state.total_packets += 4;
bulk_state.total_bytes += bulk_state.last_bulk_bytes; bulk_state.total_bytes += bulk_state.last_bulk_bytes;
bulk_state.total_duration_us += packet.timestamp_us bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(bulk_state.last_bulk_start_us);
.saturating_sub(bulk_state.last_bulk_start_us);
} else if bulk_state.last_bulk_packets > 4 { } else if bulk_state.last_bulk_packets > 4 {
// Each subsequent packet adds incrementally // Each subsequent packet adds incrementally
bulk_state.total_packets += 1; bulk_state.total_packets += 1;
bulk_state.total_bytes += packet.length as u64; bulk_state.total_bytes += packet.length as u64;
bulk_state.total_duration_us += packet.timestamp_us bulk_state.total_duration_us += packet.timestamp_us.saturating_sub(prev_us);
.saturating_sub(prev_us);
} }
} }
} else { } else {
@ -279,7 +289,8 @@ impl FlowTracker {
let initiator_direction = if is_forward { direction } else { direction.flip() }; let initiator_direction = if is_forward { direction } else { direction.flip() };
let flow = self.flows let flow = self
.flows
.entry(actual_key.clone()) .entry(actual_key.clone())
.or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction)); .or_insert_with(|| FlowData::new(actual_key, &packet, initiator_direction));
@ -305,10 +316,8 @@ impl FlowTracker {
.duration_since(time::UNIX_EPOCH) .duration_since(time::UNIX_EPOCH)
.map(|d| d.as_micros() as u64) .map(|d| d.as_micros() as u64)
.unwrap_or(0); .unwrap_or(0);
self.flows.retain(|_, flow| { self.flows
!flow.is_finished() .retain(|_, flow| !flow.is_finished() && now.saturating_sub(flow.last_time_us) < max_age_us);
&& now.saturating_sub(flow.last_time_us) < max_age_us
});
} }
pub fn flow_count(&self) -> usize { pub fn flow_count(&self) -> usize {
@ -357,4 +366,4 @@ fn detect_initiator(payload: &[u8], protocol: u8, src_port: u16, dst_port: u16)
} }
None None
} }

View File

@ -1,5 +1,5 @@
use std::collections::VecDeque;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Instant; use std::time::Instant;
@ -90,10 +90,7 @@ impl Inference {
} }
let t2 = Instant::now(); let t2 = Instant::now();
let ae_input = Array3::from_shape_fn( let ae_input = Array3::from_shape_fn((1, window_size, feat_len), |(_, t, f)| sequence[t][f]);
(1, window_size, feat_len),
|(_, t, f)| sequence[t][f],
);
let t3 = Instant::now(); let t3 = Instant::now();
let ae_score = match self.run_autoencoder(&ae_input) { let ae_score = match self.run_autoencoder(&ae_input) {
@ -114,14 +111,19 @@ impl Inference {
)); ));
let pad = window_size - buf_len_snapshot; let pad = window_size - buf_len_snapshot;
let rows = sequence.iter().enumerate().map(|(t, row)| { let rows = sequence
if t < pad { .iter()
format!(" t{t:02}: [pad]") .enumerate()
} else { .map(|(t, row)| {
let vals = row.iter().map(|v| format!("{v:7.3}")).collect::<Vec<_>>().join(" "); if t < pad {
format!(" t{t:02}: [{vals}]") format!(" t{t:02}: [pad]")
} } else {
}).collect::<Vec<_>>().join("\n"); 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( log!(MLLog::WindowDebug(
flow.flow_key.src_ip.clone(), flow.flow_key.src_ip.clone(),
pad, pad,
@ -180,4 +182,4 @@ impl Inference {
Ok(mse) Ok(mse)
} }
} }

View File

@ -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 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; pub mod traffic_logger;

View File

@ -18,7 +18,8 @@ impl MLModels {
{ {
log!(MLLog::BackendNativeOrt); log!(MLLog::BackendNativeOrt);
ort::init_from(PathBuf::from(env!("ONNXRUNTIME_PATH"))) ort::init_from(PathBuf::from(env!("ONNXRUNTIME_PATH")))
.map_err(|_| MLError::InitializeFailed)?.commit(); .map_err(|_| MLError::InitializeFailed)?
.commit();
} }
#[cfg(feature = "tract-backend")] #[cfg(feature = "tract-backend")]
{ {
@ -35,9 +36,13 @@ impl MLModels {
let model_path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(model_name); let model_path = PathBuf::from(env!("ARTIFACTCS_PATH")).join(model_name);
Session::builder() Session::builder()
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })? .map_err(|_| MLError::ModelLoadFailed {
path: model_path.clone(),
})?
.with_optimization_level(GraphOptimizationLevel::All) .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) .commit_from_file(&model_path)
.map_err(|_| MLError::ModelLoadFailed { path: model_path }) .map_err(|_| MLError::ModelLoadFailed { path: model_path })
} }
@ -55,4 +60,4 @@ impl MLModels {
_ => "unknown model".to_string(), _ => "unknown model".to_string(),
} }
} }
} }

View File

@ -1,11 +1,11 @@
use std::fs::{self, OpenOptions}; use std::fs::{self, OpenOptions};
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use std::path::{PathBuf, Path}; use std::path::{Path, PathBuf};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use chrono::Local; use chrono::Local;
use crossbeam::channel::{bounded, Sender, TrySendError, RecvTimeoutError}; use crossbeam::channel::{RecvTimeoutError, Sender, TrySendError, bounded};
use macros::log; use macros::log;
use crate::model::error::ml::MLError; use crate::model::error::ml::MLError;

View File

@ -1,3 +1,3 @@
pub mod fusion; pub mod fusion;
pub mod ml; pub mod ml;
pub mod suricata; pub mod suricata;

View File

@ -6,16 +6,15 @@ use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use crossbeam::channel::{bounded, Sender}; use crossbeam::channel::{Sender, bounded};
use macros::log; use macros::log;
use super::output;
use crate::detection::fusion::FusionEngine; use crate::detection::fusion::FusionEngine;
use crate::model::config::SuricataConfig; use crate::model::config::SuricataConfig;
use crate::model::error::suricata::SuricataError; use crate::model::error::suricata::SuricataError;
use crate::model::log::suricata::SuricataLog; use crate::model::log::suricata::SuricataLog;
use super::output;
const MIRROR_IFACE: &str = "mantis-mirror"; const MIRROR_IFACE: &str = "mantis-mirror";
const MIRROR_PEER: &str = "mantis-peer"; const MIRROR_PEER: &str = "mantis-peer";
const SURICATA_LOG: &str = "/tmp/suricata.log"; const SURICATA_LOG: &str = "/tmp/suricata.log";
@ -42,10 +41,12 @@ impl SuricataEngine {
output::start_eve_reader(path, fusion); output::start_eve_reader(path, fusion);
} }
let rule_path_str = rule_path.to_str() let rule_path_str = rule_path.to_str().ok_or_else(|| SuricataError::InvalidPath {
.ok_or_else(|| SuricataError::InvalidPath { path: rule_path.display().to_string() })?; 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 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 = Self::generate_suppress(&config.suppress);
let suppress_fd = Self::yaml_to_memfd(&suppress)?; let suppress_fd = Self::yaml_to_memfd(&suppress)?;
@ -73,7 +74,9 @@ impl SuricataEngine {
.spawn(|| { .spawn(|| {
let log_path = Path::new(SURICATA_LOG); let log_path = Path::new(SURICATA_LOG);
for _ in 0..100 { for _ in 0..100 {
if log_path.exists() { break; } if log_path.exists() {
break;
}
thread::sleep(Duration::from_millis(100)); thread::sleep(Duration::from_millis(100));
} }
let file = match std::fs::File::open(log_path) { let file = match std::fs::File::open(log_path) {
@ -96,17 +99,20 @@ impl SuricataEngine {
if inotify_fd >= 0 { if inotify_fd >= 0 {
// Block until Suricata writes more data. // Block until Suricata writes more data.
let mut buf = [0u8; 64]; let mut buf = [0u8; 64];
let n = unsafe { let n =
libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) unsafe { libc::read(inotify_fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
}; if n <= 0 {
if n <= 0 { break; } break;
}
} else { } else {
thread::sleep(Duration::from_millis(50)); thread::sleep(Duration::from_millis(50));
} }
} }
Ok(_) => { Ok(_) => {
let trimmed = line.trim_end().to_string(); let trimmed = line.trim_end().to_string();
if trimmed.is_empty() { continue; } if trimmed.is_empty() {
continue;
}
let lower = trimmed.to_ascii_lowercase(); let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("error") || lower.starts_with("critical") { if lower.starts_with("error") || lower.starts_with("critical") {
log!(SuricataLog::ProcessError { line: trimmed }); log!(SuricataLog::ProcessError { line: trimmed });
@ -144,7 +150,9 @@ impl SuricataEngine {
sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be(); sll.sll_protocol = (libc::ETH_P_ALL as u16).to_be();
sll.sll_ifindex = ifindex as i32; 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() { while let Ok(data) = rx.recv() {
unsafe { unsafe {
@ -166,7 +174,10 @@ impl SuricataEngine {
log!(SuricataLog::Initialized); 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. */ /* 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 { 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) { let threading = match (config.worker_cpu_set, config.management_cpu) {
(None, None) => r#"threading: (None, None) => r#"threading:
set-cpu-affinity: no"#.to_string(), set-cpu-affinity: no"#
.to_string(),
(worker, mgmt) => { (worker, mgmt) => {
let mgmt_cpu = mgmt.unwrap_or(0); let mgmt_cpu = mgmt.unwrap_or(0);
let worker_block = match worker { let worker_block = match worker {
Some([start, end]) => format!(r#" - worker-cpu-set: Some([start, end]) => format!(
r#" - worker-cpu-set:
cpu: [ "{start}-{end}" ] cpu: [ "{start}-{end}" ]
mode: "balanced""#), mode: "balanced""#
),
None => String::new(), None => String::new(),
}; };
format!(r#"threading: format!(
r#"threading:
set-cpu-affinity: yes set-cpu-affinity: yes
cpu-affinity: cpu-affinity:
- management-cpu-set: - management-cpu-set:
cpu: [ {mgmt_cpu} ] cpu: [ {mgmt_cpu} ]
{worker_block}"#) {worker_block}"#
)
} }
}; };
format!(r#"%YAML 1.1 format!(
r#"%YAML 1.1
--- ---
vars: vars:
address-groups: address-groups:
@ -298,23 +315,21 @@ legacy:
host-mode: sniffer-only host-mode: sniffer-only
"#, "#,
home_net = config.home_net, home_net = config.home_net,
rule_path = rule_path, rule_path = rule_path,
eve_socket = eve_socket, eve_socket = eve_socket,
suppress_path = suppress_path, suppress_path = suppress_path,
log_path = SURICATA_LOG, log_path = SURICATA_LOG,
iface = MIRROR_PEER, iface = MIRROR_PEER,
threads = config.af_packet_threads, threads = config.af_packet_threads,
ring_size = config.af_packet_ring_size, ring_size = config.af_packet_ring_size,
block_size = config.af_packet_block_size, block_size = config.af_packet_block_size,
threading = threading, threading = threading,
) )
} }
fn yaml_to_memfd(yaml: &str) -> Result<i32, SuricataError> { fn yaml_to_memfd(yaml: &str) -> Result<i32, SuricataError> {
let fd = unsafe { let fd = unsafe { libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0) };
libc::memfd_create(b"suricata-config\0".as_ptr() as *const libc::c_char, 0)
};
if fd < 0 { if fd < 0 {
let errno = unsafe { *libc::__errno_location() }; let errno = unsafe { *libc::__errno_location() };
return Err(SuricataError::MirrorSetupFailed { return Err(SuricataError::MirrorSetupFailed {
@ -322,9 +337,7 @@ host-mode: sniffer-only
}); });
} }
let bytes = yaml.as_bytes(); let bytes = yaml.as_bytes();
let written = unsafe { let written = unsafe { libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len())
};
if written < 0 { if written < 0 {
unsafe { libc::close(fd) }; unsafe { libc::close(fd) };
let errno = unsafe { *libc::__errno_location() }; let errno = unsafe { *libc::__errno_location() };
@ -351,7 +364,10 @@ host-mode: sniffer-only
.map_err(|e| SuricataError::MirrorSetupFailed { reason: e.to_string() })?; .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(()) Ok(())
} }
@ -366,13 +382,7 @@ host-mode: sniffer-only
} }
fn open_raw_socket() -> Result<i32, SuricataError> { fn open_raw_socket() -> Result<i32, SuricataError> {
let fd = unsafe { let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, (libc::ETH_P_ALL as u16).to_be() as i32) };
libc::socket(
libc::AF_PACKET,
libc::SOCK_RAW,
(libc::ETH_P_ALL as u16).to_be() as i32,
)
};
if fd < 0 { if fd < 0 {
let errno = unsafe { *libc::__errno_location() }; let errno = unsafe { *libc::__errno_location() };
return Err(SuricataError::MirrorSetupFailed { return Err(SuricataError::MirrorSetupFailed {
@ -390,4 +400,4 @@ impl Drop for SuricataEngine {
} }
let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output(); let _ = Command::new("ip").args(["link", "del", MIRROR_IFACE]).output();
} }
} }

View File

@ -1,3 +1,3 @@
mod output;
pub mod engine; pub mod engine;
mod output;
pub use engine::SuricataEngine; pub use engine::SuricataEngine;

View File

@ -78,9 +78,7 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc<FusionE
if event.event_type != "alert" { if event.event_type != "alert" {
continue; continue;
} }
let (Some(alert), Some(src_ip), Some(dst_ip)) = let (Some(alert), Some(src_ip), Some(dst_ip)) = (event.alert, event.src_ip, event.dest_ip) else {
(event.alert, event.src_ip, event.dest_ip)
else {
continue; continue;
}; };
@ -103,4 +101,4 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc<FusionE
fusion.record_rule(&m); fusion.record_rule(&m);
} }
} }

View File

@ -1,18 +1,18 @@
mod core; mod core;
mod detection;
mod model; mod model;
mod utils; mod utils;
mod web; mod web;
mod detection;
use crate::core::system::System; use crate::core::system::System;
use crate::model::error::Error; use crate::model::error::Error;
#[actix_web::main] #[tokio::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
let mut system = System::new().await?; let mut system = System::new().await?;
system.run().await?; system.run().await?;
system.terminate().await?; system.terminate().await?;
drop(system); drop(system);
std::process::exit(0); std::process::exit(0);
} }

View File

@ -21,9 +21,15 @@ pub struct SuricataConfig {
pub suppress: Vec<String>, pub suppress: Vec<String>,
} }
fn default_af_threads() -> String { "auto".to_string() } fn default_af_threads() -> String {
fn default_af_ring_size() -> u32 { 2048 } "auto".to_string()
fn default_af_block_size() -> u32 { 131072 } }
fn default_af_ring_size() -> u32 {
2048
}
fn default_af_block_size() -> u32 {
131072
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config { pub struct Config {
@ -61,6 +67,25 @@ pub struct Config {
/// Suricata rule engine config. If absent, the rule engine is disabled. /// Suricata rule engine config. If absent, the rule engine is disabled.
pub suricata: Option<SuricataConfig>, 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 { fn default_fusion_mode() -> String {
@ -73,4 +98,4 @@ fn default_fusion_window_secs() -> u64 {
fn default_ae_threshold_method() -> String { fn default_ae_threshold_method() -> String {
"95".to_string() "95".to_string()
} }

View 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,
}
}

View File

@ -70,4 +70,3 @@ traceable! {
FlowMapKeyMissing { direction: String, flow_direction: String, time_type: String } => tracing::Level::ERROR, FlowMapKeyMissing { direction: String, flow_direction: String, time_type: String } => tracing::Level::ERROR,
} }
} }

View File

@ -8,7 +8,8 @@ traceable! {
#[error("Http Server panic")] #[error("Http Server panic")]
ServerPanic => tracing::Level::ERROR, ServerPanic => tracing::Level::ERROR,
#[error("WebSocket error")] #[no_source]
WebSocketError => tracing::Level::ERROR, #[error("WebSocket error: {msg}")]
WebSocketError { msg: String } => tracing::Level::ERROR,
} }
} }

View File

@ -26,4 +26,4 @@ traceable! {
#[error("Failed to create traffic log file '{path}': {reason}")] #[error("Failed to create traffic log file '{path}': {reason}")]
TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR, TrafficLogCreateError { path: String, reason: String } => tracing::Level::ERROR,
} }
} }

View File

@ -48,4 +48,4 @@ traceable! {
#[error("Failed to write CSV row: {reason}")] #[error("Failed to write CSV row: {reason}")]
TrafficLogWriteFailed { reason: String } => tracing::Level::WARN, TrafficLogWriteFailed { reason: String } => tracing::Level::WARN,
} }
} }

View File

@ -1,13 +1,15 @@
pub mod auth;
pub mod ebpf; pub mod ebpf;
pub mod http; pub mod http;
pub mod io; pub mod io;
pub mod misc; pub mod misc;
pub mod ml; pub mod ml;
pub mod system;
pub mod suricata; pub mod suricata;
pub mod system;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::model::error::auth::AuthError;
use crate::model::error::ebpf::EbpfError; use crate::model::error::ebpf::EbpfError;
use crate::model::error::http::HttpError; use crate::model::error::http::HttpError;
use crate::model::error::io::IOError; use crate::model::error::io::IOError;
@ -18,6 +20,8 @@ use crate::model::error::system::SystemError;
#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)] #[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)]
pub enum Error { pub enum Error {
#[error("{0}")]
Auth(AuthError),
#[error("{0}")] #[error("{0}")]
Ebpf(EbpfError), Ebpf(EbpfError),
#[error("{0}")] #[error("{0}")]
@ -34,6 +38,12 @@ pub enum Error {
System(SystemError), System(SystemError),
} }
impl From<AuthError> for Error {
fn from(error: AuthError) -> Self {
Self::Auth(error)
}
}
impl From<EbpfError> for Error { impl From<EbpfError> for Error {
fn from(error: EbpfError) -> Self { fn from(error: EbpfError) -> Self {
Self::Ebpf(error) Self::Ebpf(error)
@ -74,4 +84,4 @@ impl From<SuricataError> for Error {
fn from(error: SuricataError) -> Self { fn from(error: SuricataError) -> Self {
Self::Suricata(error) Self::Suricata(error)
} }
} }

View File

@ -1,5 +1,5 @@
use serde::{Deserialize, Serialize};
use common::model::flow_stats::FlowStats; use common::model::flow_stats::FlowStats;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct FlowStatsWithGeo { pub struct FlowStatsWithGeo {

View File

@ -37,17 +37,11 @@ impl NativeConvert for AddrPortV4 {
type Native = SocketAddrV4; type Native = SocketAddrV4;
fn into_native(self) -> Self::Native { fn into_native(self) -> Self::Native {
SocketAddrV4::new( SocketAddrV4::new(Ipv4Addr::from(u32::from_be(self.ip())), self.port())
Ipv4Addr::from(u32::from_be(self.ip())),
self.port()
)
} }
fn from_native(native: Self::Native) -> Self { fn from_native(native: Self::Native) -> Self {
AddrPortV4::new( AddrPortV4::new(native.ip().to_bits().to_be(), native.port())
native.ip().to_bits().to_be(),
native.port()
)
} }
} }
@ -55,18 +49,10 @@ impl NativeConvert for AddrPortV6 {
type Native = SocketAddrV6; type Native = SocketAddrV6;
fn into_native(self) -> Self::Native { fn into_native(self) -> Self::Native {
SocketAddrV6::new( SocketAddrV6::new(Ipv6Addr::from(u128::from_be(self.ip())), self.port(), 0, 0)
Ipv6Addr::from(u128::from_be(self.ip())),
self.port(),
0,
0
)
} }
fn from_native(native: Self::Native) -> Self { fn from_native(native: Self::Native) -> Self {
AddrPortV6::new( AddrPortV6::new(native.ip().to_bits().to_be(), native.port())
native.ip().to_bits().to_be(),
native.port()
)
} }
} }

View 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,
}
}

View File

@ -26,7 +26,7 @@ loggable! {
#[error("No frames available for TX")] #[error("No frames available for TX")]
NoFramesAvailable => tracing::Level::WARN, NoFramesAvailable => tracing::Level::WARN,
#[error("TX wakeup failed: {error}")] #[error("TX wakeup failed: {error}")]
TXWakeupFailed { error: String } => tracing::Level::WARN, TXWakeupFailed { error: String } => tracing::Level::WARN,
@ -51,4 +51,4 @@ loggable! {
#[error("Fill queue incomplete: produced {produced}, expected {expected}")] #[error("Fill queue incomplete: produced {produced}, expected {expected}")]
FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN, FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN,
} }
} }

View File

@ -79,4 +79,4 @@ loggable! {
WindowDebug { src: String, pad: usize, window_size: usize, ae_score: f32, rows: String } => tracing::Level::DEBUG, WindowDebug { src: String, pad: usize, window_size: usize, ae_score: f32, rows: String } => tracing::Level::DEBUG,
} }
} }

View File

@ -1,7 +1,8 @@
pub mod auth;
pub mod ebpf; pub mod ebpf;
pub mod http;
pub mod ml;
pub mod system;
pub mod misc;
pub mod health; pub mod health;
pub mod http;
pub mod misc;
pub mod ml;
pub mod suricata; pub mod suricata;
pub mod system;

View File

@ -33,4 +33,4 @@ loggable! {
#[error("[suricata] {line}")] #[error("[suricata] {line}")]
ProcessError { line: String } => tracing::Level::ERROR, ProcessError { line: String } => tracing::Level::ERROR,
} }
} }

View File

@ -28,4 +28,4 @@ loggable! {
TrafficLoggingEnabled { path: String } => tracing::Level::INFO, TrafficLoggingEnabled { path: String } => tracing::Level::INFO,
} }
} }

View File

@ -236,4 +236,4 @@ impl UnifiedAlert {
rule_msg: Some(m.msg.clone()), rule_msg: Some(m.msg.clone()),
} }
} }
} }

View File

@ -2,10 +2,10 @@ pub mod config;
pub mod direction; pub mod direction;
pub mod error; pub mod error;
pub mod geo_stats; pub mod geo_stats;
pub mod health;
pub mod ip_address; pub mod ip_address;
pub mod list_type; pub mod list_type;
pub mod log; pub mod log;
pub mod time_type;
pub mod ml_detection; pub mod ml_detection;
pub mod health;
pub mod rule_detection; pub mod rule_detection;
pub mod time_type;

View File

@ -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 { pub fn is_private_ip(ip: &IpAddr) -> bool {
match ip { match ip {
IpAddr::V4(v4) => { IpAddr::V4(v4) => v4.is_private() || v4.is_loopback() || v4.is_link_local() || v4.is_broadcast(),
v4.is_private()
|| v4.is_loopback()
|| v4.is_link_local()
|| v4.is_broadcast()
}
IpAddr::V6(v6) => { IpAddr::V6(v6) => {
v6.is_loopback() v6.is_loopback()
|| v6.is_unique_local() // fc00::/7 || v6.is_unique_local() // fc00::/7
@ -26,4 +21,4 @@ pub fn is_private_ip(ip: &IpAddr) -> bool {
|| v6.is_multicast() || v6.is_multicast()
} }
} }
} }

View File

@ -1,20 +1,20 @@
use std::fs; use std::fs;
use tracing::Level; use tracing::Level;
use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::filter::EnvFilter; use tracing_subscriber::filter::EnvFilter;
use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use crate::model::error::io::IOError;
use crate::model::error::Error; use crate::model::error::Error;
use crate::model::error::io::IOError;
pub struct Logging; pub struct Logging;
impl Logging { impl Logging {
pub fn initialize() -> Result<(), Error> { pub fn initialize() -> Result<(), Error> {
let log_directory = "logs"; let log_directory = "logs";
fs::create_dir_all(log_directory) fs::create_dir_all(log_directory).map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
.map_err(|err| IOError::CreateDirectoryFailed(log_directory, err))?;
let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "Mantis"); let file_appender = RollingFileAppender::new(Rotation::DAILY, log_directory, "Mantis");

View File

@ -1,7 +1,7 @@
pub mod logging;
pub mod static_files;
pub mod boot_time; pub mod boot_time;
pub mod packet_parser;
pub mod cpu_affinity; pub mod cpu_affinity;
pub mod logging;
pub mod packet_parser;
pub mod static_files;
pub mod ip_address; pub mod ip_address;

View File

@ -184,4 +184,4 @@ pub fn format_ipv6(addr: u128) -> String {
bytes[14], bytes[14],
bytes[15] bytes[15]
) )
} }

105
mantis/src/web/api/auth.rs Normal file
View 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
}

View File

@ -1,93 +1,89 @@
use std::net::{SocketAddrV4, SocketAddrV6}; 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::direction::FlowDirection;
use crate::model::list_type::ListType; use crate::model::list_type::ListType;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/access_control") Router::new()
.service(get_ipv4_list) .route(
.service(get_ipv6_list) "/ipv4/{direction}/{list_type}",
.service(add_ipv4_list) get(get_ipv4_list).put(add_ipv4_list).delete(remove_ipv4_list),
.service(add_ipv6_list) )
.service(remove_ipv4_list) .route(
.service(remove_ipv6_list) "/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( async fn get_ipv4_list(
path: web::Path<(FlowDirection, ListType)>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
access_control: web::Data<AccessControl>, State(state): State<AppState>,
) -> impl Responder { ) -> impl IntoResponse {
let (direction, list_type) = path.into_inner(); Json(state.access_control.get_ipv4_list(direction, list_type).await)
let list = access_control.get_ipv4_list(direction, list_type).await;
HttpResponse::Ok().json(list)
} }
#[get("/ipv6/{direction}/{list_type}")]
async fn get_ipv6_list( async fn get_ipv6_list(
path: web::Path<(FlowDirection, ListType)>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
access_control: web::Data<AccessControl>, State(state): State<AppState>,
) -> impl Responder { ) -> impl IntoResponse {
let (direction, list_type) = path.into_inner(); Json(state.access_control.get_ipv6_list(direction, list_type).await)
let list = access_control.get_ipv6_list(direction, list_type).await;
HttpResponse::Ok().json(list)
} }
#[put("/ipv4/{direction}/{list_type}")]
async fn add_ipv4_list( async fn add_ipv4_list(
address: web::Json<SocketAddrV4>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
path: web::Path<(FlowDirection, ListType)>, State(state): State<AppState>,
access_control: web::Data<AccessControl>, Json(address): Json<SocketAddrV4>,
) -> impl Responder { ) -> impl IntoResponse {
let address = address.into_inner(); match state.access_control.add_ipv4_list(direction, list_type, address).await {
let (direction, list_type) = path.into_inner(); Ok(_) => StatusCode::OK.into_response(),
match access_control.add_ipv4_list(direction, list_type, address).await { Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(_) => HttpResponse::Ok().finish(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[put("/ipv6/{direction}/{list_type}")]
async fn add_ipv6_list( async fn add_ipv6_list(
address: web::Json<SocketAddrV6>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
path: web::Path<(FlowDirection, ListType)>, State(state): State<AppState>,
access_control: web::Data<AccessControl>, Json(address): Json<SocketAddrV6>,
) -> impl Responder { ) -> impl IntoResponse {
let address = address.into_inner(); match state.access_control.add_ipv6_list(direction, list_type, address).await {
let (direction, list_type) = path.into_inner(); Ok(_) => StatusCode::OK.into_response(),
match access_control.add_ipv6_list(direction, list_type, address).await { Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Ok(_) => HttpResponse::Ok().finish(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv4/{direction}/{list_type}")]
async fn remove_ipv4_list( async fn remove_ipv4_list(
address: web::Json<SocketAddrV4>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
path: web::Path<(FlowDirection, ListType)>, State(state): State<AppState>,
access_control: web::Data<AccessControl>, Json(address): Json<SocketAddrV4>,
) -> impl Responder { ) -> impl IntoResponse {
let address = address.into_inner(); match state
let (direction, list_type) = path.into_inner(); .access_control
match access_control.remove_ipv4_list(direction, list_type, address).await { .remove_ipv4_list(direction, list_type, address)
Ok(_) => HttpResponse::Ok().finish(), .await
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), {
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( async fn remove_ipv6_list(
address: web::Json<SocketAddrV6>, Path((direction, list_type)): Path<(FlowDirection, ListType)>,
path: web::Path<(FlowDirection, ListType)>, State(state): State<AppState>,
access_control: web::Data<AccessControl>, Json(address): Json<SocketAddrV6>,
) -> impl Responder { ) -> impl IntoResponse {
let address = address.into_inner(); match state
let (direction, list_type) = path.into_inner(); .access_control
match access_control.remove_ipv6_list(direction, list_type, address).await { .remove_ipv6_list(direction, list_type, address)
Ok(_) => HttpResponse::Ok().finish(), .await
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), {
Ok(_) => StatusCode::OK.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }

View File

@ -2,11 +2,13 @@ pub mod access_control;
pub mod service; pub mod service;
pub mod statistics; pub mod statistics;
use actix_web::{web, Scope}; use axum::Router;
pub fn initialize() -> Scope { use crate::core::app_state::AppState;
web::scope("/ebpf")
.service(access_control::initialize()) pub fn router() -> Router<AppState> {
.service(service::initialize()) Router::new()
.service(statistics::initialize()) .nest("/access_control", access_control::router())
.nest("/service", service::router())
.nest("/statistics", statistics::router())
} }

View File

@ -1,251 +1,243 @@
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; 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 common::model::http_method::HttpMethod;
use crate::core::ebpf::service::Service; use crate::core::app_state::AppState;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/service") Router::new()
.service(get_ipv4_http_service) .route(
.service(get_ipv6_http_service) "/ipv4/http_service",
.service(add_ipv4_http_service) get(get_ipv4_http_service)
.service(add_ipv6_http_service) .put(add_ipv4_http_service)
.service(remove_ipv4_http_service) .delete(remove_ipv4_http_service),
.service(remove_ipv6_http_service) )
.service(is_ssh_white_list_enable) .route(
.service(enable_ssh_white_list) "/ipv6/http_service",
.service(disable_ssh_white_list) get(get_ipv6_http_service)
.service(get_ipv4_ssh_service) .put(add_ipv6_http_service)
.service(get_ipv6_ssh_service) .delete(remove_ipv6_http_service),
.service(add_ipv4_ssh_service) )
.service(add_ipv6_ssh_service) .route("/ssh_white_list", get(is_ssh_white_list_enable))
.service(remove_ipv4_ssh_service) .route("/ssh_white_list/enable", post(enable_ssh_white_list))
.service(remove_ipv6_ssh_service) .route("/ssh_white_list/disable", post(disable_ssh_white_list))
.service(get_ipv4_ssh_white_list) .route(
.service(get_ipv6_ssh_white_list) "/ipv4/ssh_service",
.service(add_ipv4_ssh_white_list) get(get_ipv4_ssh_service)
.service(add_ipv6_ssh_white_list) .put(add_ipv4_ssh_service)
.service(remove_ipv4_ssh_white_list) .delete(remove_ipv4_ssh_service),
.service(remove_ipv6_ssh_white_list) )
.service(get_ipv4_ssh_black_list) .route(
.service(get_ipv6_ssh_black_list) "/ipv6/ssh_service",
.service(add_ipv4_ssh_black_list) get(get_ipv6_ssh_service)
.service(add_ipv6_ssh_black_list) .put(add_ipv6_ssh_service)
.service(remove_ipv4_ssh_black_list) .delete(remove_ipv6_ssh_service),
.service(remove_ipv6_ssh_black_list) )
.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(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv4_http_service(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv4_http_service().await)
let list = service.get_ipv4_http_service().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[get("/ipv6/http_service")] async fn get_ipv6_http_service(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv6_http_service(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv6_http_service().await)
let list = service.get_ipv6_http_service().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[put("/ipv4/http_service")]
async fn add_ipv4_http_service( async fn add_ipv4_http_service(
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>, State(state): State<AppState>,
service: web::Data<Service>, Json(payload): Json<(SocketAddrV4, Vec<HttpMethod>)>,
) -> impl Responder { ) -> impl IntoResponse {
let (addr, methods) = payload.into_inner(); let (addr, methods) = payload;
match service.add_ipv4_http_service(addr, methods).await { match state.service.add_ipv4_http_service(addr, methods).await {
Ok(_) => HttpResponse::Ok().finish(), Ok(_) => StatusCode::OK.into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[put("/ipv6/http_service")]
async fn add_ipv6_http_service( async fn add_ipv6_http_service(
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>, State(state): State<AppState>,
service: web::Data<Service>, Json(payload): Json<(SocketAddrV6, Vec<HttpMethod>)>,
) -> impl Responder { ) -> impl IntoResponse {
let (addr, methods) = payload.into_inner(); let (addr, methods) = payload;
match service.add_ipv6_http_service(addr, methods).await { match state.service.add_ipv6_http_service(addr, methods).await {
Ok(_) => HttpResponse::Ok().finish(), Ok(_) => StatusCode::OK.into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[delete("/ipv4/http_service")]
async fn remove_ipv4_http_service( async fn remove_ipv4_http_service(
payload: web::Json<(SocketAddrV4, Vec<HttpMethod>)>, State(state): State<AppState>,
service: web::Data<Service>, Json(payload): Json<(SocketAddrV4, Vec<HttpMethod>)>,
) -> impl Responder { ) -> impl IntoResponse {
let (addr, methods) = payload.into_inner(); let (addr, methods) = payload;
match service.remove_ipv4_http_service(addr, methods).await { match state.service.remove_ipv4_http_service(addr, methods).await {
Ok(_) => HttpResponse::Ok().finish(), Ok(_) => StatusCode::OK.into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[delete("/ipv6/http_service")]
async fn remove_ipv6_http_service( async fn remove_ipv6_http_service(
payload: web::Json<(SocketAddrV6, Vec<HttpMethod>)>, State(state): State<AppState>,
service: web::Data<Service>, Json(payload): Json<(SocketAddrV6, Vec<HttpMethod>)>,
) -> impl Responder { ) -> impl IntoResponse {
let (addr, methods) = payload.into_inner(); let (addr, methods) = payload;
match service.remove_ipv6_http_service(addr, methods).await { match state.service.remove_ipv6_http_service(addr, methods).await {
Ok(_) => HttpResponse::Ok().finish(), Ok(_) => StatusCode::OK.into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
#[get("/ssh_white_list")] async fn is_ssh_white_list_enable(State(state): State<AppState>) -> impl IntoResponse {
async fn is_ssh_white_list_enable(service: web::Data<Service>) -> impl Responder { Json(state.service.is_ssh_white_list_enable().await)
let enabled = service.is_ssh_white_list_enable().await;
HttpResponse::Ok().json(enabled)
} }
#[post("/ssh_white_list/enable")] async fn enable_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
async fn enable_ssh_white_list(service: web::Data<Service>) -> impl Responder { match state.service.enable_ssh_white_list().await {
match service.enable_ssh_white_list().await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[post("/ssh_white_list/disable")] async fn disable_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
async fn disable_ssh_white_list(service: web::Data<Service>) -> impl Responder { match state.service.disable_ssh_white_list().await {
match service.disable_ssh_white_list().await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[get("/ipv4/ssh_service")] async fn get_ipv4_ssh_service(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv4_ssh_service(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv4_ssh_service().await)
let list = service.get_ipv4_ssh_service().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[get("/ipv6/ssh_service")] async fn get_ipv6_ssh_service(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv6_ssh_service(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv6_ssh_service().await)
let list = service.get_ipv6_ssh_service().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[put("/ipv4/ssh_service")] async fn add_ipv4_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV4>) -> impl IntoResponse {
async fn add_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv4_ssh_service(addr).await {
match service.add_ipv4_ssh_service(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[put("/ipv6/ssh_service")] async fn add_ipv6_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV6>) -> impl IntoResponse {
async fn add_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv6_ssh_service(addr).await {
match service.add_ipv6_ssh_service(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv4/ssh_service")] async fn remove_ipv4_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV4>) -> impl IntoResponse {
async fn remove_ipv4_ssh_service(ip_addr: web::Json<SocketAddrV4>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv4_ssh_service(addr).await {
match service.remove_ipv4_ssh_service(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv6/ssh_service")] async fn remove_ipv6_ssh_service(State(state): State<AppState>, Json(addr): Json<SocketAddrV6>) -> impl IntoResponse {
async fn remove_ipv6_ssh_service(ip_addr: web::Json<SocketAddrV6>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv6_ssh_service(addr).await {
match service.remove_ipv6_ssh_service(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[get("/ipv4/ssh_white_list")] async fn get_ipv4_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv4_ssh_white_list(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv4_ssh_white_list().await)
let list = service.get_ipv4_ssh_white_list().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[get("/ipv6/ssh_white_list")] async fn get_ipv6_ssh_white_list(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv6_ssh_white_list(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv6_ssh_white_list().await)
let list = service.get_ipv6_ssh_white_list().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[put("/ipv4/ssh_white_list")] async fn add_ipv4_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
async fn add_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv4_ssh_white_list(addr).await {
match service.add_ipv4_ssh_white_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[put("/ipv6/ssh_white_list")] async fn add_ipv6_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
async fn add_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv6_ssh_white_list(addr).await {
match service.add_ipv6_ssh_white_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv4/ssh_white_list")] async fn remove_ipv4_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
async fn remove_ipv4_ssh_white_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv4_ssh_white_list(addr).await {
match service.remove_ipv4_ssh_white_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv6/ssh_white_list")] async fn remove_ipv6_ssh_white_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
async fn remove_ipv6_ssh_white_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv6_ssh_white_list(addr).await {
match service.remove_ipv6_ssh_white_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[get("/ipv4/ssh_black_list")] async fn get_ipv4_ssh_black_list(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv4_ssh_black_list(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv4_ssh_black_list().await)
let list = service.get_ipv4_ssh_black_list().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[get("/ipv6/ssh_black_list")] async fn get_ipv6_ssh_black_list(State(state): State<AppState>) -> impl IntoResponse {
async fn get_ipv6_ssh_black_list(service: web::Data<Service>) -> impl Responder { Json(state.service.get_ipv6_ssh_black_list().await)
let list = service.get_ipv6_ssh_black_list().await;
HttpResponse::Ok().json(web::Json(list))
} }
#[put("/ipv4/ssh_black_list")] async fn add_ipv4_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
async fn add_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv4_ssh_black_list(addr).await {
match service.add_ipv4_ssh_black_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[put("/ipv6/ssh_black_list")] async fn add_ipv6_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
async fn add_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder { match state.service.add_ipv6_ssh_black_list(addr).await {
match service.add_ipv6_ssh_black_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv4/ssh_black_list")] async fn remove_ipv4_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv4Addr>) -> impl IntoResponse {
async fn remove_ipv4_ssh_black_list(ip_addr: web::Json<Ipv4Addr>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv4_ssh_black_list(addr).await {
match service.remove_ipv4_ssh_black_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }
#[delete("/ipv6/ssh_black_list")] async fn remove_ipv6_ssh_black_list(State(state): State<AppState>, Json(addr): Json<Ipv6Addr>) -> impl IntoResponse {
async fn remove_ipv6_ssh_black_list(ip_addr: web::Json<Ipv6Addr>, service: web::Data<Service>) -> impl Responder { match state.service.remove_ipv6_ssh_black_list(addr).await {
match service.remove_ipv6_ssh_black_list(ip_addr.into_inner()).await { Ok(_) => StatusCode::OK.into_response(),
Ok(_) => HttpResponse::Ok().finish(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
} }
} }

View File

@ -1,76 +1,76 @@
use std::sync::Arc; use axum::extract::ws::WebSocketUpgrade;
use axum::extract::{Path, State};
use actix_web::{get, web, HttpRequest, HttpResponse, Responder, Scope}; use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::{Json, Router};
use macros::log; use macros::log;
use crate::core::ebpf::statistics::Statistics; use crate::core::app_state::AppState;
use crate::core::infrastructure::app_config::AppConfig;
use crate::model::direction::{Direction, FlowDirection}; use crate::model::direction::{Direction, FlowDirection};
use crate::model::time_type::TimeType; use crate::model::time_type::TimeType;
use crate::web::websocket::flow_websocket; use crate::web::websocket::flow_websocket;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/statistics") Router::new()
.service(get_ipv4_flow) .route("/get/ipv4/{direction}/{flow_direction}/{time_type}", get(get_ipv4_flow))
.service(get_ipv6_flow) .route("/get/ipv6/{direction}/{flow_direction}/{time_type}", get(get_ipv6_flow))
.service(websocket_ipv4) .route(
.service(websocket_ipv6) "/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( async fn get_ipv4_flow(
path: web::Path<(Direction, FlowDirection, TimeType)>, Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
statistics: web::Data<Arc<Statistics>>, State(state): State<AppState>,
) -> impl Responder { ) -> impl IntoResponse {
let (direction, flow_direction, time_type) = path.into_inner(); match state
match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await { .statistics
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)), .get_ipv4_flow_data(direction, flow_direction, time_type)
.await
{
Ok(data) => Json(data).into_response(),
Err(e) => { Err(e) => {
log!(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( async fn get_ipv6_flow(
path: web::Path<(Direction, FlowDirection, TimeType)>, Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
statistics: web::Data<Arc<Statistics>>, State(state): State<AppState>,
) -> impl Responder { ) -> impl IntoResponse {
let (direction, flow_direction, time_type) = path.into_inner(); match state
match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await { .statistics
Ok(flow_data) => HttpResponse::Ok().json(web::Json(flow_data)), .get_ipv6_flow_data(direction, flow_direction, time_type)
.await
{
Ok(data) => Json(data).into_response(),
Err(e) => { Err(e) => {
log!(e); log!(e);
HttpResponse::InternalServerError().finish() StatusCode::INTERNAL_SERVER_ERROR.into_response()
} }
} }
} }
#[get("/websocket/ipv4/{direction}/{flow_direction}/{time_type}")]
async fn websocket_ipv4( async fn websocket_ipv4(
req: HttpRequest, Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
stream: web::Payload, State(state): State<AppState>,
path: web::Path<(Direction, FlowDirection, TimeType)>, ws: WebSocketUpgrade,
app_config: web::Data<AppConfig>, ) -> impl IntoResponse {
statistics: web::Data<Statistics>, ws.on_upgrade(move |socket| flow_websocket::handle_ipv4_flow(socket, state, direction, flow_direction, time_type))
) -> 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)),
}
} }
#[get("/websocket/ipv6/{direction}/{flow_direction}/{time_type}")]
async fn websocket_ipv6( async fn websocket_ipv6(
req: HttpRequest, Path((direction, flow_direction, time_type)): Path<(Direction, FlowDirection, TimeType)>,
stream: web::Payload, State(state): State<AppState>,
path: web::Path<(Direction, FlowDirection, TimeType)>, ws: WebSocketUpgrade,
app_config: web::Data<AppConfig>, ) -> impl IntoResponse {
statistics: web::Data<Statistics>, ws.on_upgrade(move |socket| flow_websocket::handle_ipv6_flow(socket, state, direction, flow_direction, time_type))
) -> 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)),
}
} }

View File

@ -1,10 +1,10 @@
use actix_web::{HttpRequest, HttpResponse, Responder}; use axum::http::{StatusCode, Uri};
use mime_guess::from_path; use axum::response::{IntoResponse, Response};
use crate::utils::static_files::StaticFiles; use crate::utils::static_files::StaticFiles;
pub async fn default_route(req: HttpRequest) -> impl Responder { pub async fn default_route(uri: Uri) -> Response {
let request_path = req.path(); let request_path = uri.path();
let file_system_path = if request_path == "/" { let file_system_path = if request_path == "/" {
"web/index.html".to_string() "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) { if let Some(content) = StaticFiles::get(&file_system_path) {
let mime_type = from_path(&file_system_path).first_or_octet_stream(); let mime_type = mime_guess::from_path(&file_system_path).first_or_octet_stream();
return HttpResponse::Ok() return (
.content_type(mime_type.as_ref()) [("content-type", mime_type.as_ref().to_string())],
.body(content.data.into_owned()); content.data.into_owned(),
)
.into_response();
} }
let html_path = format!("{}.html", file_system_path); let html_path = format!("{}.html", file_system_path);
if let Some(content) = StaticFiles::get(&html_path) { if let Some(content) = StaticFiles::get(&html_path) {
return HttpResponse::Ok() return ([("content-type", "text/html")], content.data.into_owned()).into_response();
.content_type("text/html")
.body(content.data.into_owned());
} }
let index_path = format!("{}/index.html", file_system_path); let index_path = format!("{}/index.html", file_system_path);
if let Some(content) = StaticFiles::get(&index_path) { if let Some(content) = StaticFiles::get(&index_path) {
return HttpResponse::Ok() return ([("content-type", "text/html")], content.data.into_owned()).into_response();
.content_type("text/html")
.body(content.data.into_owned());
} }
match StaticFiles::get("web/404.html") { match StaticFiles::get("web/404.html") {
Some(page) => HttpResponse::NotFound() Some(page) => (
.content_type("text/html") StatusCode::NOT_FOUND,
.body(page.data.into_owned()), [("content-type", "text/html")],
None => HttpResponse::NotFound().body("404 Not Found"), page.data.into_owned(),
)
.into_response(),
None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
} }
} }

View File

@ -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; use crate::web::websocket::alert_websocket;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/detection") Router::new().route("/websocket/alert", get(websocket_alert))
.service(websocket_alert)
} }
#[get("/websocket/alert")] async fn websocket_alert(ws: axum::extract::ws::WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
async fn websocket_alert( let rx = state.detection_alert.subscribe();
req: HttpRequest, ws.on_upgrade(|socket| alert_websocket::handle_alert(socket, rx))
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))
}
}
} }

View File

@ -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; use crate::web::websocket::health_websocket;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/health") Router::new()
.service(get_current_metrics) .route("/metrics", get(get_current_metrics))
.service(get_health_status) .route("/status", get(get_health_status))
.service(websocket_metrics) .route("/websocket/metrics", get(websocket_metrics))
} }
#[get("/metrics")] async fn get_current_metrics(State(state): State<AppState>) -> impl IntoResponse {
async fn get_current_metrics(health: web::Data<SystemHealth>) -> impl Responder { Json(state.health.get_current_metrics().await)
let metrics = health.get_current_metrics().await;
HttpResponse::Ok().json(metrics)
} }
#[get("/status")] async fn get_health_status(State(state): State<AppState>) -> impl IntoResponse {
async fn get_health_status(health: web::Data<SystemHealth>) -> impl Responder { Json(state.health.is_system_healthy().await)
let status = health.is_system_healthy().await;
HttpResponse::Ok().json(status)
} }
#[get("/websocket/metrics")]
async fn websocket_metrics( async fn websocket_metrics(
req: HttpRequest, ws: axum::extract::ws::WebSocketUpgrade,
stream: web::Payload, State(state): State<AppState>,
health: web::Data<SystemHealth>, ) -> impl IntoResponse {
) -> impl Responder { let rx = state.health.subscribe_to_metrics();
match health_websocket::websocket_system_health(req, stream, health).await { ws.on_upgrade(|socket| health_websocket::handle_health(socket, rx))
Ok(response) => response, }
Err(err) => HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err)),
}
}

View File

@ -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; use crate::utils::boot_time::boot_time;
pub fn initialize() -> Scope { pub fn router() -> Router<AppState> {
web::scope("/misc") Router::new().route("/boot_time", get(get_boot_time))
.service(get_boot_time)
} }
#[get("/boot_time")] async fn get_boot_time() -> impl IntoResponse {
async fn get_boot_time() -> impl Responder { Json(boot_time())
let boot_time = boot_time();
HttpResponse::Ok().json(boot_time)
} }

View File

@ -1,3 +1,4 @@
pub mod auth;
pub mod control; pub mod control;
pub mod default; pub mod default;
pub mod detection_alert; pub mod detection_alert;

View 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"))
}
}

View File

@ -0,0 +1 @@
pub mod auth;

View File

@ -1,2 +1,3 @@
pub mod api; pub mod api;
pub mod middleware;
pub mod websocket; pub mod websocket;

View File

@ -1,91 +1,47 @@
use actix_web::{web, HttpRequest, HttpResponse, Result}; use axum::extract::ws::{Message, WebSocket};
use actix_ws::{handle, Message, MessageStream, Session}; use futures_util::{SinkExt, StreamExt};
use futures_util::StreamExt;
use macros::log; use macros::log;
use tokio::sync::broadcast; 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::http::HttpError;
use crate::model::error::misc::MiscError; use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog; use crate::model::log::http::HttpLog;
use crate::model::ml_detection::UnifiedAlert;
pub async fn websocket_alert( pub async fn handle_alert(socket: WebSocket, mut broadcast_rx: broadcast::Receiver<UnifiedAlert>) {
req: HttpRequest, let (mut sender, mut receiver) = socket.split();
body: web::Payload,
da: web::Data<DetectionAlert>,
) -> Result<HttpResponse> {
let (response, session, msg_stream) = handle(&req, body)?;
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 { loop {
tokio::select! { tokio::select! {
msg_result = msg_stream.next() => { msg = receiver.next() => {
if !handle_client_message(&mut session, msg_result).await { match msg {
break; Some(Ok(Message::Ping(data))) => {
} if sender.send(Message::Pong(data)).await.is_err() { break; }
},
broadcast_result = broadcast_rx.recv() => {
match broadcast_result {
Ok(alert) => {
if !send_alert(&mut session, &alert).await {
break;
}
} }
Err(broadcast::error::RecvError::Lagged(skipped)) => { Some(Ok(Message::Close(_))) | None => break,
log!(HttpLog::WebSocketLaged(skipped)); Some(Err(e)) => {
continue; log!(HttpError::WebSocketError { msg: e.to_string() });
}
Err(broadcast::error::RecvError::Closed) => {
break; break;
} }
_ => {}
} }
}, }
} result = broadcast_rx.recv() => {
} match result {
Ok(alert) => {
let _ = session.close(None).await; match serde_json::to_string(&alert) {
} Ok(json) => {
if sender.send(Message::Text(json.into())).await.is_err() { break; }
async fn handle_client_message( }
session: &mut Session, Err(e) => { log!(MiscError::SerializeError(e)); }
msg_result: Option<Result<Message, actix_ws::ProtocolError>>, }
) -> bool { }
match msg_result { Err(broadcast::error::RecvError::Lagged(n)) => {
Some(Ok(Message::Text(_))) => true, log!(HttpLog::WebSocketLaged { skipped: n });
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(), }
Some(Ok(Message::Close(reason))) => { Err(broadcast::error::RecvError::Closed) => break,
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
} }
} }
} }

View File

@ -1,192 +1,96 @@
use std::sync::Arc; use axum::extract::ws::{Message, WebSocket};
use futures_util::{SinkExt, StreamExt};
use actix_web::{web, HttpRequest, HttpResponse, Result};
use actix_ws::{handle, Message, MessageStream, Session};
use futures_util::StreamExt;
use macros::log; use macros::log;
use tokio::time::{interval, Duration}; use tokio::time::{Duration, interval};
use crate::core::ebpf::statistics::Statistics; use crate::core::app_state::AppState;
use crate::core::infrastructure::app_config::AppConfig;
use crate::model::direction::{Direction, FlowDirection}; use crate::model::direction::{Direction, FlowDirection};
use crate::model::error::http::HttpError; use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError; use crate::model::error::misc::MiscError;
use crate::model::time_type::TimeType; use crate::model::time_type::TimeType;
pub async fn websocket_ipv4_flow( pub async fn handle_ipv4_flow(
req: HttpRequest, socket: WebSocket,
body: web::Payload, state: AppState,
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,
direction: Direction, direction: Direction,
flow_direction: FlowDirection, flow_direction: FlowDirection,
time_type: TimeType, time_type: TimeType,
) { ) {
let config = app_config.config.clone(); let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
let refresh_interval = Duration::from_secs(config.refresh_interval); let (mut sender, mut receiver) = socket.split();
let mut data_interval = interval(refresh_interval); let mut data_interval = interval(refresh_interval);
loop { loop {
tokio::select! { tokio::select! {
msg_result = msg_stream.next() => { msg = receiver.next() => {
if !handle_client_message(&mut session, msg_result).await { match msg {
break; 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() => { _ = data_interval.tick() => {
if !send_ipv4_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { match state.statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
break; 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( pub async fn handle_ipv6_flow(
app_config: Arc<AppConfig>, socket: WebSocket,
statistics: Arc<Statistics>, state: AppState,
mut session: Session,
mut msg_stream: MessageStream,
direction: Direction, direction: Direction,
flow_direction: FlowDirection, flow_direction: FlowDirection,
time_type: TimeType, time_type: TimeType,
) { ) {
let config = app_config.config.clone(); let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
let refresh_interval = Duration::from_secs(config.refresh_interval); let (mut sender, mut receiver) = socket.split();
let mut data_interval = interval(refresh_interval); let mut data_interval = interval(refresh_interval);
loop { loop {
tokio::select! { tokio::select! {
msg_result = msg_stream.next() => { msg = receiver.next() => {
if !handle_client_message(&mut session, msg_result).await { match msg {
break; 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() => { _ = data_interval.tick() => {
if !send_ipv6_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await { match state.statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
break; 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
} }
} }
} }

View File

@ -1,91 +1,47 @@
use actix_web::{web, HttpRequest, HttpResponse, Result}; use axum::extract::ws::{Message, WebSocket};
use actix_ws::{handle, Message, MessageStream, Session}; use futures_util::{SinkExt, StreamExt};
use futures_util::StreamExt;
use macros::log; use macros::log;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use crate::core::infrastructure::health::SystemHealth;
use crate::model::error::http::HttpError; use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError; use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::model::health::SystemHealthMetrics; use crate::model::health::SystemHealthMetrics;
use crate::model::log::http::HttpLog;
pub async fn websocket_system_health( pub async fn handle_health(socket: WebSocket, mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>) {
req: HttpRequest, let (mut sender, mut receiver) = socket.split();
body: web::Payload,
health: web::Data<SystemHealth>,
) -> Result<HttpResponse> {
let (response, session, msg_stream) = handle(&req, body)?;
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 { loop {
tokio::select! { tokio::select! {
msg_result = msg_stream.next() => { msg = receiver.next() => {
if !handle_client_message(&mut session, msg_result).await { match msg {
break; Some(Ok(Message::Ping(data))) => {
} if sender.send(Message::Pong(data)).await.is_err() { break; }
},
broadcast_result = broadcast_rx.recv() => {
match broadcast_result {
Ok(metrics) => {
if !send_metrics(&mut session, &metrics).await {
break;
}
} }
Err(broadcast::error::RecvError::Lagged(skipped)) => { Some(Ok(Message::Close(_))) | None => break,
log!(HttpLog::WebSocketLaged(skipped)); Some(Err(e)) => {
continue; log!(HttpError::WebSocketError { msg: e.to_string() });
}
Err(broadcast::error::RecvError::Closed) => {
break; 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
}
}
}

View File

@ -1,3 +1,3 @@
pub mod alert_websocket;
pub mod flow_websocket; pub mod flow_websocket;
pub mod health_websocket; pub mod health_websocket;
pub mod alert_websocket;