feat/optimized-inference-speed (#5)

* feat: complete rule scan, maybe

* feat: improve inference speed and timely stop for ^C and solve cpu race
This commit is contained in:
ParrotXray 2026-05-19 09:47:48 +08:00 committed by GitHub
parent 5e42c137ae
commit e686449a98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 165 additions and 72 deletions

86
TODO
View File

@ -1,12 +1,12 @@
1. .csv 一開始文件格式為 traffic-yyyy-oo-zz.csv, 換天後直接變日期創新檔,例如今天啟動今天為 traffic-2026-05-11.csv 隔天變成 traffic-2026-05-12.csv [x]
2. will change tract-onnx engine to ort-tract engine [x]
3. 改善推論效能及速度,在大量資料時
3. 改善推論效能及速度,在大量資料時 [x]
4. 實現 ML + RULE 的共用 HashMap 實現共同決策結果
5. 代碼最佳化,檢查是否除了 build.rs 以外有無 .unwarp() and eprintln() [x]
6. 完成前端 detection 頁面
7. 使用 sqllite 實現帳號系統、白黑名單永久記錄
[o]
[x]
Suricata
提升偵測準確率:
pcre 很多規則依賴正則,沒有會漏掉很多威脅
@ -18,7 +18,7 @@ $HOME_NET / $EXTERNAL_NET 區分內外網,很多規則依賴這個
flow:established 順便過濾掉不完整連線
============================================================
PRIORITY BACKLOG (updated 2026-05-16)
PRIORITY BACKLOG (updated 2026-05-18)
============================================================
-- MUST DO (system is not usable without these) -----------
@ -59,23 +59,69 @@ PRIORITY BACKLOG (updated 2026-05-16)
-- SHOULD DO (runtime management) ------------------------
[ ] HTTP API - rule / suppress / whitelist management
Endpoints needed:
GET /api/rules - list loaded rules (sid, msg, enabled)
POST /api/rules/reload - trigger hot-reload (see below)
GET /api/suppress - list suppress entries
POST /api/suppress - add entry { sid, track, ip }
[ ] Account system + persistent list DB (updated 2026-05-18)
Two SQLite files, separated by concern:
static/db/rules.db (rule-related, already exists)
suppress (id, sid INTEGER, track INTEGER, ip_net TEXT, comment TEXT, created_at)
- track: 1=by_src 2=by_dst 4=by_either (Suricata values)
- already populated by build.rs; API writes go here; RuleEngine reloads on change
static/db/app.db (account + network list, new)
accounts (id, username, password_hash, role, created_at)
- role: "admin" | "viewer"
- password_hash: argon2 or bcrypt
sessions (token TEXT PK, account_id, expires_at)
- token: 32-byte random hex, expires in 24 h
whitelist (id, ip_net TEXT, comment TEXT, created_at)
- packets from whitelisted CIDRs skip rule + ML evaluation entirely
blacklist (id, ip_net TEXT, comment TEXT, action TEXT, created_at)
- action: "alert" | "drop"
- packets from blacklisted CIDRs auto-alert without rule evaluation
Migration: CREATE TABLE IF NOT EXISTS at startup in AppServices::init().
File: core/infrastructure/app_db.rs (new)
[ ] HTTP API - accounts / suppress / whitelist / blacklist / rules
All routes require session token in Authorization: Bearer <token> header
except POST /api/auth/login.
Auth:
POST /api/auth/login - { username, password } -> { token, expires_at }
POST /api/auth/logout - invalidate current token
GET /api/auth/me - current account info
Accounts (admin only):
GET /api/accounts - list accounts
POST /api/accounts - create account { username, password, role }
PUT /api/accounts/:id/password - change password
DELETE /api/accounts/:id - delete account
Suppress:
GET /api/suppress - list all entries
POST /api/suppress - add { sid, track, ip_net, comment }
DELETE /api/suppress/:id - remove entry
GET /api/whitelist - list whitelist IPs / CIDRs
POST /api/whitelist - add entry { ip_net, comment }
DELETE /api/whitelist/:id - remove entry
GET /api/blacklist - list blacklist entries
POST /api/blacklist - add entry
DELETE /api/blacklist/:id - remove entry
POST /api/system/restart - graceful restart (re-attach eBPF, reload all)
Storage: rules.db (suppress / whitelist / blacklist tables already planned in item 7).
Auth: session token via SQLite account system (item 7).
Files: web/routes/rules.rs, web/routes/system.rs
(runtime effect: RuleEngine reloads suppress table after write)
Whitelist:
GET /api/whitelist - list entries
POST /api/whitelist - add { ip_net, comment }
DELETE /api/whitelist/:id - remove
Blacklist:
GET /api/blacklist - list entries
POST /api/blacklist - add { ip_net, action, comment }
DELETE /api/blacklist/:id - remove
Rules:
GET /api/rules - list loaded rules (sid, msg, enabled)
POST /api/rules/reload - trigger hot-reload
System:
POST /api/system/restart - graceful restart
Files: web/routes/auth.rs, web/routes/accounts.rs, web/routes/lists.rs,
web/routes/rules.rs, web/routes/system.rs
Middleware: web/middleware/auth.rs (token extractor + role check)
[ ] Hot-reload: rules + suppress without process restart
Trigger: POST /api/rules/reload OR SIGHUP signal.
@ -121,4 +167,4 @@ PRIORITY BACKLOG (updated 2026-05-16)
[x] isdataat
Completed 2026-05-15 (included with byte_test/jump/extract).
ByteOp kind=3; supports negated form (!isdataat) and relative flag.
build.rs: parse_isdataat(); rule_engine.rs: eval_byte_ops() case 3.
build.rs: parse_isdataat(); rule_engine.rs: eval_byte_ops() case 3.

View File

@ -4,7 +4,11 @@ egress_ifname = "enp4s0f0" # Egress NIC Name
geoip_db_name = "GeoLite2-City.mmdb"
deep_autoencoder_name = "deep_autoencoder.onnx"
models_config_name = "inference_config.json"
combined_queue_count = 8 # NIC Combined Queue Count (ethtool -l <NIC>)
# NIC Combined Queue Count (ethtool -l <NIC>)
# If you will edit this, please enter ethtool -L <NIC> combined <combined number> at System Terminal.
# Then change this item.
combined_queue_count = 8
channel_size = 4096
fill_queue_size = 4096 # Umem Used (Should not modify)
comp_queue_size = 4096 # Umem Used (Should not modify)
@ -23,9 +27,18 @@ aggregator_window_secs = 30
inference_batch_size = 200
flow_timeout_us = 60_000_000
traffic_logging_mode = true # When true, disables ML inference and records all ingress/egress packets to CSV
traffic_logging_mode = false # When true, disables ML inference and records all ingress/egress packets to CSV
traffic_log_csv_path = "traffic_log.csv" # Output CSV file path for traffic logging mode
home_net = ["140.130.34.0/24"]
# tls_keylog_path = "/tmp/tls_keys.log" # NSS key log file for TLS decryption (SSLKEYLOGFILE)
# tls_keylog_path = "/tmp/tls_keys.log" # NSS key log file for TLS decryption (SSLKEYLOGFILE)
# CPU affinity (Linux only). Uncomment and tune for your hardware.
# Pin XSK packet threads starting from this core (one core per queue pair).
# Example: xsk_cpu_base=0 with combined_queue_count=4 uses cores 0-3 for packets.
xsk_cpu_base = 0
#
# Pin ML inference (ONNX spawn_blocking) to this core.
# Example: on an 8-core machine, reserve core 7 for inference.
ml_cpu = 7

View File

@ -28,6 +28,7 @@ use crate::model::error::Error;
use crate::model::log::ebpf::EbpfLog;
use crate::model::log::ml::MLLog;
use crate::model::log::rule::RuleLog;
use crate::utils::cpu_affinity::set_cpu_affinity;
pub struct XskManager {
app_config: Arc<AppConfig>,
@ -124,6 +125,8 @@ impl XskManager {
pub struct XskPair {
direction: Direction,
queue_id: u32,
xsk_cpu_base: Option<u32>,
umem: Arc<Umem>,
fill_queue: FillQueue,
comp_queue: CompQueue,
@ -193,6 +196,8 @@ impl XskPair {
let xsk_pair = Self {
direction,
queue_id,
xsk_cpu_base: config.xsk_cpu_base,
umem: Arc::new(umem),
fill_queue,
comp_queue,
@ -219,6 +224,10 @@ impl XskPair {
thread::Builder::new()
.name(thread_name.clone())
.spawn(move || {
if let Some(base) = self.xsk_cpu_base {
set_cpu_affinity((base + self.queue_id) as usize);
}
// StreamReassembler is !Send (Rc inside protolens), so create it here.
let min_sig_matches = self.min_signature_matches;
let is_ingress = self.direction == Direction::Ingress;

View File

@ -66,6 +66,7 @@ impl AppServices {
app_config.aggregator_window_secs,
app_config.flow_timeout_us,
traffic_logger,
app_config.ml_cpu,
));

View File

@ -17,6 +17,7 @@ use crate::core::infrastructure::ml_alert::MLAlert;
use crate::model::error::ml::MLError;
use crate::model::log::ml::MLLog;
use crate::model::ml_detection::{EngineStats, InferenceStats};
use crate::utils::cpu_affinity::{num_cpus, set_cpu_affinity};
use crate::utils::packet_parser::parse_packet;
pub struct Engine {
@ -29,6 +30,7 @@ pub struct Engine {
inference_interval_secs: u64,
flow_timeout_us: u64,
traffic_logger: Option<Arc<TrafficLogger>>,
ml_cpu: Option<u32>,
}
impl Engine {
@ -43,6 +45,7 @@ impl Engine {
window_secs: u64,
flow_timeout_us: u64,
traffic_logger: Option<Arc<TrafficLogger>>,
ml_cpu: Option<u32>,
) -> Self {
let tracker = Arc::new(Mutex::new(FlowTracker::new(max_flows)));
let inference_pipeline = Arc::new(Inference::new(models, config));
@ -62,6 +65,7 @@ impl Engine {
inference_interval_secs: interval_secs,
flow_timeout_us,
traffic_logger,
ml_cpu,
}
}
@ -86,21 +90,23 @@ impl Engine {
_ = ticker.tick() => {}
}
let Ok(mut t) = self.tracker.lock() else {
log!(MLError::TrackerLockPoisoned);
continue;
let (total_flows, packet_counts, flows) = {
let Ok(mut t) = self.tracker.lock() else {
log!(MLError::TrackerLockPoisoned);
continue;
};
let total_flows = t.flow_count();
let packet_counts: Vec<usize> = t.get_flows_snapshot()
.iter()
.map(|f| f.packet_count())
.collect();
let flows = if self.traffic_logger.is_some() {
t.drain_flows(self.min_packets)
} else {
t.get_flows_for_inference(self.min_packets)
};
(total_flows, packet_counts, flows)
};
let total_flows = t.flow_count();
let packet_counts: Vec<usize> = t.get_flows_snapshot()
.iter()
.map(|f| f.packet_count())
.collect();
let flows = if self.traffic_logger.is_some() {
t.drain_flows(self.min_packets)
} else {
t.get_flows_for_inference(self.min_packets)
};
drop(t);
log!(MLLog::FlowStats(
total_flows,
@ -149,12 +155,27 @@ impl Engine {
log!(MLLog::RunningInference(batch.len()));
let start = Instant::now();
let results = self.inference_pipeline.infer_batch(&batch);
let batch_len = batch.len();
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));
set_cpu_affinity(cpu);
pipeline.infer_batch(&batch)
});
let results = tokio::select! {
_ = &mut shutdown_rx => {
handle.abort();
break;
}
res = &mut handle => res.unwrap_or_default(),
};
let elapsed_us = start.elapsed().as_micros() as u64;
let stats = InferenceStats::from_results(&results, elapsed_us);
if results.len() != batch.len() {
log!(MLLog::InferenceResults(batch.len(), results.len()));
if results.len() != batch_len {
log!(MLLog::InferenceResults(batch_len, results.len()));
}
log!(MLLog::InferenceCompleted(
@ -226,4 +247,4 @@ impl Engine {
};
EngineStats { active_flows }
}
}
}

View File

@ -1,6 +1,7 @@
use std::collections::VecDeque;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use macros::log;
use ndarray::Array3;
@ -42,11 +43,12 @@ impl Inference {
flows.iter().filter_map(|flow| self.infer_single(flow)).collect()
}
pub fn infer_single(&self, flow: &FlowData) -> Option<DetectionResult> {
fn infer_single(&self, flow: &FlowData) -> Option<DetectionResult> {
let t0 = Instant::now();
let features = self.preprocess_ae_features(flow);
let t1 = Instant::now();
let window_size = self.config.window_size;
// Update per-src_ip buffer
let sequence = {
let Ok(mut buffers) = self.flow_buffers.lock() else {
log!(MLError::InferenceLockPoisoned);
@ -56,43 +58,24 @@ impl Inference {
.entry(flow.flow_key.src_ip.clone())
.or_insert_with(VecDeque::new);
buf.push_back(features.clone());
buf.push_back(features);
if buf.len() > window_size {
buf.pop_front();
}
println!(
"Buffer [{}->{}]: {}/{} | contents: {:?}",
flow.flow_key.src_ip,
flow.flow_key.dst_ip,
buf.len(),
window_size,
buf.iter().map(|v| format!("{:.3}", v[0])).collect::<Vec<_>>()
);
if buf.len() < window_size {
return None;
}
// Left-pad with zeros if not enough history
let mut seq: Vec<Vec<f32>> = Vec::with_capacity(window_size);
let pad_len = window_size.saturating_sub(buf.len());
let feat_len = self.config.num_ae_features();
for _ in 0..pad_len {
seq.push(vec![0.0f32; feat_len]);
}
for v in buf.iter() {
seq.push(v.clone());
}
seq
buf.iter().cloned().collect::<Vec<_>>()
};
// Build 3D tensor (1, window_size, features)
let t2 = Instant::now();
let feat_len = self.config.num_ae_features();
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) {
Ok(score) => score,
Err(e) => {
@ -100,9 +83,17 @@ impl Inference {
return None;
}
};
let t4 = Instant::now();
log!(MLLog::InferenceTiming(
flow.flow_key.src_ip.clone(),
t1.duration_since(t0).as_millis() as u64,
t2.duration_since(t1).as_millis() as u64,
t3.duration_since(t2).as_millis() as u64,
t4.duration_since(t3).as_millis() as u64,
));
let is_attack = ae_score >= self.config.ae_threshold;
let flow_key = format!(
"{}:{} -> {}:{} (proto {}) [{}]",
flow.flow_key.src_ip,
@ -145,7 +136,6 @@ impl Inference {
let output = outputs[0].try_extract_array::<f32>().map_err(|e| e.to_string())?;
// MSE between input and reconstructed output
let input_dyn = input.view().into_dyn();
let diff = &input_dyn - &output;
let n = (self.config.window_size * self.config.num_ae_features()) as f32;
@ -153,4 +143,4 @@ impl Inference {
Ok(mse)
}
}
}

View File

@ -27,7 +27,7 @@ impl MLModels {
Session::builder()
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?
.with_optimization_level(GraphOptimizationLevel::Disable)
.with_optimization_level(GraphOptimizationLevel::All)
.map_err(|_| MLError::ModelLoadFailed { path: model_path.clone() })?
.commit_from_file(&model_path)
.map_err(|_| MLError::ModelLoadFailed { path: model_path })

View File

@ -12,5 +12,7 @@ async fn main() -> Result<(), Error> {
let mut system = System::new().await?;
system.run().await?;
system.terminate().await?;
Ok(())
}
drop(system);
std::process::exit(0);
}

View File

@ -37,4 +37,11 @@ pub struct Config {
/// Only useful in [external]->NetGuardia->[internal] deployments where the
/// internal server can be configured to write TLS session keys.
pub tls_keylog_path: Option<String>,
/// First CPU core assigned to XSK packet threads. Each queue pair gets one core
/// starting from this base (e.g. base=0 with 4 queues pins to cores 0-3).
/// If absent, no affinity is set.
pub xsk_cpu_base: Option<u32>,
/// CPU core pinned to the ML inference spawn_blocking thread.
/// If absent, defaults to the last available core.
pub ml_cpu: Option<u32>,
}

View File

@ -72,5 +72,8 @@ loggable! {
#[error("CSV rotated to: {path}")]
TrafficLogRotated { path: String } => tracing::Level::INFO,
#[error("Timing [{src}]: feature={feature_ms}ms buffer={buffer_ms}ms tensor={tensor_ms}ms onnx={onnx_ms}ms")]
InferenceTiming { src: String, feature_ms: u64, buffer_ms: u64, tensor_ms: u64, onnx_ms: u64 } => tracing::Level::INFO,
}
}

View File

@ -2,5 +2,6 @@ pub mod logging;
pub mod static_files;
pub mod boot_time;
pub mod packet_parser;
pub mod cpu_affinity;
pub mod ip_address;