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
- **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)
- Real-time control system occupancy rate
[//]: # ()
[//]: # (### Resource Overview)
### Dashboard Overview
![Dashboard](.github/images/dashboard.png)
- Real-time network traffic monitoring and visualization
- Recent traffic statistics and trend analysis
[//]: # (![Home](.github/images/index.png))
### Detailed Traffic Statistics
![Statistics](.github/images/statistics.png)
- Detailed traffic usage information per IP address
[//]: # (- Real-time control system occupancy rate)
### Network Access Control
![accessControl](.github/images/accessControl.png)
- IPv4/IPv6 whitelist and blacklist management
- Precise port-level access control
[//]: # ()
[//]: # (### Dashboard Overview)
[//]: # (![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)

1
TODO
View File

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

View File

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

View File

@ -40,6 +40,13 @@ ml_cpu = 7
ae_threshold_method = "94"
# Auth system. Remove this entire section to disable auth.
[Config.auth]
jwt_secret = "change-me-jwt-secret-must-be-32-bytes-min"
db_key = "change-me-db-key-must-be-32-bytes-min-x"
token_ttl_secs = 86400
default_admin_password = "admin"
# Suricata rule engine. Remove this entire section to disable.
[Config.suricata]
home_net = "140.130.34.0/24"

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

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

View File

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

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

View File

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

View File

@ -8,12 +8,18 @@ use std::time::SystemTime;
use cargo_metadata::{Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target};
fn main() {
let suricata_eve_socket = PathBuf::from("/").join("tmp").join("suricata-alerts.sock");
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let artifact_dir = manifest_dir.join("static").join("artifacts");
let rule_dir = manifest_dir.join("static").join("rules");
let suricata_eve_socket = PathBuf::from("/tmp").join("suricata-alerts.sock");
let db_dir = manifest_dir.join("static").join("db");
let csv_dir = manifest_dir.parent().unwrap().join("records");
let onnxruntime_dir = manifest_dir.parent().unwrap().join("onnxruntime").join("libonnxruntime.so");
let lib_dir = manifest_dir.parent().unwrap().join("lib");
let onnxruntime_dir = lib_dir.join("onnxruntime").join("libonnxruntime.so");
let ingress_edpf_dir = lib_dir.join("ebpf").join("mantis-ingress");
let egress_edpf_dir = lib_dir.join("ebpf").join("mantis-egress");
let static_web = manifest_dir.join("static").join("web");
let project_name = manifest_dir.file_name().unwrap().to_string_lossy().into_owned();
@ -25,7 +31,10 @@ fn main() {
println!("cargo:rustc-env=ARTIFACTCS_PATH={}", artifact_dir.display());
println!("cargo:rustc-env=CSV_RECORD_PATH={}", csv_dir.display());
println!("cargo:rustc-env=ONNXRUNTIME_PATH={}", onnxruntime_dir.display());
println!("cargo:rustc-env=INGRESS_PATH={}", ingress_edpf_dir.display());
println!("cargo:rustc-env=EGRESS_PATH={}", egress_edpf_dir.display());
println!("cargo:rustc-env=RULE_PATH={}", rule_dir.display());
println!("cargo:rustc-env=DB_PATH={}", db_dir.display());
println!("cargo:rustc-env=RULE_EVE_PATH={}", suricata_eve_socket.display());
for item in &[
@ -42,21 +51,19 @@ fn main() {
}
if env::var_os("SKIP_EBPF_BUILD").is_some() {
let out = PathBuf::from(env::var_os("OUT_DIR").unwrap());
for name in &["mantis-ingress", "mantis-egress"] {
let path = out.join(name);
for path in &[&ingress_edpf_dir, &egress_edpf_dir] {
if !path.exists() {
fs::write(&path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
fs::write(path, []).unwrap_or_else(|e| panic!("cannot write stub {path:?}: {e}"));
}
}
return;
}
build_ingress_ebpf();
build_egress_ebpf();
build_ingress_ebpf(&ingress_edpf_dir);
build_egress_ebpf(&egress_edpf_dir);
build_frontend(&frontend_dir, &static_web);
}
fn build_ingress_ebpf() {
fn build_ingress_ebpf(dst: &PathBuf) {
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages
.into_iter()
@ -155,14 +162,12 @@ fn build_ingress_ebpf() {
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
for (_name, binary) in executables {
let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
}
fn build_egress_ebpf() {
fn build_egress_ebpf(dst: &PathBuf) {
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages
.into_iter()
@ -262,10 +267,8 @@ fn build_egress_ebpf() {
stderr.join().map_err(std::panic::resume_unwind).unwrap();
for (name, binary) in executables {
let dst = out_dir.join(name);
let _: u64 =
fs::copy(&binary, &dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
for (_name, binary) in executables {
let _: u64 = fs::copy(&binary, dst).unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}"));
}
}

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 crate::model::direction::FlowDirection;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::Error;
use crate::model::error::ebpf::EbpfError;
use crate::model::ip_address::NativeConvert;
use crate::model::list_type::ListType;
use crate::utils::ip_address::convert_ports_to_vec;

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,7 +1,8 @@
pub mod app_config;
pub mod app_db;
pub mod detection_alert;
pub mod health;
pub mod geoip;
pub mod health;
use std::path::PathBuf;
use std::sync::Arc;
@ -13,6 +14,7 @@ use macros::log;
use tokio::sync::oneshot;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::app_db::AppDB;
use crate::core::infrastructure::detection_alert::DetectionAlert;
use crate::core::infrastructure::health::SystemHealth;
use crate::detection::fusion::{FusionEngine, FusionMode};
@ -22,8 +24,8 @@ use crate::detection::ml::feature_extractor::FlowFeatures;
use crate::detection::ml::model_loader::MLModels;
use crate::detection::ml::traffic_logger::TrafficLogger;
use crate::detection::suricata::SuricataEngine;
use crate::model::error::misc::MiscError;
use crate::model::error::Error;
use crate::model::error::misc::MiscError;
use crate::model::error::system::SystemError;
use crate::model::log::system::SystemLog;
@ -34,6 +36,7 @@ pub struct AppServices {
pub ml_models: Arc<MLModels>,
pub ml_engine: Arc<Engine>,
pub suricata_engine: Option<Arc<SuricataEngine>>,
pub app_db: Option<Arc<AppDB>>,
shutdowns: SegQueue<oneshot::Sender<()>>,
}
@ -53,12 +56,21 @@ impl AppServices {
let traffic_logger = if app_config.traffic_logging_mode {
let dir = PathBuf::from(env!("CSV_RECORD_PATH"));
let basename = app_config.traffic_log_csv_path.trim_end_matches(".csv").to_string();
let mut header = vec!["Source IP".to_string(), "Destination IP".to_string(), "Timestamp".to_string()];
let mut header = vec![
"Source IP".to_string(),
"Destination IP".to_string(),
"Timestamp".to_string(),
];
header.extend(FlowFeatures::all_feature_names_owned());
header.push("Label".to_string());
let logger = TrafficLogger::new(&dir, &basename, header)
.map_err(|e| MiscError::TrafficLogCreateError(dir.display().to_string(), e.to_string()))?;
log!(SystemLog::TrafficLoggingEnabled(format!("{}/{}-{}.csv", dir.display(), basename, Local::now().format("%Y-%m-%d"))));
log!(SystemLog::TrafficLoggingEnabled(format!(
"{}/{}-{}.csv",
dir.display(),
basename,
Local::now().format("%Y-%m-%d")
)));
Some(Arc::new(logger))
} else {
None
@ -80,7 +92,21 @@ impl AppServices {
let suricata_engine = if let Some(ref sc) = app_config.suricata {
let rule_path = PathBuf::from(env!("RULE_PATH"));
let eve_socket = PathBuf::from(env!("RULE_EVE_PATH"));
Some(SuricataEngine::start(sc, &rule_path, &eve_socket, fusion_engine.clone())?)
Some(SuricataEngine::start(
sc,
&rule_path,
&eve_socket,
fusion_engine.clone(),
)?)
} else {
None
};
let app_db = if let Some(ref auth) = app_config.auth {
let db_path = PathBuf::from(env!("DB_PATH")).join("app.db");
let db = AppDB::open(db_path, &auth.db_key)?;
db.ensure_default_admin(&auth.default_admin_password)?;
Some(Arc::new(db))
} else {
None
};
@ -92,6 +118,7 @@ impl AppServices {
ml_models,
ml_engine,
suricata_engine,
app_db,
shutdowns: SegQueue::new(),
})
}
@ -116,4 +143,4 @@ impl AppServices {
}
}
}
}
}

View File

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

View File

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

View File

@ -14,7 +14,11 @@ pub enum FusionMode {
impl FusionMode {
pub fn from_str(s: &str) -> Self {
if s.eq_ignore_ascii_case("and") { FusionMode::And } else { FusionMode::Or }
if s.eq_ignore_ascii_case("and") {
FusionMode::And
} else {
FusionMode::Or
}
}
}
@ -60,7 +64,14 @@ impl FusionEngine {
return;
}
self.alert.broadcast(UnifiedAlert::from_ml(result));
map.insert(key, FusionState { ml: Some(result.clone()), rule: None, created_at: now });
map.insert(
key,
FusionState {
ml: Some(result.clone()),
rule: None,
created_at: now,
},
);
}
FusionMode::And => {
let maybe_rule = {
@ -100,7 +111,14 @@ impl FusionEngine {
return;
}
self.alert.broadcast(UnifiedAlert::from_rule(m));
map.insert(key, FusionState { ml: None, rule: Some(m.clone()), created_at: now });
map.insert(
key,
FusionState {
ml: None,
rule: Some(m.clone()),
created_at: now,
},
);
}
FusionMode::And => {
let maybe_ml = {

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use std::collections::HashMap;
use chrono::{Utc, TimeZone};
use chrono::{TimeZone, Utc};
use super::flow_tracker::FlowData;
use crate::model::ml_detection::{ClipParams, PacketData};
@ -67,7 +68,9 @@ impl FlowFeatures {
let bwd_urg = flow.bwd_packets.iter().filter(|p| p.flags.urg).count() as f64;
let all_lengths: Vec<f64> = flow
.fwd_packets.iter().chain(flow.bwd_packets.iter())
.fwd_packets
.iter()
.chain(flow.bwd_packets.iter())
.map(|p| p.payload_length as f64)
.collect();
let (max_len, min_len, mean_len, std_len) = compute_stats(&all_lengths);
@ -75,11 +78,7 @@ impl FlowFeatures {
let fwd_bulk = &flow.fwd_bulk_state;
let bwd_bulk = &flow.bwd_bulk_state;
let fwd_header_sizes: Vec<f64> = flow
.fwd_packets
.iter()
.map(|p| p.header_length as f64)
.collect();
let fwd_header_sizes: Vec<f64> = flow.fwd_packets.iter().map(|p| p.header_length as f64).collect();
let (active_max, active_min, active_mean, active_std) =
compute_stats(&flow.active_periods.iter().map(|&x| x as f64).collect::<Vec<_>>());
@ -145,14 +144,22 @@ impl FlowFeatures {
"Avg Fwd Segment Size" | "avg_fwd_seg_size" => safe_div(flow.fwd_total_bytes as f64, fwd_count),
"Avg Bwd Segment Size" | "avg_bwd_seg_size" => safe_div(flow.bwd_total_bytes as f64, bwd_count),
"Fwd Header Length.1" | "fwd_header_length_1" => flow.fwd_header_bytes as f64,
"Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64),
"Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64),
"Fwd Avg Bytes/Bulk" | "fwd_avg_bytes_bulk" => {
safe_div(fwd_bulk.total_bytes as f64, fwd_bulk.bulk_count as f64)
}
"Fwd Avg Packets/Bulk" | "fwd_avg_pkts_bulk" => {
safe_div(fwd_bulk.total_packets as f64, fwd_bulk.bulk_count as f64)
}
"Fwd Avg Bulk Rate" | "fwd_avg_bulk_rate" => safe_div(
fwd_bulk.total_bytes as f64,
fwd_bulk.total_duration_us as f64 / 1_000_000.0,
),
"Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64),
"Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64),
"Bwd Avg Bytes/Bulk" | "bwd_avg_bytes_bulk" => {
safe_div(bwd_bulk.total_bytes as f64, bwd_bulk.bulk_count as f64)
}
"Bwd Avg Packets/Bulk" | "bwd_avg_pkts_bulk" => {
safe_div(bwd_bulk.total_packets as f64, bwd_bulk.bulk_count as f64)
}
"Bwd Avg Bulk Rate" | "bwd_avg_bulk_rate" => safe_div(
bwd_bulk.total_bytes as f64,
bwd_bulk.total_duration_us as f64 / 1_000_000.0,
@ -358,4 +365,4 @@ fn compute_flow_iats(fwd_packets: &[PacketData], bwd_packets: &[PacketData]) ->
.windows(2)
.map(|w| (w[1].timestamp_us - w[0].timestamp_us) as f64)
.collect()
}
}

View File

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

View File

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

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 config_loader;
pub mod engine;
pub mod feature_extractor;
pub mod flow_tracker;
pub mod inference;
pub mod model_loader;
pub mod traffic_logger;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
mod output;
pub mod engine;
mod output;
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" {
continue;
}
let (Some(alert), Some(src_ip), Some(dst_ip)) =
(event.alert, event.src_ip, event.dest_ip)
else {
let (Some(alert), Some(src_ip), Some(dst_ip)) = (event.alert, event.src_ip, event.dest_ip) else {
continue;
};
@ -103,4 +101,4 @@ fn handle_eve_stream(stream: std::os::unix::net::UnixStream, fusion: Arc<FusionE
fusion.record_rule(&m);
}
}
}

View File

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

View File

@ -21,9 +21,15 @@ pub struct SuricataConfig {
pub suppress: Vec<String>,
}
fn default_af_threads() -> String { "auto".to_string() }
fn default_af_ring_size() -> u32 { 2048 }
fn default_af_block_size() -> u32 { 131072 }
fn default_af_threads() -> String {
"auto".to_string()
}
fn default_af_ring_size() -> u32 {
2048
}
fn default_af_block_size() -> u32 {
131072
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Config {
@ -61,6 +67,25 @@ pub struct Config {
/// Suricata rule engine config. If absent, the rule engine is disabled.
pub suricata: Option<SuricataConfig>,
/// Auth system config. If absent, auth is disabled and all routes are public.
pub auth: Option<AuthConfig>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthConfig {
pub jwt_secret: String,
pub db_key: String,
#[serde(default = "default_token_ttl")]
pub token_ttl_secs: u64,
#[serde(default = "default_admin_password")]
pub default_admin_password: String,
}
fn default_token_ttl() -> u64 {
86400
}
fn default_admin_password() -> String {
"admin".to_string()
}
fn default_fusion_mode() -> String {
@ -73,4 +98,4 @@ fn default_fusion_window_secs() -> u64 {
fn default_ae_threshold_method() -> String {
"95".to_string()
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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")]
NoFramesAvailable => tracing::Level::WARN,
#[error("TX wakeup failed: {error}")]
TXWakeupFailed { error: String } => tracing::Level::WARN,
@ -51,4 +51,4 @@ loggable! {
#[error("Fill queue incomplete: produced {produced}, expected {expected}")]
FillQueueIncomplete { produced: usize, expected: usize } => tracing::Level::WARN,
}
}
}

View File

@ -79,4 +79,4 @@ loggable! {
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 http;
pub mod ml;
pub mod system;
pub mod misc;
pub mod health;
pub mod http;
pub mod misc;
pub mod ml;
pub mod suricata;
pub mod system;

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
pub fn initialize() -> Scope {
web::scope("/detection")
.service(websocket_alert)
pub fn router() -> Router<AppState> {
Router::new().route("/websocket/alert", get(websocket_alert))
}
#[get("/websocket/alert")]
async fn websocket_alert(
req: HttpRequest,
stream: web::Payload,
da: web::Data<DetectionAlert>,
) -> impl Responder {
match alert_websocket::websocket_alert(req, stream, da).await {
Ok(response) => response,
Err(err) => {
HttpResponse::InternalServerError().body(format!("WebSocket error: {}", err))
}
}
async fn websocket_alert(ws: axum::extract::ws::WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
let rx = state.detection_alert.subscribe();
ws.on_upgrade(|socket| alert_websocket::handle_alert(socket, rx))
}

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

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

View File

@ -1,3 +1,4 @@
pub mod auth;
pub mod control;
pub mod default;
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 middleware;
pub mod websocket;

View File

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

View File

@ -1,192 +1,96 @@
use std::sync::Arc;
use actix_web::{web, HttpRequest, HttpResponse, Result};
use actix_ws::{handle, Message, MessageStream, Session};
use futures_util::StreamExt;
use axum::extract::ws::{Message, WebSocket};
use futures_util::{SinkExt, StreamExt};
use macros::log;
use tokio::time::{interval, Duration};
use tokio::time::{Duration, interval};
use crate::core::ebpf::statistics::Statistics;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::app_state::AppState;
use crate::model::direction::{Direction, FlowDirection};
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::time_type::TimeType;
pub async fn websocket_ipv4_flow(
req: HttpRequest,
body: web::Payload,
path: web::Path<(Direction, FlowDirection, TimeType)>,
app_config: web::Data<AppConfig>,
statistics: web::Data<Statistics>,
) -> Result<HttpResponse> {
let app_config = app_config.into_inner();
let statistics = statistics.into_inner();
let (direction, flow_direction, time_type) = path.into_inner();
let (response, session, msg_stream) = handle(&req, body)?;
actix_web::rt::spawn(async move {
handle_ipv4_flow_connection(
app_config,
statistics,
session,
msg_stream,
direction,
flow_direction,
time_type,
)
.await;
});
Ok(response)
}
pub async fn websocket_ipv6_flow(
req: HttpRequest,
body: web::Payload,
path: web::Path<(Direction, FlowDirection, TimeType)>,
app_config: web::Data<AppConfig>,
statistics: web::Data<Statistics>,
) -> Result<HttpResponse> {
let app_config = app_config.into_inner();
let statistics = statistics.into_inner();
let (direction, flow_direction, time_type) = path.into_inner();
let (response, session, msg_stream) = handle(&req, body)?;
actix_web::rt::spawn(async move {
handle_ipv6_flow_connection(
app_config,
statistics,
session,
msg_stream,
direction,
flow_direction,
time_type,
)
.await;
});
Ok(response)
}
async fn handle_ipv4_flow_connection(
app_config: Arc<AppConfig>,
statistics: Arc<Statistics>,
mut session: Session,
mut msg_stream: MessageStream,
pub async fn handle_ipv4_flow(
socket: WebSocket,
state: AppState,
direction: Direction,
flow_direction: FlowDirection,
time_type: TimeType,
) {
let config = app_config.config.clone();
let refresh_interval = Duration::from_secs(config.refresh_interval);
let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
let (mut sender, mut receiver) = socket.split();
let mut data_interval = interval(refresh_interval);
loop {
tokio::select! {
msg_result = msg_stream.next() => {
if !handle_client_message(&mut session, msg_result).await {
break;
msg = receiver.next() => {
match msg {
Some(Ok(Message::Ping(data))) => {
if sender.send(Message::Pong(data)).await.is_err() { break; }
}
Some(Ok(Message::Close(_))) | None => break,
Some(Err(e)) => {
log!(HttpError::WebSocketError { msg: e.to_string() });
break;
}
_ => {}
}
},
}
_ = data_interval.tick() => {
if !send_ipv4_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await {
break;
match state.statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
Ok(data) => {
match serde_json::to_string(&data) {
Ok(json) => {
if sender.send(Message::Text(json.into())).await.is_err() { break; }
}
Err(e) => { log!(MiscError::SerializeError(e)); }
}
}
Err(e) => { log!(e); break; }
}
},
}
}
}
let _ = session.close(None).await;
}
async fn handle_ipv6_flow_connection(
app_config: Arc<AppConfig>,
statistics: Arc<Statistics>,
mut session: Session,
mut msg_stream: MessageStream,
pub async fn handle_ipv6_flow(
socket: WebSocket,
state: AppState,
direction: Direction,
flow_direction: FlowDirection,
time_type: TimeType,
) {
let config = app_config.config.clone();
let refresh_interval = Duration::from_secs(config.refresh_interval);
let refresh_interval = Duration::from_secs(state.app_config.refresh_interval);
let (mut sender, mut receiver) = socket.split();
let mut data_interval = interval(refresh_interval);
loop {
tokio::select! {
msg_result = msg_stream.next() => {
if !handle_client_message(&mut session, msg_result).await {
break;
msg = receiver.next() => {
match msg {
Some(Ok(Message::Ping(data))) => {
if sender.send(Message::Pong(data)).await.is_err() { break; }
}
Some(Ok(Message::Close(_))) | None => break,
Some(Err(e)) => {
log!(HttpError::WebSocketError { msg: e.to_string() });
break;
}
_ => {}
}
},
}
_ = data_interval.tick() => {
if !send_ipv6_flow_data(&statistics, &mut session, direction, flow_direction, time_type).await {
break;
match state.statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
Ok(data) => {
match serde_json::to_string(&data) {
Ok(json) => {
if sender.send(Message::Text(json.into())).await.is_err() { break; }
}
Err(e) => { log!(MiscError::SerializeError(e)); }
}
}
Err(e) => { log!(e); break; }
}
},
}
}
let _ = session.close(None).await;
}
async fn handle_client_message(
session: &mut Session,
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
) -> bool {
match msg_result {
Some(Ok(Message::Text(_))) => true,
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
Some(Ok(Message::Close(reason))) => {
let _ = (session.clone()).close(reason).await;
false
}
Some(Err(err)) => {
log!(HttpError::WebSocketError(err));
false
}
None => false,
_ => true,
}
}
async fn send_ipv4_flow_data(
statistics: &Arc<Statistics>,
session: &mut Session,
direction: Direction,
flow_direction: FlowDirection,
time_type: TimeType,
) -> bool {
let flow_data = match statistics.get_ipv4_flow_data(direction, flow_direction, time_type).await {
Ok(data) => data,
Err(e) => { log!(e); return false; }
};
match serde_json::to_string(&flow_data) {
Ok(json) => session.text(json).await.is_ok(),
Err(err) => {
log!(MiscError::SerializeError(err));
true
}
}
}
async fn send_ipv6_flow_data(
statistics: &Arc<Statistics>,
session: &mut Session,
direction: Direction,
flow_direction: FlowDirection,
time_type: TimeType,
) -> bool {
let flow_data = match statistics.get_ipv6_flow_data(direction, flow_direction, time_type).await {
Ok(data) => data,
Err(e) => { log!(e); return false; }
};
match serde_json::to_string(&flow_data) {
Ok(json) => session.text(json).await.is_ok(),
Err(err) => {
log!(MiscError::SerializeError(err));
true
}
}
}
}

View File

@ -1,91 +1,47 @@
use actix_web::{web, HttpRequest, HttpResponse, Result};
use actix_ws::{handle, Message, MessageStream, Session};
use futures_util::StreamExt;
use axum::extract::ws::{Message, WebSocket};
use futures_util::{SinkExt, StreamExt};
use macros::log;
use tokio::sync::broadcast;
use crate::core::infrastructure::health::SystemHealth;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;
use crate::model::health::SystemHealthMetrics;
use crate::model::log::http::HttpLog;
pub async fn websocket_system_health(
req: HttpRequest,
body: web::Payload,
health: web::Data<SystemHealth>,
) -> Result<HttpResponse> {
let (response, session, msg_stream) = handle(&req, body)?;
pub async fn handle_health(socket: WebSocket, mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>) {
let (mut sender, mut receiver) = socket.split();
let broadcast_rx = health.subscribe_to_metrics();
actix_web::rt::spawn(async move {
handle_health_connection(session, msg_stream, broadcast_rx).await;
});
Ok(response)
}
async fn handle_health_connection(
mut session: Session,
mut msg_stream: MessageStream,
mut broadcast_rx: broadcast::Receiver<SystemHealthMetrics>,
) {
loop {
tokio::select! {
msg_result = msg_stream.next() => {
if !handle_client_message(&mut session, msg_result).await {
break;
}
},
broadcast_result = broadcast_rx.recv() => {
match broadcast_result {
Ok(metrics) => {
if !send_metrics(&mut session, &metrics).await {
break;
}
msg = receiver.next() => {
match msg {
Some(Ok(Message::Ping(data))) => {
if sender.send(Message::Pong(data)).await.is_err() { break; }
}
Err(broadcast::error::RecvError::Lagged(skipped)) => {
log!(HttpLog::WebSocketLaged(skipped));
continue;
}
Err(broadcast::error::RecvError::Closed) => {
Some(Ok(Message::Close(_))) | None => break,
Some(Err(e)) => {
log!(HttpError::WebSocketError { msg: e.to_string() });
break;
}
_ => {}
}
},
}
result = broadcast_rx.recv() => {
match result {
Ok(metrics) => {
match serde_json::to_string(&metrics) {
Ok(json) => {
if sender.send(Message::Text(json.into())).await.is_err() { break; }
}
Err(e) => { log!(MiscError::SerializeError(e)); }
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
log!(HttpLog::WebSocketLaged { skipped: n });
}
Err(broadcast::error::RecvError::Closed) => break,
}
}
}
}
let _ = session.close(None).await;
}
async fn handle_client_message(
session: &mut Session,
msg_result: Option<Result<Message, actix_ws::ProtocolError>>,
) -> bool {
match msg_result {
Some(Ok(Message::Text(_))) => true,
Some(Ok(Message::Ping(bytes))) => session.pong(&bytes).await.is_ok(),
Some(Ok(Message::Close(reason))) => {
let _ = (session.clone()).close(reason).await;
false
}
Some(Err(err)) => {
log!(HttpError::WebSocketError(err));
false
}
None => false,
_ => true,
}
}
async fn send_metrics(session: &mut Session, metrics: &SystemHealthMetrics) -> bool {
match serde_json::to_string(metrics) {
Ok(json) => session.text(json).await.is_ok(),
Err(err) => {
log!(MiscError::SerializeError(err));
false
}
}
}

View File

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