feat: frontend overhaul, design system, CI fix

Frontend (submodule update):
- JWT authentication with login page and route protection
- WebSocket refactor: 26 connections → 4 with subscription filtering
- All API paths migrated from /ebpf/ to /api/ with JWT headers
- 6 new pages: drops, geo-block, dns-filter, rate-limit,
  protocol-filter, system settings
- 3-group sidebar navigation (監控/安全/系統)
- Updated all existing pages to new backend API

Design system:
- DESIGN.md: Industrial/Utilitarian aesthetic, Geist + JetBrains Mono,
  Slate palette, compact spacing, accessibility specs
- CLAUDE.md: design system reference for future work
- TODOS.md: implementation tracking

CI fix:
- Add Node.js 22 setup + npm install for frontend build in build.rs
- Fix bpf-linker resolution: find_bpf_linker() in net-guardia/build.rs
  resolves path and passes via CARGO_TARGET_BPFEL_UNKNOWN_NONE_LINKER
  env var to eBPF subprocess (no more PATH guessing)
- Add which crate to net-guardia build-dependencies
- Force-install bpf-linker to avoid stale cache false positive
- Set stable as default toolchain so clippy runs on stable
- Make ingress/egress-ebpf build.rs non-fatal on which() failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
DaLaw2 2026-03-22 03:17:22 +08:00
parent 88b49d7493
commit 89cdbff542
25 changed files with 159 additions and 167 deletions

View File

@ -29,32 +29,47 @@ jobs:
gcc m4 clang llvm \
libelf-dev zlib1g-dev pkg-config
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: net-guardia-frontend/package-lock.json
- name: Install frontend dependencies
run: npm install
working-directory: net-guardia-frontend
- name: Install Rust stable toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install Rust nightly toolchain (for bpf-linker)
- name: Install Rust nightly toolchain (for eBPF)
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
# Ensure stable is the default so cargo check/test/clippy use stable.
# Nightly is only needed for the eBPF subprocess (which uses
# rust-toolchain.toml in ingress-ebpf/egress-ebpf).
- name: Set stable as default toolchain
run: rustup default stable
- 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
cargo binstall bpf-linker --no-confirm --force || cargo install bpf-linker --locked
else
cargo install bpf-linker --locked
fi
ls -la "$HOME/.cargo/bin/bpf-linker"
timeout-minutes: 45
- name: cargo check
@ -76,17 +91,6 @@ jobs:
- 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 "============================================"

3
.gitignore vendored
View File

@ -33,3 +33,6 @@ license-generator/target/
license.key
license_priv.key
license_pub.key
.gstack/
interfaces.txt
traffic_log.csv

1
Cargo.lock generated
View File

@ -2009,6 +2009,7 @@ dependencies = [
"tracing-subscriber",
"tract-onnx",
"url",
"which",
"xsk-rs",
]

View File

@ -1,4 +1,4 @@
#[cfg(all(feature = "user"))]
#[cfg(feature = "user")]
use std::vec::Vec;
#[cfg(feature = "user")]
@ -23,7 +23,7 @@ pub enum HttpMethod {
#[cfg(feature = "user")]
impl HttpMethod {
pub fn convert_from_bitmap(http_method_bitmap: HttpMethodBitmap) -> Vec<HttpMethod> {
let value = http_method_bitmap as u16;
let value = http_method_bitmap;
let mut http_methods = Vec::new();
let all_methods = [

View File

@ -1,6 +1,8 @@
use which::which;
fn main() {
let bpf_linker = which("bpf-linker").unwrap();
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
// bpf-linker path is resolved and injected by net-guardia/build.rs
// via CARGO_TARGET_BPFEB_UNKNOWN_NONE_LINKER env var.
// This build.rs only needs to exist for cargo to run it.
if let Ok(linker) = which::which("bpf-linker") {
println!("cargo:rerun-if-changed={}", linker.display());
}
}

View File

@ -1,6 +1,8 @@
use which::which;
fn main() {
let bpf_linker = which("bpf-linker").unwrap();
println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
// bpf-linker path is resolved and injected by net-guardia/build.rs
// via CARGO_TARGET_BPFEL_UNKNOWN_NONE_LINKER env var.
// This build.rs only needs to exist for cargo to run it.
if let Ok(linker) = which::which("bpf-linker") {
println!("cargo:rerun-if-changed={}", linker.display());
}
}

View File

@ -198,7 +198,7 @@ pub fn generate_error_enum(input: TokenStream, force_no_source: bool) -> TokenSt
});
let expanded = quote! {
#[allow(dead_code)]
#[allow(dead_code, clippy::enum_variant_names)]
#[derive(Debug, Clone, thiserror::Error, serde::Serialize, serde::Deserialize)]
pub enum #enum_name {
#(#enum_variants,)*

@ -1 +1 @@
Subproject commit 55da05e710cb00bbc9bef85804667624b0ddf377
Subproject commit 599f170385f6c36071dbb7be4664db7edfdcdf3e

View File

@ -71,6 +71,7 @@ license = ["dep:ed25519-dalek", "dep:base64"]
[build-dependencies]
cargo_metadata = { workspace = true }
which = { workspace = true }
dotenvy = "0.15.7"
[[bin]]

View File

@ -15,7 +15,6 @@ fn main() {
}
fn embed_license_public_key() {
// Only needed when license feature is enabled
let license_enabled = env::var("CARGO_FEATURE_LICENSE").is_ok();
let project_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
@ -40,11 +39,34 @@ fn embed_license_public_key() {
key_path.display()
);
} else {
// Feature not enabled, set clearly invalid placeholder (won't be used)
println!("cargo:rustc-env=LICENSE_PUBLIC_KEY=DISABLED");
}
}
/// Resolve the absolute path of bpf-linker.
/// Searches PATH first, then falls back to CARGO_HOME/bin.
fn find_bpf_linker() -> PathBuf {
// Try PATH via which
if let Ok(path) = which::which("bpf-linker") {
return path;
}
// Fallback: CARGO_HOME/bin (handles CI cache + which v8 issues)
let cargo_home = env::var("CARGO_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap_or_default();
format!("{home}/.cargo")
});
let candidate = PathBuf::from(format!("{cargo_home}/bin/bpf-linker"));
if candidate.exists() {
return candidate;
}
panic!(
"bpf-linker not found in PATH or $CARGO_HOME/bin.\n\
Install with: cargo install bpf-linker"
);
}
fn build_ebpf_package(package_name: &str, target_subdir: &str) {
let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap();
let ebpf_package = packages
@ -67,9 +89,13 @@ fn build_ebpf_package(package_name: &str, target_subdir: &str) {
let build_ebpf = true;
if build_ebpf {
let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap();
let target = format!("{target}-unknown-none");
// Find bpf-linker once, pass its path to the subprocess explicitly.
let bpf_linker = find_bpf_linker();
let bpf_linker_str = bpf_linker.to_str()
.expect("bpf-linker path is not valid UTF-8");
let Package { manifest_path, .. } = ebpf_package;
let ebpf_dir = manifest_path.parent().unwrap();
@ -90,6 +116,13 @@ fn build_ebpf_package(package_name: &str, target_subdir: &str) {
cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch);
// Tell cargo which linker to use for the BPF targets.
// This avoids relying on PATH in the subprocess.
let linker_env_bpfel = "CARGO_TARGET_BPFEL_UNKNOWN_NONE_LINKER";
let linker_env_bpfeb = "CARGO_TARGET_BPFEB_UNKNOWN_NONE_LINKER";
cmd.env(linker_env_bpfel, bpf_linker_str);
cmd.env(linker_env_bpfeb, bpf_linker_str);
for key in ["RUSTUP_TOOLCHAIN", "RUSTC", "RUSTC_WORKSPACE_WRAPPER"] {
cmd.env_remove(key);
}
@ -168,10 +201,6 @@ fn build_ebpf_package(package_name: &str, target_subdir: &str) {
fn build_frontend() {
let _ = dotenvy::dotenv();
// let Some(frontend_dir) = env::var_os("FRONTEND_DIR") else {
// panic!("FRONTEND_DIR environment variable is required but not set");
// };
let project_root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let static_dir = project_root.join("static").join("web");
@ -188,26 +217,11 @@ fn build_frontend() {
println!("cargo:rerun-if-changed={}", frontend_dir.join("src").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("public").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("package.json").display());
println!(
"cargo:rerun-if-changed={}",
frontend_dir.join("package-lock.json").display()
);
println!(
"cargo:rerun-if-changed={}",
frontend_dir.join("next.config.js").display()
);
println!(
"cargo:rerun-if-changed={}",
frontend_dir.join("tailwind.config.js").display()
);
println!(
"cargo:rerun-if-changed={}",
frontend_dir.join("postcss.config.js").display()
);
println!(
"cargo:rerun-if-changed={}",
frontend_dir.join("tsconfig.json").display()
);
println!("cargo:rerun-if-changed={}", frontend_dir.join("package-lock.json").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("next.config.js").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("tailwind.config.js").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("postcss.config.js").display());
println!("cargo:rerun-if-changed={}", frontend_dir.join("tsconfig.json").display());
let out_dir = frontend_dir.join("out");
let need_build = needs_frontend_rebuild(&frontend_dir, &out_dir, &static_dir);
@ -215,22 +229,24 @@ fn build_frontend() {
return;
}
let mut cmd = Command::new("npm");
cmd.arg("install")
.current_dir(&frontend_dir);
let npm = which::which("npm")
.unwrap_or_else(|_| panic!("npm not found in PATH. Install Node.js first."));
let status = cmd
let status = Command::new(&npm)
.arg("install")
.current_dir(&frontend_dir)
.status()
.unwrap_or_else(|err| panic!("failed to run npm install: {err}"));
if !status.success() {
panic!("npm install failed with exit code: {:?}", status.code());
}
let mut cmd = Command::new("npx");
cmd.args(["next", "build"])
.current_dir(&frontend_dir);
let npx = which::which("npx")
.unwrap_or_else(|_| panic!("npx not found in PATH. Install Node.js first."));
let status = cmd
let status = Command::new(&npx)
.args(["next", "build"])
.current_dir(&frontend_dir)
.status()
.unwrap_or_else(|err| panic!("failed to run next build: {err}"));
if !status.success() {
@ -245,38 +261,24 @@ fn build_frontend() {
copy_dir_all(&out_dir, &static_dir).unwrap_or_else(|err| panic!("failed to copy frontend build: {err}"));
}
fn needs_frontend_rebuild(frontend_dir: &PathBuf, out_dir: &PathBuf, static_dir: &PathBuf) -> bool {
if !out_dir.exists() {
return true;
}
if !static_dir.exists() {
fn needs_frontend_rebuild(frontend_dir: &std::path::Path, out_dir: &std::path::Path, static_dir: &std::path::Path) -> bool {
if !out_dir.exists() || !static_dir.exists() {
return true;
}
let out_modified = match fs::metadata(out_dir).and_then(|m| m.modified()) {
Ok(time) => time,
Err(_) => {
return true;
}
Err(_) => return true,
};
let static_modified = match fs::metadata(static_dir).and_then(|m| m.modified()) {
Ok(time) => time,
Err(_) => {
return true;
}
Err(_) => return true,
};
let essential_items = [
"src",
"public",
"package.json",
"next.config.js",
"tailwind.config.js",
"postcss.config.js",
"tsconfig.json",
"package-lock.json",
"src", "public", "package.json", "next.config.js",
"tailwind.config.js", "postcss.config.js", "tsconfig.json", "package-lock.json",
];
for item_name in essential_items {
@ -284,49 +286,39 @@ fn needs_frontend_rebuild(frontend_dir: &PathBuf, out_dir: &PathBuf, static_dir:
if !item_path.exists() {
continue;
}
let item_modified = match get_dir_last_modified(&item_path) {
Some(time) => time,
None => continue,
};
if item_modified > out_modified {
if let Some(item_modified) = get_dir_last_modified(&item_path)
&& item_modified > out_modified
{
return true;
}
}
if out_modified > static_modified {
return true;
}
false
out_modified > static_modified
}
fn get_dir_last_modified(path: &PathBuf) -> Option<SystemTime> {
fn get_dir_last_modified(path: &std::path::Path) -> Option<SystemTime> {
if path.is_file() {
return fs::metadata(path).and_then(|m| m.modified()).ok();
}
if path.is_dir() {
let mut latest = fs::metadata(path).and_then(|m| m.modified()).ok()?;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
if let Some(modified) = get_dir_last_modified(&entry.path()) {
if modified > latest {
latest = modified;
}
if let Some(modified) = get_dir_last_modified(&entry.path())
&& modified > latest
{
latest = modified;
}
}
}
return Some(latest);
}
None
}
fn copy_dir_all(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
for entry in fs::read_dir(src)? {
let entry = entry?;
let file_type = entry.file_type()?;

View File

@ -86,7 +86,7 @@ impl Database {
Ok(())
}
pub fn load_acl_rules(&self) -> Result<Vec<(u8, String, String, String, u16)>, Error> {
pub fn load_acl_rules(&self) -> Result<Vec<crate::interface::port::repository::AclRuleTuple>, Error> {
let conn = self.conn.lock();
let mut stmt = conn.prepare("SELECT ip_version, direction, list_type, ip_address, port FROM acl_rules")?;
let rows = stmt.query_map([], |row| {
@ -201,7 +201,7 @@ impl Database {
}
// --- Users ---
pub fn find_user(&self, username: &str) -> Result<Option<(i64, String, String, String, bool)>, Error> {
pub fn find_user(&self, username: &str) -> Result<Option<crate::interface::port::repository::UserTuple>, Error> {
let conn = self.conn.lock();
let result = conn.query_row(
"SELECT id, username, password_hash, role, force_password_change FROM users WHERE username = ?1",
@ -270,8 +270,8 @@ impl Database {
pub fn check_login_locked(&self, username: &str) -> Result<Option<u64>, Error> {
let key_locked = format!("login_locked_until:{}", username);
if let Some(locked_str) = self.get_setting(&key_locked)? {
if let Ok(locked_until) = locked_str.parse::<u64>() {
if let Some(locked_str) = self.get_setting(&key_locked)?
&& let Ok(locked_until) = locked_str.parse::<u64>() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
@ -281,7 +281,6 @@ impl Database {
}
// Lock expired, clear it
self.clear_login_failures(username)?;
}
}
Ok(None)
}
@ -299,7 +298,7 @@ impl Database {
impl crate::interface::port::repository::RepositoryPort for Database {
fn insert_acl_rule(&self, ip_version: u8, direction: &str, list_type: &str, ip_address: &str, port: u16) -> Result<(), Error> { self.insert_acl_rule(ip_version, direction, list_type, ip_address, port) }
fn delete_acl_rule(&self, ip_version: u8, direction: &str, list_type: &str, ip_address: &str, port: u16) -> Result<(), Error> { self.delete_acl_rule(ip_version, direction, list_type, ip_address, port) }
fn load_acl_rules(&self) -> Result<Vec<(u8, String, String, String, u16)>, Error> { self.load_acl_rules() }
fn load_acl_rules(&self) -> Result<Vec<crate::interface::port::repository::AclRuleTuple>, Error> { self.load_acl_rules() }
fn set_rate_limit(&self, key: &str, value: u64) -> Result<(), Error> { self.set_rate_limit(key, value) }
fn load_rate_limit_config(&self) -> Result<Vec<(String, u64)>, Error> { self.load_rate_limit_config() }
fn insert_dns_domain(&self, domain: &str) -> Result<(), Error> { self.insert_dns_domain(domain) }
@ -310,7 +309,7 @@ impl crate::interface::port::repository::RepositoryPort for Database {
fn load_geo_countries(&self) -> Result<Vec<String>, Error> { self.load_geo_countries() }
fn get_setting(&self, key: &str) -> Result<Option<String>, Error> { self.get_setting(key) }
fn set_setting(&self, key: &str, value: &str) -> Result<(), Error> { self.set_setting(key, value) }
fn find_user(&self, username: &str) -> Result<Option<(i64, String, String, String, bool)>, Error> { self.find_user(username) }
fn find_user(&self, username: &str) -> Result<Option<crate::interface::port::repository::UserTuple>, Error> { self.find_user(username) }
fn insert_user(&self, username: &str, password_hash: &str, role: &str, force_password_change: bool) -> Result<(), Error> { self.insert_user(username, password_hash, role, force_password_change) }
fn update_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error> { self.update_user_password(user_id, password_hash) }
fn user_count(&self) -> Result<i64, Error> { self.user_count() }

View File

@ -35,10 +35,9 @@ pub async fn flow_stats_ws(
tokio::select! {
_ = ticker.tick() => {
let flows = stats.get_filtered_flows(&subscription);
if let Ok(json) = serde_json::to_string(&flows) {
if session.text(json).await.is_err() {
if let Ok(json) = serde_json::to_string(&flows)
&& session.text(json).await.is_err() {
break;
}
}
}
msg = msg_stream.next() => {
@ -52,10 +51,9 @@ pub async fn flow_stats_ws(
ticker = interval(Duration::from_secs(new_interval));
let flows = stats.get_filtered_flows(&subscription);
if let Ok(json) = serde_json::to_string(&flows) {
if session.text(json).await.is_err() {
if let Ok(json) = serde_json::to_string(&flows)
&& session.text(json).await.is_err() {
break;
}
}
}
Err(e) => {

View File

@ -75,8 +75,8 @@ where
let token = match auth_header {
Some(val) => {
let val_str = val.to_str().unwrap_or("");
if val_str.starts_with("Bearer ") {
&val_str[7..]
if let Some(token_str) = val_str.strip_prefix("Bearer ") {
token_str
} else {
let resp = HttpResponse::Unauthorized()
.json(serde_json::json!({"error": "Invalid authorization header"}));

View File

@ -33,7 +33,7 @@ impl DnsFilter {
self.blacklist
.read()
.iter()
.filter_map(|name| wire_format_to_domain(name))
.filter_map(wire_format_to_domain)
.collect()
}
@ -169,7 +169,7 @@ impl DnsFilter {
out += 1;
for j in 0..ll {
let mut b = raw[pos + 1 + j];
if b >= b'A' && b <= b'Z' {
if b.is_ascii_uppercase() {
b += 32;
}
name.data[out] = b;

View File

@ -70,7 +70,8 @@ impl EbpfServices {
let xsk_manager = self.xsk_manager.clone();
xsk_manager.run(Some(ml_engine), Some(self.dns_filter.clone()), &self.shutdowns)?;
if let Some(ring_buf) = self.drop_ring_buf.lock().take() {
let ring_buf = self.drop_ring_buf.lock().take();
if let Some(ring_buf) = ring_buf {
let shutdown = drop_monitor::start_consumer(ring_buf, self.drop_monitor.clone()).await;
self.shutdowns.push(shutdown);
}

View File

@ -191,11 +191,7 @@ impl WhiteListControl {
fn is_white_list_enable(&self) -> bool {
match self.map.get(&0, 0) {
Ok(status) => {
if status == 0 {
false
} else {
true
}
status != 0
}
Err(_) => false,
}
@ -253,7 +249,7 @@ impl<T: NativeConvert + Pod> HttpServiceWrapper<T> {
} else {
let new_http_method = HttpMethod::convert_to_bitmap(http_method);
self.map
.insert(&address, new_http_method, 0)
.insert(address, new_http_method, 0)
.map_err(EbpfError::MapOperationError)?;
}
Ok(())

View File

@ -314,7 +314,7 @@ impl XskPair {
for rx_desc in rx_descs.iter().take(rx_count) {
let lengths = rx_desc.lengths();
let packet_len = lengths.data() as usize;
let packet_len = lengths.data();
let data = unsafe { self.umem.data(rx_desc) };
let contents = data.contents();
@ -326,20 +326,17 @@ impl XskPair {
let raw = &contents[..packet_len];
// DNS blacklist check — drop blacklisted DNS queries before forwarding
if let Some(ref dns) = self.dns_filter {
if let Some((dns_name, name_len)) = DnsFilter::parse_query_name(raw) {
if dns.is_blacklisted(&dns_name, name_len) {
continue;
}
}
if let Some(ref dns) = self.dns_filter
&& let Some((dns_name, name_len)) = DnsFilter::parse_query_name(raw)
&& dns.is_blacklisted(&dns_name, name_len) {
continue;
}
// Parse directly from UMEM (zero-copy for ML path).
// Only clone for the forwarding path afterwards.
if let Some(ref tracker) = self.tracker {
if let Some((packet_info, _)) = parse_packet(raw) {
if let Some(ref tracker) = self.tracker
&& let Some((packet_info, _)) = parse_packet(raw) {
tracker.lock().process_packet(packet_info, is_ingress);
}
}
// Clone into pooled buffer for forwarding
@ -428,10 +425,9 @@ impl XskPair {
}
}
if let Err(e) = self.tx.wakeup() {
if e.kind() != std::io::ErrorKind::WouldBlock {
if let Err(e) = self.tx.wakeup()
&& e.kind() != std::io::ErrorKind::WouldBlock {
log!(EbpfLog::TXWakeupFailed(e.to_string()));
}
}
// Log dropped packets when frames < packets

View File

@ -1,5 +1,2 @@
pub mod report;
pub mod scheduler;
pub use report::generate_weekly_report;
pub use scheduler::ReportScheduler;

View File

@ -43,10 +43,9 @@ impl FlowFeatures {
pub fn winsorize(&mut self, clip_params: &HashMap<String, ClipParams>, feature_names: &[String]) {
for (i, feature_name) in feature_names.iter().enumerate() {
if i < self.feature_num {
if let Some(params) = clip_params.get(feature_name) {
if i < self.feature_num
&& let Some(params) = clip_params.get(feature_name) {
self.features[i] = self.features[i].clamp(params.lower, params.upper);
}
}
}
}

View File

@ -95,9 +95,8 @@ impl FlowData {
if iat > IDLE_THRESHOLD_US {
if self.idle_periods.len() < MAX_PERIODS { self.idle_periods.push(iat); }
} else if iat > 0 {
if self.active_periods.len() < MAX_PERIODS { self.active_periods.push(iat); }
}
} else if iat > 0
&& self.active_periods.len() < MAX_PERIODS { self.active_periods.push(iat); }
self.last_packet_time = packet.timestamp_us;
self.last_time_us = packet.timestamp_us;
@ -239,13 +238,12 @@ impl FlowTracker {
flow.add_packet(&packet);
if self.active.len() > self.max_flows {
if let Some(oldest_key) = self.active.iter()
if self.active.len() > self.max_flows
&& let Some(oldest_key) = self.active.iter()
.min_by_key(|(_, flow)| flow.last_time_us)
.map(|(k, _)| k.clone())
{
self.active.remove(&oldest_key);
}
{
self.active.remove(&oldest_key);
}
}

View File

@ -24,7 +24,7 @@ impl MLModels {
let load = || -> Result<RunnableModel, Box<dyn std::error::Error>> {
let mut model = onnx().model_for_path(&model_path)?;
model.set_input_fact(0, f32::fact(&[1, features]).into())?;
model.set_input_fact(0, f32::fact([1, features]).into())?;
Ok(model.into_optimized()?.into_runnable()?)
};

View File

@ -90,10 +90,9 @@ impl SystemHealth {
drop(networks);
drop(components);
if self.broadcast_tx.receiver_count() > 0 {
if let Err(e) = self.broadcast_tx.send(metrics) {
if self.broadcast_tx.receiver_count() > 0
&& let Err(e) = self.broadcast_tx.send(metrics) {
log!(Health::BroadcastFailed(e.to_string()));
}
}
}

View File

@ -174,7 +174,7 @@ impl ServiceFactory {
if stages.is_empty() {
next_stage
.set(STAGE_ENTRY as u32, STAGE_TRANSMISSION, 0)
.set(STAGE_ENTRY, STAGE_TRANSMISSION, 0)
.map_err(EbpfError::MapOperationError)?;
return Ok(program_array);
}
@ -190,7 +190,7 @@ impl ServiceFactory {
}
next_stage
.set(STAGE_ENTRY as u32, slots[0].1, 0)
.set(STAGE_ENTRY, slots[0].1, 0)
.map_err(EbpfError::MapOperationError)?;
for i in 0..slots.len() {
@ -201,7 +201,7 @@ impl ServiceFactory {
STAGE_TRANSMISSION
};
next_stage
.set(stage_id as u32, next_slot, 0)
.set(stage_id, next_slot, 0)
.map_err(EbpfError::MapOperationError)?;
}
@ -315,14 +315,13 @@ impl ServiceFactory {
}
fn restore_geo_countries(db: &Database, ebpf_services: &EbpfServices) {
if let Ok(countries) = db.load_geo_countries() {
if !countries.is_empty() {
if let Ok(countries) = db.load_geo_countries()
&& !countries.is_empty() {
if let Err(e) = ebpf_services.geo_block.block_countries(&countries) {
tracing::warn!("Failed to restore geo-blocked countries: {}", e);
} else {
tracing::info!("Restored {} geo-blocked countries from database", countries.len());
}
}
}
}

View File

@ -1,4 +1,3 @@
use crate::model::error::Error;
use crate::model::health::{SystemHealthMetrics, SystemHealthStatus};
use async_trait::async_trait;

View File

@ -1,12 +1,18 @@
use crate::model::error::Error;
/// Type alias for ACL rule tuples: (ip_version, direction, list_type, ip_address, port)
pub type AclRuleTuple = (u8, String, String, String, u16);
/// Type alias for user record tuples: (id, username, password_hash, role, force_password_change)
pub type UserTuple = (i64, String, String, String, bool);
/// Port for persistent storage operations.
/// Adapters: SQLite (current), could be Postgres, etc.
pub trait RepositoryPort: Send + Sync {
// --- ACL ---
fn insert_acl_rule(&self, ip_version: u8, direction: &str, list_type: &str, ip_address: &str, port: u16) -> Result<(), Error>;
fn delete_acl_rule(&self, ip_version: u8, direction: &str, list_type: &str, ip_address: &str, port: u16) -> Result<(), Error>;
fn load_acl_rules(&self) -> Result<Vec<(u8, String, String, String, u16)>, Error>;
fn load_acl_rules(&self) -> Result<Vec<AclRuleTuple>, Error>;
// --- Rate Limit ---
fn set_rate_limit(&self, key: &str, value: u64) -> Result<(), Error>;
@ -27,7 +33,7 @@ pub trait RepositoryPort: Send + Sync {
fn set_setting(&self, key: &str, value: &str) -> Result<(), Error>;
// --- Users ---
fn find_user(&self, username: &str) -> Result<Option<(i64, String, String, String, bool)>, Error>;
fn find_user(&self, username: &str) -> Result<Option<UserTuple>, Error>;
fn insert_user(&self, username: &str, password_hash: &str, role: &str, force_password_change: bool) -> Result<(), Error>;
fn update_user_password(&self, user_id: i64, password_hash: &str) -> Result<(), Error>;
fn user_count(&self) -> Result<i64, Error>;