refactor: move core/infrastructure/ to infrastructure/, fix layer violations

- Move app_config, health, statistics, geoip from core/infrastructure/ to
  infrastructure/ — completes hexagonal layer separation
- Rename MLService to AppServices (name reflected actual contents: health,
  statistics, ML engine, not just ML)
- Fix core/ → adapter/ dependency violations: jwt.rs, email/scheduler.rs,
  email/report.rs now use dyn RepositoryPort trait instead of concrete Database
- Move misplaced data types to model/:
  - Claims → model/auth.rs
  - AlertMessage → model/ml_detection.rs
  - LicensePayload + LicenseInfo → model/license.rs
  - DropEventMessage + DropCounters → model/drop_event.rs
  - InferenceConfig (ML JSON) → model/config.rs as MLInferenceConfig
- Wire CommunicationManager: enforce mode flow now goes through CQRS
  (ChangeEnforceModeCommand + GetEnforceModeQuery via EnforceModeHandler)
- Add GitHub Actions CI workflow (cargo check + test + clippy)
- Add 9 new tests (38 total): enforce mode handler (3), auth validation (6)
- core/ now contains only business logic with no adapter imports
  (except #[cfg(test)] blocks which need concrete types)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-03-22 00:07:44 +08:00
parent 39ebb15be3
commit 04141a8cc9
40 changed files with 446 additions and 178 deletions

98
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: CI
on:
push:
branches: [master, dalaw2-dev]
pull_request:
branches: [master, dalaw2-dev]
env:
CARGO_TERM_COLOR: always
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.SUBMODULE_PAT }}
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
gcc m4 clang llvm \
libelf-dev zlib1g-dev pkg-config
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Rust nightly toolchain (for bpf-linker)
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
- name: Cache cargo registry and build artifacts
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
# bpf-linker is required by build.rs to compile eBPF programs.
# Use cargo-binstall for a faster pre-built binary install when available,
# otherwise fall back to building from source (can take ~30 min).
- name: Install bpf-linker
run: |
cargo install cargo-binstall --locked 2>/dev/null || true
if command -v cargo-binstall &>/dev/null; then
cargo binstall bpf-linker --no-confirm --locked || cargo install bpf-linker --locked
else
cargo install bpf-linker --locked
fi
timeout-minutes: 45
- name: cargo check
run: cargo check --package net-guardia
- name: cargo test
run: cargo test --package net-guardia
- name: cargo clippy
run: cargo clippy --package net-guardia -- -D warnings -A dead_code
integration-test:
name: Integration Test (placeholder)
runs-on: ubuntu-latest
needs: build-and-test
if: github.event_name == 'push' || github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
# Integration tests require a podman-based multi-container network
# environment (netguardia, router, external, internal containers) that
# is not available in GitHub Actions runners.
#
# The real integration tests are run on the dev server via:
# /home/dalaw2/test_netguardia.sh
#
# That script tests XDP packet forwarding, Web API endpoints, ACL
# blocking, rate limiting, ML engine, WebSocket, GeoIP country
# blocking, and DNS blacklist functionality across the container
# network topology.
- name: Integration test reminder
run: |
echo "============================================"
echo " Integration tests are not run in CI."
echo " They require the podman test environment."
echo ""
echo " Run on the dev server:"
echo " bash /home/dalaw2/test_netguardia.sh"
echo "============================================"

View File

@ -1,7 +1,8 @@
use actix_web::{web, HttpMessage, HttpRequest, HttpResponse, Responder, Scope};
use serde::Deserialize;
use crate::core::auth::jwt::{Claims, JwtService};
use crate::core::auth::jwt::JwtService;
use crate::model::auth::Claims;
use crate::core::auth::password;
use crate::interface::port::repository::RepositoryPort;
@ -220,3 +221,47 @@ async fn change_password(
.json(serde_json::json!({"error": e.to_string()})),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_username_valid() {
assert!(validate_username("admin").is_ok());
assert!(validate_username("user_123").is_ok());
assert!(validate_username("a").is_ok());
}
#[test]
fn test_validate_username_empty() {
assert!(validate_username("").is_err());
}
#[test]
fn test_validate_username_too_long() {
let long = "a".repeat(33);
assert!(validate_username(&long).is_err());
}
#[test]
fn test_validate_username_special_chars() {
assert!(validate_username("admin@host").is_err());
assert!(validate_username("user name").is_err());
assert!(validate_username("user-name").is_err());
assert!(validate_username("用戶").is_err());
}
#[test]
fn test_validate_password_valid() {
assert!(validate_password("12345678").is_ok());
assert!(validate_password("a very long password").is_ok());
}
#[test]
fn test_validate_password_too_short() {
assert!(validate_password("").is_err());
assert!(validate_password("1234567").is_err());
assert!(validate_password("a").is_err());
}
}

View File

@ -1,6 +1,6 @@
use actix_web::{web, HttpResponse, Responder, Scope};
use crate::core::infrastructure::health::SystemHealth;
use crate::infrastructure::health::SystemHealth;
pub fn initialize() -> Scope {
web::scope("/health")

View File

@ -1,7 +1,7 @@
use actix_web::{web, HttpResponse, Responder, Scope};
use crate::core::ebpf::drop_monitor::DropMonitor;
use crate::core::infrastructure::statistics::FlowStatistics;
use crate::infrastructure::statistics::FlowStatistics;
pub fn initialize() -> Scope {
web::scope("/stats")

View File

@ -1,6 +1,9 @@
use actix_web::{web, HttpResponse, Responder, Scope};
use serde::Deserialize;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
use crate::interface::communication::query_types::GetEnforceModeQuery;
use crate::interface::port::repository::RepositoryPort;
type Repo = dyn RepositoryPort;
@ -27,10 +30,9 @@ async fn get_boot_time() -> impl Responder {
HttpResponse::Ok().json(crate::utils::boot_time::boot_time())
}
async fn get_enforce_mode(db: web::Data<Repo>) -> impl Responder {
match db.get_setting("enforce_mode") {
Ok(Some(mode)) => HttpResponse::Ok().json(serde_json::json!({"mode": mode})),
Ok(None) => HttpResponse::Ok().json(serde_json::json!({"mode": "monitor"})),
async fn get_enforce_mode(comm: web::Data<CommunicationManager>) -> impl Responder {
match comm.send_query(GetEnforceModeQuery).await {
Ok(mode) => HttpResponse::Ok().json(serde_json::json!({"mode": mode})),
Err(e) => HttpResponse::InternalServerError()
.json(serde_json::json!({"error": e.to_string()})),
}
@ -38,7 +40,7 @@ async fn get_enforce_mode(db: web::Data<Repo>) -> impl Responder {
async fn set_enforce_mode(
body: web::Json<EnforceModeRequest>,
db: web::Data<Repo>,
comm: web::Data<CommunicationManager>,
) -> impl Responder {
let mode = &body.mode;
if mode != "monitor" && mode != "enforce" {
@ -46,9 +48,8 @@ async fn set_enforce_mode(
.json(serde_json::json!({"error": "Mode must be 'monitor' or 'enforce'"}));
}
match db.set_setting("enforce_mode", mode) {
match comm.send_command(ChangeEnforceModeCommand { mode: mode.clone() }).await {
Ok(_) => {
tracing::info!("Enforce mode changed to: {}", mode);
HttpResponse::Ok().json(serde_json::json!({"mode": mode}))
}
Err(e) => HttpResponse::InternalServerError()

View File

@ -4,7 +4,8 @@ use futures_util::StreamExt;
use macros::log;
use tokio::sync::broadcast;
use crate::core::ml::alert::{MLAlert, AlertMessage};
use crate::core::ml::alert::MLAlert;
use crate::model::ml_detection::AlertMessage;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;

View File

@ -4,7 +4,8 @@ use futures_util::StreamExt;
use macros::log;
use tokio::sync::broadcast;
use crate::core::ebpf::drop_monitor::{DropMonitor, DropEventMessage};
use crate::core::ebpf::drop_monitor::DropMonitor;
use crate::model::drop_event::DropEventMessage;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;

View File

@ -5,7 +5,7 @@ use actix_ws::Message;
use futures_util::StreamExt;
use tokio::time::interval;
use crate::core::infrastructure::statistics::FlowStatistics;
use crate::infrastructure::statistics::FlowStatistics;
use crate::model::flow_stats::FlowSubscription;
/// Default subscription: all flows, no filter, 5 second interval

View File

@ -4,7 +4,7 @@ use futures_util::StreamExt;
use macros::log;
use tokio::sync::broadcast;
use crate::core::infrastructure::health::SystemHealth;
use crate::infrastructure::health::SystemHealth;
use crate::model::error::http::HttpError;
use crate::model::error::misc::MiscError;
use crate::model::log::http::HttpLog;

View File

@ -3,8 +3,8 @@ use serde::Deserialize;
use crate::core::auth::jwt::JwtService;
use crate::core::ebpf::drop_monitor::DropMonitor;
use crate::core::infrastructure::health::SystemHealth;
use crate::core::infrastructure::statistics::FlowStatistics;
use crate::infrastructure::health::SystemHealth;
use crate::infrastructure::statistics::FlowStatistics;
use crate::core::ml::alert::MLAlert;
use super::{alert_websocket, drop_websocket, flow_websocket, health_websocket};

View File

@ -1,18 +1,10 @@
use jsonwebtoken::{decode, encode, errors::ErrorKind, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use crate::adapter::persistence::Database;
use crate::interface::port::repository::RepositoryPort;
use crate::model::auth::Claims;
use crate::model::error::auth::AuthError;
use crate::model::error::Error;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: i64,
pub username: String,
pub role: String,
pub exp: usize,
}
pub struct JwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
@ -20,7 +12,7 @@ pub struct JwtService {
}
impl JwtService {
pub fn new(db: &Database, expiry_hours: u64) -> Result<Self, Error> {
pub fn new(db: &dyn RepositoryPort, expiry_hours: u64) -> Result<Self, Error> {
let secret = match db.get_setting("jwt_secret")? {
Some(s) => s,
None => {
@ -81,6 +73,7 @@ fn hex_encode(data: &[u8]) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::persistence::Database;
fn test_jwt_service() -> JwtService {
let db = Database::new(":memory:").unwrap();

View File

@ -3,40 +3,16 @@ use std::mem;
use std::time::Duration;
use aya::maps::{MapData, RingBuf};
use serde::Serialize;
use tokio::sync::{broadcast, oneshot};
use common::define::drop_reason::*;
use common::model::drop_event::DropEvent as RawDropEvent;
use parking_lot::Mutex;
use crate::model::drop_event::{DropCounters, DropEventMessage};
const DROP_CHANNEL_CAPACITY: usize = 100;
#[derive(Debug, Clone, Serialize)]
pub struct DropEventMessage {
pub timestamp_ns: u64,
pub src_ip: String,
pub dst_ip: String,
pub src_port: u16,
pub dst_port: u16,
pub protocol: u8,
pub reason: String,
pub ip_version: u8,
}
#[derive(Default, Clone, Serialize)]
pub struct DropCounters {
pub acl_blacklist: u64,
pub rate_limit_pkt: u64,
pub rate_limit_syn: u64,
pub rate_limit_udp: u64,
pub rate_limit_dns: u64,
pub protocol_filter: u64,
pub dns_blacklist: u64,
pub geo_block: u64,
pub total: u64,
}
pub struct DropMonitor {
broadcast_tx: broadcast::Sender<DropEventMessage>,
counters: Mutex<DropCounters>,

View File

@ -8,7 +8,7 @@ use ipnetwork::IpNetwork;
use maxminddb::{geoip2, Reader};
use parking_lot::RwLock;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::misc::MiscError;
use crate::model::error::Error;

View File

@ -22,7 +22,7 @@ use crate::core::ebpf::geo_block::GeoBlock;
use crate::core::ebpf::rate_limit::RateLimitConfig;
use crate::core::ebpf::protocol_filter::ProtocolFilter;
use crate::core::ebpf::xsk_manager::XskManager;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::core::ml::engine::Engine;
use crate::model::error::ebpf::EbpfError;
use crate::model::error::system::SystemError;

View File

@ -17,7 +17,7 @@ use xsk_rs::config::{BindFlags, FrameSize, Interface, LibxdpFlags, QueueSize, So
use xsk_rs::{CompQueue, FillQueue, FrameDesc, RxQueue, Socket, TxQueue, Umem};
use crate::core::ebpf::dns_filter::DnsFilter;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::core::ml::engine::Engine;
use crate::core::ml::flow_tracker::FlowTracker;
use crate::model::config::NetworkConfig;

View File

@ -1,6 +1,5 @@
use crate::adapter::persistence::Database;
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
use std::sync::Arc;
/// Generate an HTML weekly report email body.
///
@ -14,7 +13,7 @@ use std::sync::Arc;
///
/// If a key is missing the report falls back to placeholder data so it can
/// be exercised before the ML aggregation pipeline is wired up.
pub fn generate_weekly_report(db: &Arc<Database>) -> Result<String, Error> {
pub fn generate_weekly_report(db: &dyn RepositoryPort) -> Result<String, Error> {
let threats_count = db
.get_setting("weekly_threats_count")
?

View File

@ -1,4 +1,4 @@
use crate::adapter::persistence::Database;
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::database::DatabaseError;
use crate::model::error::Error;
use lettre::message::header::ContentType;
@ -22,7 +22,7 @@ impl SmtpClient {
///
/// Returns `None` if any required setting (`smtp_host`, `smtp_port`,
/// `smtp_username`, `smtp_password`) is missing.
pub fn from_database(db: &Database) -> Result<Option<Self>, Error> {
pub fn from_database(db: &dyn RepositoryPort) -> Result<Option<Self>, Error> {
let host = match db.get_setting("smtp_host")? {
Some(v) if !v.is_empty() => v,
_ => return Ok(None),
@ -100,11 +100,11 @@ impl SmtpClient {
/// Scheduler that checks once per hour whether it is time to send the weekly
/// report (Monday 08:00 local time) and dispatches it via SMTP.
pub struct ReportScheduler {
db: Arc<Database>,
db: Arc<dyn RepositoryPort>,
}
impl ReportScheduler {
pub fn new(db: Arc<Database>) -> Self {
pub fn new(db: Arc<dyn RepositoryPort>) -> Self {
Self { db }
}
@ -123,7 +123,7 @@ impl ReportScheduler {
info!("Weekly report window reached — preparing report");
let smtp = match SmtpClient::from_database(&db) {
let smtp = match SmtpClient::from_database(&*db) {
Ok(Some(client)) => client,
Ok(None) => {
warn!(
@ -146,7 +146,7 @@ impl ReportScheduler {
}
};
let html = match super::report::generate_weekly_report(&db) {
let html = match super::report::generate_weekly_report(&*db) {
Ok(h) => h,
Err(e) => {
error!("Failed to generate weekly report: {e}");

View File

@ -1,4 +0,0 @@
pub mod app_config;
pub mod geoip;
pub mod health;
pub mod statistics;

View File

@ -1,3 +1,3 @@
pub mod validator;
pub use validator::LicenseInfo;
pub use crate::model::license::LicenseInfo;

View File

@ -3,40 +3,15 @@ use std::path::Path;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use ed25519_dalek::{Signature, VerifyingKey, Verifier};
use serde::{Deserialize, Serialize};
use crate::model::error::license::LicenseError;
use crate::model::license::{LicenseInfo, LicensePayload};
/// Public key auto-embedded from license_pub.key at compile time.
/// Generate with: cd license-generator && cargo run -- keygen
/// Then place license_pub.key in the repo root.
const PUBLIC_KEY_HEX: &str = env!("LICENSE_PUBLIC_KEY");
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicensePayload {
pub ingress_mac: String,
pub egress_mac: String,
pub expires: String,
pub features: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseInfo {
pub payload: Option<LicensePayload>,
pub valid: bool,
pub days_remaining: i64,
}
impl LicenseInfo {
pub fn unlicensed() -> Self {
Self {
payload: None,
valid: false,
days_remaining: 0,
}
}
}
pub fn validate_license(license_path: &str, ingress_ifname: &str, egress_ifname: &str) -> Result<LicenseInfo, crate::model::error::Error> {
// Guard against builds where the license feature was not configured
if PUBLIC_KEY_HEX == "DISABLED" {

View File

@ -1,50 +1,11 @@
use macros::log;
use serde::Serialize;
use tokio::sync::broadcast;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::DetectionResult;
use crate::model::ml_detection::{AlertMessage, DetectionResult};
const ALERT_CHANNEL_CAPACITY: usize = 100;
#[derive(Debug, Clone, Serialize)]
pub struct AlertMessage {
pub timestamp: u64,
pub flow_key: String,
pub src_ip: String,
pub dst_ip: String,
pub src_port: u16,
pub dst_port: u16,
pub protocol: u8,
pub is_attack: bool,
pub attack_type: Option<String>,
pub confidence: f32,
pub ae_score: f32,
}
impl AlertMessage {
pub fn from_detection_result(result: &DetectionResult) -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
timestamp,
flow_key: result.flow_key.clone(),
src_ip: result.flow_key_raw.src_ip_string(),
dst_ip: result.flow_key_raw.dst_ip_string(),
src_port: result.flow_key_raw.src_port,
dst_port: result.flow_key_raw.dst_port,
protocol: result.flow_key_raw.protocol,
is_attack: result.is_attack,
attack_type: result.attack_type.clone(),
confidence: result.confidence,
ae_score: result.ae_score,
}
}
}
pub struct MLAlert {
broadcast_tx: broadcast::Sender<AlertMessage>,
}

View File

@ -1,30 +1,19 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::model::error::ml::MLError;
use crate::model::ml_detection::ClipParams;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceConfig {
pub ae_feature_names: Vec<String>,
pub ae_clip_params: HashMap<String, ClipParams>,
pub ae_scaler_mean: Vec<f64>,
pub ae_scaler_std: Vec<f64>,
pub ae_post_clip_min: f64,
pub ae_post_clip_max: f64,
pub ae_threshold: f32,
pub classifier_feature_names: Vec<String>,
pub attack_labels: HashMap<String, String>,
}
pub use crate::model::config::MLInferenceConfig;
impl InferenceConfig {
/// Backward-compatible alias so existing `use config_loader::InferenceConfig` paths still compile.
pub type InferenceConfig = MLInferenceConfig;
impl MLInferenceConfig {
pub fn load_file(file: &str) -> Result<Self, MLError> {
let path = PathBuf::from("models").join(file);
let content = fs::read_to_string(&path)
.map_err(|_| MLError::ConfigLoadFailed(path.to_path_buf()))?;
let config: InferenceConfig = serde_json::from_str(&content)
let config: MLInferenceConfig = serde_json::from_str(&content)
.map_err(|e| MLError::ConfigParseFailed(e.to_string()))?;
if config.ae_feature_names.is_empty() {
return Err(MLError::ConfigParseFailed("ae_feature_names is empty"));
@ -37,17 +26,4 @@ impl InferenceConfig {
}
Ok(config)
}
pub fn num_ae_features(&self) -> usize {
self.ae_feature_names.len()
}
pub fn num_classifier_features(&self) -> usize {
self.classifier_feature_names.len()
}
pub fn num_attack_types(&self) -> usize {
self.attack_labels.len()
}
}
}

View File

@ -1,7 +1,7 @@
use tract_onnx::prelude::*;
use std::path::PathBuf;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::model::error::ml::MLError;
use crate::model::ml_detection::RunnableModel;

View File

@ -1,7 +1,6 @@
pub mod auth;
pub mod email;
pub mod ebpf;
pub mod infrastructure;
#[cfg(feature = "license")]
pub mod license;
pub mod ml;

View File

@ -7,8 +7,9 @@ use macros::log;
use crate::core::auth::jwt::JwtService;
use crate::adapter::persistence::Database;
use crate::core::ebpf::EbpfServices;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_services::AppServices;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::core::ml::config_loader::InferenceConfig;
#[cfg(feature = "license")]
use crate::core::license::LicenseInfo;
@ -30,6 +31,7 @@ pub struct System {
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,
pub jwt_service: Arc<JwtService>,
pub comm: Arc<CommunicationManager>,
#[cfg(feature = "license")]
pub license_info: Arc<LicenseInfo>,
pub ingress_ebpf: Ebpf,
@ -48,6 +50,7 @@ impl System {
app_services: state.app_services,
db: state.db,
jwt_service: state.jwt_service,
comm: state.comm,
#[cfg(feature = "license")]
license_info: state.license_info,
ingress_ebpf: state.ingress_ebpf,
@ -122,6 +125,7 @@ impl System {
app_services: self.app_services.clone(),
db: self.db.clone(),
jwt_service: self.jwt_service.clone(),
comm: self.comm.clone(),
#[cfg(feature = "license")]
license_info: self.license_info.clone(),
};

View File

@ -5,10 +5,10 @@ use crossbeam::queue::SegQueue;
use macros::log;
use tokio::sync::oneshot;
use crate::core::infrastructure::app_config::AppConfig;
use crate::core::infrastructure::health::SystemHealth;
use crate::infrastructure::app_config::AppConfig;
use crate::infrastructure::health::SystemHealth;
use crate::core::ml::alert::MLAlert;
use crate::core::infrastructure::statistics::FlowStatistics;
use crate::infrastructure::statistics::FlowStatistics;
use crate::core::ml::config_loader::InferenceConfig;
use crate::core::ml::engine::Engine;
use crate::model::ml_detection::EngineConfig;

View File

@ -0,0 +1,84 @@
use async_trait::async_trait;
use std::sync::Arc;
use crate::interface::communication::command::CommandHandler;
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
use crate::interface::communication::query::QueryHandler;
use crate::interface::communication::query_types::GetEnforceModeQuery;
use crate::interface::port::repository::RepositoryPort;
use crate::model::error::Error;
/// Handles enforce-mode commands and queries by delegating to the repository.
pub struct EnforceModeHandler {
db: Arc<dyn RepositoryPort>,
}
impl EnforceModeHandler {
pub fn new(db: Arc<dyn RepositoryPort>) -> Self {
Self { db }
}
}
#[async_trait]
impl CommandHandler<ChangeEnforceModeCommand> for EnforceModeHandler {
async fn handle_command(&self, command: ChangeEnforceModeCommand) -> Result<(), Error> {
self.db.set_setting("enforce_mode", &command.mode)?;
tracing::info!("Enforce mode changed to: {}", command.mode);
Ok(())
}
}
#[async_trait]
impl QueryHandler<GetEnforceModeQuery> for EnforceModeHandler {
async fn handle_query(&self, _query: GetEnforceModeQuery) -> Result<String, Error> {
match self.db.get_setting("enforce_mode")? {
Some(mode) => Ok(mode),
None => Ok("monitor".to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::adapter::persistence::Database;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
use crate::interface::communication::query_types::GetEnforceModeQuery;
fn test_handler() -> (Arc<EnforceModeHandler>, Arc<CommunicationManager>) {
let db = Arc::new(Database::new(":memory:").unwrap()) as Arc<dyn RepositoryPort>;
let handler = Arc::new(EnforceModeHandler::new(db));
let comm = Arc::new(CommunicationManager::new());
let _ = comm.clone()
.with_service(handler.clone())
.command::<ChangeEnforceModeCommand>()
.query::<GetEnforceModeQuery>()
.build();
(handler, comm)
}
#[tokio::test]
async fn test_default_mode_is_monitor() {
let (_, comm) = test_handler();
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
assert_eq!(mode, "monitor");
}
#[tokio::test]
async fn test_change_to_enforce() {
let (_, comm) = test_handler();
comm.send_command(ChangeEnforceModeCommand { mode: "enforce".into() }).await.unwrap();
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
assert_eq!(mode, "enforce");
}
#[tokio::test]
async fn test_change_back_to_monitor() {
let (_, comm) = test_handler();
comm.send_command(ChangeEnforceModeCommand { mode: "enforce".into() }).await.unwrap();
comm.send_command(ChangeEnforceModeCommand { mode: "monitor".into() }).await.unwrap();
let mode = comm.send_query(GetEnforceModeQuery).await.unwrap();
assert_eq!(mode, "monitor");
}
}

View File

@ -6,7 +6,7 @@ use tokio::sync::{broadcast, oneshot, RwLock};
use tokio::time::interval;
use macros::log;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::model::log::health::Health;
use crate::model::error::Error;
use crate::model::health::{

View File

@ -6,8 +6,9 @@ use actix_web::{web, App, HttpServer};
use crate::core::auth::jwt::JwtService;
use crate::adapter::persistence::Database;
use crate::core::ebpf::EbpfServices;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_services::AppServices;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::core::ml::config_loader::InferenceConfig;
#[cfg(feature = "license")]
use crate::core::license::LicenseInfo;
@ -25,6 +26,7 @@ pub struct HttpServerParams {
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,
pub jwt_service: Arc<JwtService>,
pub comm: Arc<CommunicationManager>,
#[cfg(feature = "license")]
pub license_info: Arc<LicenseInfo>,
}
@ -45,6 +47,7 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
let inference_config = params.inference_config;
let db = params.db;
let jwt_service = params.jwt_service;
let comm = params.comm;
#[cfg(feature = "license")]
let license_info = params.license_info;
let port = app_config.http.http_server_bind_port;
@ -70,7 +73,8 @@ pub async fn run(params: HttpServerParams) -> Result<(), Error> {
.app_data(web::Data::from(flow_statistics.clone()))
.app_data(web::Data::from(drop_monitor.clone()))
.app_data(web::Data::from(db.clone() as Arc<dyn RepositoryPort>))
.app_data(web::Data::from(jwt_service.clone()));
.app_data(web::Data::from(jwt_service.clone()))
.app_data(web::Data::from(comm.clone()));
#[cfg(feature = "license")]
let app = app.app_data(web::Data::from(license_info.clone()));
app.service(

View File

@ -1,4 +1,9 @@
pub mod app_config;
pub mod app_services;
pub mod communication_manager;
pub mod enforce_mode_handler;
pub mod geoip;
pub mod health;
pub mod http_server;
pub mod service_factory;
pub mod statistics;

View File

@ -12,8 +12,13 @@ use crate::core::auth::jwt::JwtService;
use crate::core::auth::password;
use crate::adapter::persistence::Database;
use crate::core::ebpf::EbpfServices;
use crate::core::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_config::AppConfig;
use crate::infrastructure::app_services::AppServices;
use crate::infrastructure::communication_manager::CommunicationManager;
use crate::infrastructure::enforce_mode_handler::EnforceModeHandler;
use crate::interface::communication::command_types::ChangeEnforceModeCommand;
use crate::interface::communication::query_types::GetEnforceModeQuery;
use crate::interface::port::repository::RepositoryPort;
#[cfg(feature = "license")]
use crate::core::license::LicenseInfo;
#[cfg(feature = "license")]
@ -33,6 +38,7 @@ pub struct AppState {
pub app_services: Arc<AppServices>,
pub db: Arc<Database>,
pub jwt_service: Arc<JwtService>,
pub comm: Arc<CommunicationManager>,
#[cfg(feature = "license")]
pub license_info: Arc<LicenseInfo>,
pub ingress_ebpf: Ebpf,
@ -93,7 +99,7 @@ impl ServiceFactory {
db.set_setting("enforce_mode", "monitor")?;
}
let jwt_service = Arc::new(JwtService::new(&db, app_config.http.jwt_expiry_hours)?);
let jwt_service = Arc::new(JwtService::new(db.as_ref(), app_config.http.jwt_expiry_hours)?);
let ebpf_services = Arc::new(EbpfServices::new(
app_config.clone(),
@ -103,6 +109,15 @@ impl ServiceFactory {
let app_services = Arc::new(AppServices::new(app_config.clone(), inference_config.clone())?);
// Create CommunicationManager and register enforce-mode handler
let comm = Arc::new(CommunicationManager::new());
let enforce_handler = Arc::new(EnforceModeHandler::new(db.clone() as Arc<dyn RepositoryPort>));
let _ = comm.clone()
.with_service(enforce_handler)
.command::<ChangeEnforceModeCommand>()
.query::<GetEnforceModeQuery>()
.build();
// Restore persisted state from database
Self::restore_dns_blacklist(&db, &ebpf_services);
Self::restore_geo_countries(&db, &ebpf_services);
@ -116,6 +131,7 @@ impl ServiceFactory {
app_services,
db,
jwt_service,
comm,
#[cfg(feature = "license")]
license_info,
ingress_ebpf,

View File

@ -0,0 +1,9 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
pub sub: i64,
pub username: String,
pub role: String,
pub exp: usize,
}

View File

@ -1,5 +1,9 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::model::ml_detection::ClipParams;
#[derive(Debug, Deserialize)]
pub struct AppConfigTable {
#[serde(rename = "Http")]
@ -76,3 +80,30 @@ pub struct PipelineConfig {
pub ingress: Vec<String>,
pub egress: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MLInferenceConfig {
pub ae_feature_names: Vec<String>,
pub ae_clip_params: HashMap<String, ClipParams>,
pub ae_scaler_mean: Vec<f64>,
pub ae_scaler_std: Vec<f64>,
pub ae_post_clip_min: f64,
pub ae_post_clip_max: f64,
pub ae_threshold: f32,
pub classifier_feature_names: Vec<String>,
pub attack_labels: HashMap<String, String>,
}
impl MLInferenceConfig {
pub fn num_ae_features(&self) -> usize {
self.ae_feature_names.len()
}
pub fn num_classifier_features(&self) -> usize {
self.classifier_feature_names.len()
}
pub fn num_attack_types(&self) -> usize {
self.attack_labels.len()
}
}

View File

@ -0,0 +1,26 @@
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct DropEventMessage {
pub timestamp_ns: u64,
pub src_ip: String,
pub dst_ip: String,
pub src_port: u16,
pub dst_port: u16,
pub protocol: u8,
pub reason: String,
pub ip_version: u8,
}
#[derive(Default, Clone, Serialize)]
pub struct DropCounters {
pub acl_blacklist: u64,
pub rate_limit_pkt: u64,
pub rate_limit_syn: u64,
pub rate_limit_udp: u64,
pub rate_limit_dns: u64,
pub protocol_filter: u64,
pub dns_blacklist: u64,
pub geo_block: u64,
pub total: u64,
}

View File

@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicensePayload {
pub ingress_mac: String,
pub egress_mac: String,
pub expires: String,
pub features: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicenseInfo {
pub payload: Option<LicensePayload>,
pub valid: bool,
pub days_remaining: i64,
}
impl LicenseInfo {
pub fn unlicensed() -> Self {
Self {
payload: None,
valid: false,
days_remaining: 0,
}
}
}

View File

@ -139,3 +139,41 @@ impl InferenceStats {
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct AlertMessage {
pub timestamp: u64,
pub flow_key: String,
pub src_ip: String,
pub dst_ip: String,
pub src_port: u16,
pub dst_port: u16,
pub protocol: u8,
pub is_attack: bool,
pub attack_type: Option<String>,
pub confidence: f32,
pub ae_score: f32,
}
impl AlertMessage {
pub fn from_detection_result(result: &DetectionResult) -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
timestamp,
flow_key: result.flow_key.clone(),
src_ip: result.flow_key_raw.src_ip_string(),
dst_ip: result.flow_key_raw.dst_ip_string(),
src_port: result.flow_key_raw.src_port,
dst_port: result.flow_key_raw.dst_port,
protocol: result.flow_key_raw.protocol,
is_attack: result.is_attack,
attack_type: result.attack_type.clone(),
confidence: result.confidence,
ae_score: result.ae_score,
}
}
}

View File

@ -1,9 +1,13 @@
pub mod auth;
pub mod config;
pub mod direction;
pub mod drop_event;
pub mod error;
pub mod flow_stats;
pub mod health;
pub mod ip_address;
#[cfg(feature = "license")]
pub mod license;
pub mod list_type;
pub mod log;
pub mod ml_detection;