mirror of
https://github.com/xzcrpw/blackwall.git
synced 2026-04-24 11:56:21 +02:00
v2.0.0: adaptive eBPF firewall with AI honeypot and P2P threat mesh
This commit is contained in:
commit
37c6bbf5a1
133 changed files with 28073 additions and 0 deletions
17
common/Cargo.toml
Executable file
17
common/Cargo.toml
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["user", "aya"]
|
||||
user = ["dep:serde", "dep:serde_json"]
|
||||
aya = ["dep:aya", "user"]
|
||||
|
||||
[dependencies]
|
||||
aya = { version = "0.13", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
157
common/src/base64.rs
Executable file
157
common/src/base64.rs
Executable file
|
|
@ -0,0 +1,157 @@
|
|||
//! Minimal base64/base64url codec — no external crates.
|
||||
//!
|
||||
//! Used by the A2A firewall (JWT parsing, PoP verification) and
|
||||
//! the A2A shim (token encoding). A single implementation avoids
|
||||
//! drift between the three nearly-identical copies that existed before.
|
||||
|
||||
/// Standard base64 alphabet lookup table (6-bit value per ASCII char).
|
||||
const DECODE_TABLE: &[u8; 128] = &{
|
||||
let mut t = [255u8; 128];
|
||||
let mut i = 0u8;
|
||||
while i < 26 {
|
||||
t[(b'A' + i) as usize] = i;
|
||||
t[(b'a' + i) as usize] = i + 26;
|
||||
i += 1;
|
||||
}
|
||||
let mut i = 0u8;
|
||||
while i < 10 {
|
||||
t[(b'0' + i) as usize] = i + 52;
|
||||
i += 1;
|
||||
}
|
||||
t[b'+' as usize] = 62;
|
||||
t[b'/' as usize] = 63;
|
||||
t
|
||||
};
|
||||
|
||||
/// Base64url alphabet for encoding (RFC 4648 §5, no padding).
|
||||
const ENCODE_TABLE: &[u8; 64] =
|
||||
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
|
||||
/// Decode a base64url string (no padding required) into bytes.
|
||||
///
|
||||
/// Handles the URL-safe alphabet (`-` → `+`, `_` → `/`) and adds
|
||||
/// padding automatically before delegating to the standard decoder.
|
||||
pub fn decode_base64url(input: &str) -> Result<Vec<u8>, &'static str> {
|
||||
// Add padding if needed
|
||||
let padded = match input.len() % 4 {
|
||||
2 => format!("{input}=="),
|
||||
3 => format!("{input}="),
|
||||
0 => input.to_string(),
|
||||
_ => return Err("invalid base64url length"),
|
||||
};
|
||||
|
||||
// Convert base64url → standard base64
|
||||
let standard: String = padded
|
||||
.chars()
|
||||
.map(|c| match c {
|
||||
'-' => '+',
|
||||
'_' => '/',
|
||||
other => other,
|
||||
})
|
||||
.collect();
|
||||
|
||||
decode_base64_standard(&standard)
|
||||
}
|
||||
|
||||
/// Decode a standard base64 string (with `=` padding) into bytes.
|
||||
pub fn decode_base64_standard(input: &str) -> Result<Vec<u8>, &'static str> {
|
||||
let bytes = input.as_bytes();
|
||||
let len = bytes.len();
|
||||
if !len.is_multiple_of(4) {
|
||||
return Err("invalid base64 length");
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(len / 4 * 3);
|
||||
let mut i = 0;
|
||||
while i < len {
|
||||
let a = bytes[i];
|
||||
let b = bytes[i + 1];
|
||||
let c = bytes[i + 2];
|
||||
let d = bytes[i + 3];
|
||||
|
||||
let va = if a == b'=' { 0 } else if a > 127 { return Err("invalid char") } else { DECODE_TABLE[a as usize] };
|
||||
let vb = if b == b'=' { 0 } else if b > 127 { return Err("invalid char") } else { DECODE_TABLE[b as usize] };
|
||||
let vc = if c == b'=' { 0 } else if c > 127 { return Err("invalid char") } else { DECODE_TABLE[c as usize] };
|
||||
let vd = if d == b'=' { 0 } else if d > 127 { return Err("invalid char") } else { DECODE_TABLE[d as usize] };
|
||||
|
||||
if va == 255 || vb == 255 || vc == 255 || vd == 255 {
|
||||
return Err("invalid base64 character");
|
||||
}
|
||||
|
||||
let triple = (va as u32) << 18 | (vb as u32) << 12 | (vc as u32) << 6 | (vd as u32);
|
||||
out.push((triple >> 16) as u8);
|
||||
if c != b'=' {
|
||||
out.push((triple >> 8) as u8);
|
||||
}
|
||||
if d != b'=' {
|
||||
out.push(triple as u8);
|
||||
}
|
||||
i += 4;
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Encode bytes to base64url (no padding, RFC 4648 §5).
|
||||
pub fn encode_base64url(input: &[u8]) -> String {
|
||||
let mut out = String::with_capacity((input.len() * 4 / 3) + 4);
|
||||
for chunk in input.chunks(3) {
|
||||
let b0 = chunk[0] as u32;
|
||||
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
|
||||
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
|
||||
let triple = (b0 << 16) | (b1 << 8) | b2;
|
||||
|
||||
out.push(ENCODE_TABLE[((triple >> 18) & 0x3F) as usize] as char);
|
||||
out.push(ENCODE_TABLE[((triple >> 12) & 0x3F) as usize] as char);
|
||||
if chunk.len() > 1 {
|
||||
out.push(ENCODE_TABLE[((triple >> 6) & 0x3F) as usize] as char);
|
||||
}
|
||||
if chunk.len() > 2 {
|
||||
out.push(ENCODE_TABLE[(triple & 0x3F) as usize] as char);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let data = b"Hello, Blackwall!";
|
||||
let encoded = encode_base64url(data);
|
||||
let decoded = decode_base64url(&encoded).unwrap();
|
||||
assert_eq!(decoded, data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_base64_padding() {
|
||||
// "Man" → "TWFu"
|
||||
assert_eq!(decode_base64_standard("TWFu").unwrap(), b"Man");
|
||||
// "Ma" → "TWE="
|
||||
assert_eq!(decode_base64_standard("TWE=").unwrap(), b"Ma");
|
||||
// "M" → "TQ=="
|
||||
assert_eq!(decode_base64_standard("TQ==").unwrap(), b"M");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_safe_chars() {
|
||||
// '+' and '/' in standard → '-' and '_' in url-safe
|
||||
let standard = "ab+c/d==";
|
||||
let url_safe = "ab-c_d";
|
||||
let decode_std = decode_base64_standard(standard).unwrap();
|
||||
let decode_url = decode_base64url(url_safe).unwrap();
|
||||
assert_eq!(decode_std, decode_url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_length() {
|
||||
assert!(decode_base64url("A").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_char() {
|
||||
assert!(decode_base64_standard("!!!!").is_err());
|
||||
}
|
||||
}
|
||||
407
common/src/hivemind.rs
Executable file
407
common/src/hivemind.rs
Executable file
|
|
@ -0,0 +1,407 @@
|
|||
//! HiveMind Threat Mesh — shared types for P2P threat intelligence.
|
||||
//!
|
||||
//! These types are used by both the hivemind daemon and potentially
|
||||
//! by eBPF programs that feed threat data into the mesh.
|
||||
|
||||
/// Maximum length of a JA4 fingerprint string (e.g., "t13d1516h2_8daaf6152771_e5627efa2ab1").
|
||||
pub const JA4_FINGERPRINT_LEN: usize = 36;
|
||||
|
||||
/// Maximum length of a threat description.
|
||||
pub const THREAT_DESC_LEN: usize = 128;
|
||||
|
||||
/// Maximum bootstrap nodes in configuration.
|
||||
pub const MAX_BOOTSTRAP_NODES: usize = 16;
|
||||
|
||||
/// Default GossipSub fan-out.
|
||||
pub const GOSSIPSUB_FANOUT: usize = 10;
|
||||
|
||||
/// Default GossipSub heartbeat interval in seconds.
|
||||
pub const GOSSIPSUB_HEARTBEAT_SECS: u64 = 1;
|
||||
|
||||
/// Default Kademlia query timeout in seconds.
|
||||
pub const KADEMLIA_QUERY_TIMEOUT_SECS: u64 = 60;
|
||||
|
||||
/// Maximum GossipSub message size (64 KB).
|
||||
pub const MAX_MESSAGE_SIZE: usize = 65536;
|
||||
|
||||
/// GossipSub message deduplication TTL in seconds.
|
||||
pub const MESSAGE_DEDUP_TTL_SECS: u64 = 120;
|
||||
|
||||
/// Kademlia k-bucket size.
|
||||
pub const K_BUCKET_SIZE: usize = 20;
|
||||
|
||||
/// Severity level of a threat indicator.
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ThreatSeverity {
|
||||
/// Informational — low confidence or low impact.
|
||||
Info = 0,
|
||||
/// Low — minor scanning or recon activity.
|
||||
Low = 1,
|
||||
/// Medium — active probing or known-bad pattern.
|
||||
Medium = 2,
|
||||
/// High — confirmed malicious with high confidence.
|
||||
High = 3,
|
||||
/// Critical — active exploitation or C2 communication.
|
||||
Critical = 4,
|
||||
}
|
||||
|
||||
impl ThreatSeverity {
|
||||
/// Convert raw u8 to ThreatSeverity.
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
0 => ThreatSeverity::Info,
|
||||
1 => ThreatSeverity::Low,
|
||||
2 => ThreatSeverity::Medium,
|
||||
3 => ThreatSeverity::High,
|
||||
4 => ThreatSeverity::Critical,
|
||||
_ => ThreatSeverity::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type of Indicator of Compromise.
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IoCType {
|
||||
/// IPv4 address associated with malicious activity.
|
||||
MaliciousIp = 0,
|
||||
/// JA4 TLS fingerprint of known malware/tool.
|
||||
Ja4Fingerprint = 1,
|
||||
/// High-entropy payload pattern (encrypted C2, exfil).
|
||||
EntropyAnomaly = 2,
|
||||
/// DNS tunneling indicator.
|
||||
DnsTunnel = 3,
|
||||
/// Behavioral pattern (port scan, brute force).
|
||||
BehavioralPattern = 4,
|
||||
}
|
||||
|
||||
impl IoCType {
|
||||
/// Convert raw u8 to IoCType.
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
0 => IoCType::MaliciousIp,
|
||||
1 => IoCType::Ja4Fingerprint,
|
||||
2 => IoCType::EntropyAnomaly,
|
||||
3 => IoCType::DnsTunnel,
|
||||
4 => IoCType::BehavioralPattern,
|
||||
_ => IoCType::MaliciousIp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicator of Compromise — the core unit of threat intelligence shared
|
||||
/// across the HiveMind mesh. Designed for GossipSub transmission.
|
||||
///
|
||||
/// This is a userspace-only type (not eBPF), so we use std types freely.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct IoC {
|
||||
/// Type of indicator.
|
||||
pub ioc_type: u8,
|
||||
/// Severity level.
|
||||
pub severity: u8,
|
||||
/// IPv4 address (if applicable, 0 otherwise).
|
||||
pub ip: u32,
|
||||
/// JA4 fingerprint string (if applicable).
|
||||
pub ja4: Option<String>,
|
||||
/// Byte diversity score (unique_count × 31, if applicable).
|
||||
pub entropy_score: Option<u32>,
|
||||
/// Human-readable description.
|
||||
pub description: String,
|
||||
/// Unix timestamp when this IoC was first observed.
|
||||
pub first_seen: u64,
|
||||
/// Number of independent peers that confirmed this IoC.
|
||||
pub confirmations: u32,
|
||||
/// ZKP proof blob (empty until Phase 1 implementation).
|
||||
pub zkp_proof: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A threat report broadcast via GossipSub. Contains one or more IoCs
|
||||
/// from a single reporting node.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ThreatReport {
|
||||
/// Unique report ID (UUID v4 bytes).
|
||||
pub report_id: [u8; 16],
|
||||
/// Reporter's Ed25519 public key (32 bytes).
|
||||
pub reporter_pubkey: [u8; 32],
|
||||
/// Unix timestamp of the report.
|
||||
pub timestamp: u64,
|
||||
/// List of IoCs in this report.
|
||||
pub indicators: Vec<IoC>,
|
||||
/// Ed25519 signature over the serialized indicators.
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
/// GossipSub topic identifiers for HiveMind.
|
||||
pub mod topics {
|
||||
/// IoC broadcast topic — new threat indicators.
|
||||
pub const IOC_TOPIC: &str = "hivemind/ioc/v1";
|
||||
/// JA4 fingerprint sharing topic.
|
||||
pub const JA4_TOPIC: &str = "hivemind/ja4/v1";
|
||||
/// Federated learning gradient exchange topic.
|
||||
pub const GRADIENT_TOPIC: &str = "hivemind/federated/gradients/v1";
|
||||
/// Peer heartbeat / presence topic.
|
||||
pub const HEARTBEAT_TOPIC: &str = "hivemind/heartbeat/v1";
|
||||
/// A2A violation proof sharing topic.
|
||||
pub const A2A_VIOLATIONS_TOPIC: &str = "hivemind/a2a-violations/v1";
|
||||
}
|
||||
|
||||
/// Port for local proof ingestion IPC (enterprise module → hivemind).
|
||||
///
|
||||
/// Hivemind listens on `127.0.0.1:PROOF_INGEST_PORT` for length-prefixed
|
||||
/// proof envelopes from the enterprise daemon (optional) on the same machine.
|
||||
pub const PROOF_INGEST_PORT: u16 = 9821;
|
||||
|
||||
/// Port for local IoC injection (testing/integration).
|
||||
///
|
||||
/// Hivemind listens on `127.0.0.1:IOC_INJECT_PORT` for length-prefixed
|
||||
/// IoC JSON payloads. The injected IoC is published to GossipSub and
|
||||
/// submitted to local consensus with the node's own pubkey.
|
||||
pub const IOC_INJECT_PORT: u16 = 9822;
|
||||
|
||||
// --- Phase 1: Anti-Poisoning Constants ---
|
||||
|
||||
/// Initial reputation stake for new peers.
|
||||
///
|
||||
/// Set BELOW MIN_TRUSTED_REPUTATION so new peers must earn trust through
|
||||
/// accurate reports before participating in consensus. Prevents Sybil
|
||||
/// attacks where fresh peers immediately inject false IoCs.
|
||||
pub const INITIAL_STAKE: u64 = 30;
|
||||
|
||||
/// Minimum reputation score to be considered trusted.
|
||||
pub const MIN_TRUSTED_REPUTATION: u64 = 50;
|
||||
|
||||
/// Initial reputation stake for explicitly configured seed peers.
|
||||
/// Seed peers start trusted to bootstrap the consensus network.
|
||||
pub const SEED_PEER_STAKE: u64 = 100;
|
||||
|
||||
/// Slashing penalty for submitting a false IoC (% of stake).
|
||||
pub const SLASHING_PENALTY_PERCENT: u64 = 25;
|
||||
|
||||
/// Reward for accurate IoC report (stake units).
|
||||
pub const ACCURACY_REWARD: u64 = 5;
|
||||
|
||||
/// Minimum independent peer confirmations to accept an IoC.
|
||||
pub const CROSS_VALIDATION_THRESHOLD: usize = 3;
|
||||
|
||||
/// Time window (seconds) for pending IoC cross-validation before expiry.
|
||||
pub const CONSENSUS_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
/// Proof-of-Work difficulty for new peer registration (leading zero bits).
|
||||
pub const POW_DIFFICULTY_BITS: u32 = 20;
|
||||
|
||||
/// Maximum new peer registrations per minute (rate limit).
|
||||
pub const MAX_PEER_REGISTRATIONS_PER_MINUTE: usize = 10;
|
||||
|
||||
/// PoW challenge freshness window (seconds).
|
||||
pub const POW_CHALLENGE_TTL_SECS: u64 = 120;
|
||||
|
||||
// --- Phase 1: ZKP Stub Types ---
|
||||
|
||||
/// A zero-knowledge proof that a threat was observed without revealing
|
||||
/// raw packet data. Stub type for Phase 0-1 interface stability.
|
||||
///
|
||||
/// In future phases, this will contain a bellman/arkworks SNARK proof.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ThreatProof {
|
||||
/// Version of the proof format (for forward compatibility).
|
||||
pub version: u8,
|
||||
/// The statement being proven (what claims are made).
|
||||
pub statement: ProofStatement,
|
||||
/// Opaque proof bytes. Empty = stub, non-empty = real SNARK proof.
|
||||
pub proof_data: Vec<u8>,
|
||||
/// Unix timestamp when proof was generated.
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
/// What a ZKP proof claims to demonstrate, without revealing private inputs.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProofStatement {
|
||||
/// JA4 fingerprint hash that was matched (public output).
|
||||
pub ja4_hash: Option<[u8; 32]>,
|
||||
/// Whether entropy exceeded the anomaly threshold (public output).
|
||||
pub entropy_exceeded: bool,
|
||||
/// Whether the behavioral classifier labeled this as malicious.
|
||||
pub classified_malicious: bool,
|
||||
/// IoC type this proof covers.
|
||||
pub ioc_type: u8,
|
||||
}
|
||||
|
||||
/// Proof-of-Work challenge for Sybil resistance.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PowChallenge {
|
||||
/// The peer's public key (32 bytes Ed25519).
|
||||
pub peer_pubkey: [u8; 32],
|
||||
/// Nonce found by the peer that satisfies difficulty.
|
||||
pub nonce: u64,
|
||||
/// Timestamp when the PoW was computed.
|
||||
pub timestamp: u64,
|
||||
/// Difficulty in leading zero bits.
|
||||
pub difficulty: u32,
|
||||
}
|
||||
|
||||
/// Peer reputation record shared across the mesh.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PeerReputationRecord {
|
||||
/// Ed25519 public key of the peer (32 bytes).
|
||||
pub peer_pubkey: [u8; 32],
|
||||
/// Current stake (starts at INITIAL_STAKE).
|
||||
pub stake: u64,
|
||||
/// Cumulative accuracy score (accurate reports).
|
||||
pub accurate_reports: u64,
|
||||
/// Count of false positives flagged by consensus.
|
||||
pub false_reports: u64,
|
||||
/// Unix timestamp of last activity.
|
||||
pub last_active: u64,
|
||||
}
|
||||
|
||||
// --- Phase 2: Federated Learning Constants ---
|
||||
|
||||
/// Interval between federated learning aggregation rounds (seconds).
|
||||
pub const FL_ROUND_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
/// Minimum peers required to run an aggregation round.
|
||||
pub const FL_MIN_PEERS_PER_ROUND: usize = 3;
|
||||
|
||||
/// Maximum serialized gradient payload size (bytes). 16 KB.
|
||||
pub const FL_MAX_GRADIENT_SIZE: usize = 16384;
|
||||
|
||||
/// Percentage of extreme values to trim in Byzantine-resistant FedAvg.
|
||||
/// Trims top and bottom 20% of gradient contributions per dimension.
|
||||
pub const FL_BYZANTINE_TRIM_PERCENT: usize = 20;
|
||||
|
||||
/// Feature vector dimension for the local NIDS model.
|
||||
pub const FL_FEATURE_DIM: usize = 32;
|
||||
|
||||
/// Hidden layer size for the local NIDS model.
|
||||
pub const FL_HIDDEN_DIM: usize = 16;
|
||||
|
||||
/// Z-score threshold × 1000 for gradient anomaly detection.
|
||||
/// Value of 3000 means z-score > 3.0 triggers alarm.
|
||||
pub const GRADIENT_ANOMALY_ZSCORE_THRESHOLD: u64 = 3000;
|
||||
|
||||
/// Maximum gradient norm (squared, integer) before rejection.
|
||||
/// Prevents gradient explosion attacks.
|
||||
pub const FL_MAX_GRADIENT_NORM_SQ: u64 = 1_000_000;
|
||||
|
||||
// --- Phase 2: Federated Learning Types ---
|
||||
|
||||
/// Encrypted gradient update broadcast via GossipSub.
|
||||
///
|
||||
/// Privacy invariant: raw gradients NEVER leave the node.
|
||||
/// Only FHE-encrypted ciphertext is transmitted.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct GradientUpdate {
|
||||
/// Reporter's Ed25519 public key (32 bytes).
|
||||
pub peer_pubkey: [u8; 32],
|
||||
/// Aggregation round identifier.
|
||||
pub round_id: u64,
|
||||
/// FHE-encrypted gradient payload. Raw gradients NEVER transmitted.
|
||||
pub encrypted_gradients: Vec<u8>,
|
||||
/// Unix timestamp of gradient computation.
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// Result of a federated aggregation round.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AggregatedModel {
|
||||
/// Aggregation round identifier.
|
||||
pub round_id: u64,
|
||||
/// Aggregated model weights (after FedAvg).
|
||||
pub weights: Vec<f32>,
|
||||
/// Number of peers that contributed to this round.
|
||||
pub participant_count: usize,
|
||||
/// Unix timestamp of aggregation completion.
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
// --- Phase 3: Enterprise Threat Feed Constants ---
|
||||
|
||||
/// Default HTTP port for the Enterprise Threat Feed API.
|
||||
///
|
||||
/// Uses 8090 (not 8443) because all traffic is plaintext HTTP on loopback.
|
||||
/// Port 8443 conventionally implies HTTPS and would be misleading.
|
||||
pub const API_DEFAULT_PORT: u16 = 8090;
|
||||
|
||||
/// Default API listen address.
|
||||
pub const API_DEFAULT_ADDR: &str = "127.0.0.1";
|
||||
|
||||
/// Maximum IoCs returned per API request.
|
||||
pub const API_MAX_PAGE_SIZE: usize = 1000;
|
||||
|
||||
/// Default IoCs per page in API responses.
|
||||
pub const API_DEFAULT_PAGE_SIZE: usize = 100;
|
||||
|
||||
/// API key length in bytes (hex-encoded = 64 chars).
|
||||
pub const API_KEY_LENGTH: usize = 32;
|
||||
|
||||
/// TAXII 2.1 content type header value.
|
||||
pub const TAXII_CONTENT_TYPE: &str = "application/taxii+json;version=2.1";
|
||||
|
||||
/// STIX 2.1 content type header value.
|
||||
pub const STIX_CONTENT_TYPE: &str = "application/stix+json;version=2.1";
|
||||
|
||||
/// TAXII collection ID for the primary threat feed.
|
||||
pub const TAXII_COLLECTION_ID: &str = "hivemind-threat-feed-v1";
|
||||
|
||||
/// TAXII collection title.
|
||||
pub const TAXII_COLLECTION_TITLE: &str = "HiveMind Verified Threat Feed";
|
||||
|
||||
/// STIX spec version.
|
||||
pub const STIX_SPEC_VERSION: &str = "2.1";
|
||||
|
||||
/// Product vendor name for SIEM formats.
|
||||
pub const SIEM_VENDOR: &str = "Blackwall";
|
||||
|
||||
/// Product name for SIEM formats.
|
||||
pub const SIEM_PRODUCT: &str = "HiveMind";
|
||||
|
||||
/// Product version for SIEM formats.
|
||||
pub const SIEM_VERSION: &str = "1.0";
|
||||
|
||||
/// Splunk sourcetype for HiveMind events.
|
||||
pub const SPLUNK_SOURCETYPE: &str = "hivemind:threat_feed";
|
||||
|
||||
// --- Phase 3: Enterprise API Tier Types ---
|
||||
|
||||
/// API access tier determining rate limits and format availability.
|
||||
#[cfg(feature = "user")]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ApiTier {
|
||||
/// Free tier: JSON feed, limited page size.
|
||||
Free,
|
||||
/// Enterprise tier: all formats, full page size, STIX/TAXII.
|
||||
Enterprise,
|
||||
/// National security tier: full access + macro-analytics.
|
||||
NationalSecurity,
|
||||
}
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
impl ApiTier {
|
||||
/// Maximum page size allowed for this tier.
|
||||
pub fn max_page_size(self) -> usize {
|
||||
match self {
|
||||
ApiTier::Free => 50,
|
||||
ApiTier::Enterprise => API_MAX_PAGE_SIZE,
|
||||
ApiTier::NationalSecurity => API_MAX_PAGE_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this tier can access SIEM integration formats.
|
||||
pub fn can_access_siem(self) -> bool {
|
||||
matches!(self, ApiTier::Enterprise | ApiTier::NationalSecurity)
|
||||
}
|
||||
|
||||
/// Whether this tier can access STIX/TAXII endpoints.
|
||||
pub fn can_access_taxii(self) -> bool {
|
||||
matches!(self, ApiTier::Enterprise | ApiTier::NationalSecurity)
|
||||
}
|
||||
}
|
||||
562
common/src/lib.rs
Executable file
562
common/src/lib.rs
Executable file
|
|
@ -0,0 +1,562 @@
|
|||
#![cfg_attr(not(feature = "user"), no_std)]
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub mod base64;
|
||||
pub mod hivemind;
|
||||
|
||||
/// Action to take on a matched rule.
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RuleAction {
|
||||
/// Allow packet through
|
||||
Pass = 0,
|
||||
/// Drop packet silently
|
||||
Drop = 1,
|
||||
/// Redirect to tarpit honeypot
|
||||
RedirectTarpit = 2,
|
||||
}
|
||||
|
||||
/// Packet event emitted from eBPF via RingBuf when anomaly detected.
|
||||
/// 32 bytes, naturally aligned, zero-copy safe.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PacketEvent {
|
||||
/// Source IPv4 address (network byte order)
|
||||
pub src_ip: u32,
|
||||
/// Destination IPv4 address (network byte order)
|
||||
pub dst_ip: u32,
|
||||
/// Source port (network byte order)
|
||||
pub src_port: u16,
|
||||
/// Destination port (network byte order)
|
||||
pub dst_port: u16,
|
||||
/// IP protocol number (6=TCP, 17=UDP, 1=ICMP)
|
||||
pub protocol: u8,
|
||||
/// TCP flags bitmask (SYN=0x02, ACK=0x10, RST=0x04, FIN=0x01)
|
||||
pub flags: u8,
|
||||
/// Number of payload bytes analyzed for entropy
|
||||
pub payload_len: u16,
|
||||
/// Byte diversity score: unique_count × 31 (range 0–7936).
|
||||
/// NOT Shannon entropy — uses bitmap popcount heuristic in eBPF.
|
||||
pub entropy_score: u32,
|
||||
/// Lower 32 bits of bpf_ktime_get_ns()
|
||||
pub timestamp_ns: u32,
|
||||
/// Reserved padding for alignment
|
||||
pub _padding: u32,
|
||||
/// Total IP packet size in bytes
|
||||
pub packet_size: u32,
|
||||
}
|
||||
|
||||
/// Key for IP blocklist/allowlist HashMap.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RuleKey {
|
||||
pub ip: u32,
|
||||
}
|
||||
|
||||
/// Value for IP blocklist/allowlist HashMap.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RuleValue {
|
||||
/// Action: 0=Pass, 1=Drop, 2=RedirectTarpit
|
||||
pub action: u8,
|
||||
pub _pad1: u8,
|
||||
pub _pad2: u16,
|
||||
/// Expiry in seconds since boot (0 = permanent)
|
||||
pub expires_at: u32,
|
||||
}
|
||||
|
||||
/// Key for LpmTrie CIDR matching.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct CidrKey {
|
||||
/// Prefix length (0-32)
|
||||
pub prefix_len: u32,
|
||||
/// Network address (network byte order)
|
||||
pub ip: u32,
|
||||
}
|
||||
|
||||
/// Global statistics counters.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Counters {
|
||||
pub packets_total: u64,
|
||||
pub packets_passed: u64,
|
||||
pub packets_dropped: u64,
|
||||
pub anomalies_sent: u64,
|
||||
}
|
||||
|
||||
/// Maximum cipher suite IDs to capture from TLS ClientHello.
|
||||
pub const TLS_MAX_CIPHERS: usize = 20;
|
||||
|
||||
/// Maximum extension IDs to capture from TLS ClientHello.
|
||||
pub const TLS_MAX_EXTENSIONS: usize = 20;
|
||||
|
||||
/// Maximum SNI hostname bytes to capture.
|
||||
pub const TLS_MAX_SNI: usize = 32;
|
||||
|
||||
/// TLS ClientHello raw components emitted from eBPF for JA4 assembly.
|
||||
/// Contains the raw fields needed to compute JA4 fingerprint in userspace.
|
||||
/// 128 bytes total, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TlsComponentsEvent {
|
||||
/// Source IPv4 address (network byte order on LE host)
|
||||
pub src_ip: u32,
|
||||
/// Destination IPv4 address
|
||||
pub dst_ip: u32,
|
||||
/// Source port (host byte order)
|
||||
pub src_port: u16,
|
||||
/// Destination port (host byte order)
|
||||
pub dst_port: u16,
|
||||
/// TLS version from ClientHello (e.g., 0x0303 = TLS 1.2)
|
||||
pub tls_version: u16,
|
||||
/// Number of cipher suites in ClientHello
|
||||
pub cipher_count: u8,
|
||||
/// Number of extensions in ClientHello
|
||||
pub ext_count: u8,
|
||||
/// First N cipher suite IDs (network byte order)
|
||||
pub ciphers: [u16; TLS_MAX_CIPHERS],
|
||||
/// First N extension type IDs (network byte order)
|
||||
pub extensions: [u16; TLS_MAX_EXTENSIONS],
|
||||
/// SNI hostname (first 32 bytes, null-padded)
|
||||
pub sni: [u8; TLS_MAX_SNI],
|
||||
/// ALPN first protocol length (0 if no ALPN)
|
||||
pub alpn_first_len: u8,
|
||||
/// Whether SNI extension was present
|
||||
pub has_sni: u8,
|
||||
/// Lower 32 bits of bpf_ktime_get_ns()
|
||||
pub timestamp_ns: u32,
|
||||
/// Padding to 140 bytes
|
||||
pub _padding: [u8; 2],
|
||||
}
|
||||
|
||||
/// Egress event emitted from TC classifier for outbound traffic analysis.
|
||||
/// 32 bytes, naturally aligned, zero-copy safe.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct EgressEvent {
|
||||
/// Source IPv4 address (local server)
|
||||
pub src_ip: u32,
|
||||
/// Destination IPv4 address (remote)
|
||||
pub dst_ip: u32,
|
||||
/// Source port
|
||||
pub src_port: u16,
|
||||
/// Destination port
|
||||
pub dst_port: u16,
|
||||
/// IP protocol (6=TCP, 17=UDP)
|
||||
pub protocol: u8,
|
||||
/// TCP flags (if TCP)
|
||||
pub flags: u8,
|
||||
/// Payload length in bytes
|
||||
pub payload_len: u16,
|
||||
/// DNS query name length (0 if not DNS)
|
||||
pub dns_query_len: u16,
|
||||
/// Entropy score of outbound payload (same scale as ingress)
|
||||
pub entropy_score: u16,
|
||||
/// Lower 32 bits of bpf_ktime_get_ns()
|
||||
pub timestamp_ns: u32,
|
||||
/// Total packet size
|
||||
pub packet_size: u32,
|
||||
}
|
||||
|
||||
/// Detected protocol from DPI tail call analysis.
|
||||
#[repr(u8)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DpiProtocol {
|
||||
/// Unknown protocol
|
||||
Unknown = 0,
|
||||
/// HTTP (detected by method keyword)
|
||||
Http = 1,
|
||||
/// SSH (detected by "SSH-" banner)
|
||||
Ssh = 2,
|
||||
/// DNS (detected by port 53 + valid structure)
|
||||
Dns = 3,
|
||||
/// TLS (handled separately via TlsComponentsEvent)
|
||||
Tls = 4,
|
||||
}
|
||||
|
||||
impl DpiProtocol {
|
||||
/// Convert a raw u8 value to DpiProtocol.
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
1 => DpiProtocol::Http,
|
||||
2 => DpiProtocol::Ssh,
|
||||
3 => DpiProtocol::Dns,
|
||||
4 => DpiProtocol::Tls,
|
||||
_ => DpiProtocol::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DPI event emitted from eBPF tail call programs via RingBuf.
|
||||
/// 24 bytes, naturally aligned, zero-copy safe.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct DpiEvent {
|
||||
/// Source IPv4 address
|
||||
pub src_ip: u32,
|
||||
/// Destination IPv4 address
|
||||
pub dst_ip: u32,
|
||||
/// Source port
|
||||
pub src_port: u16,
|
||||
/// Destination port
|
||||
pub dst_port: u16,
|
||||
/// Detected protocol (DpiProtocol as u8)
|
||||
pub protocol: u8,
|
||||
/// Protocol-specific flags (e.g., suspicious path for HTTP, tunneling for DNS)
|
||||
pub flags: u8,
|
||||
/// Payload length
|
||||
pub payload_len: u16,
|
||||
/// Lower 32 bits of bpf_ktime_get_ns()
|
||||
pub timestamp_ns: u32,
|
||||
}
|
||||
|
||||
/// DPI flags for HTTP detection.
|
||||
pub const DPI_HTTP_FLAG_SUSPICIOUS_PATH: u8 = 0x01;
|
||||
/// DPI flags for DNS detection.
|
||||
pub const DPI_DNS_FLAG_LONG_QUERY: u8 = 0x01;
|
||||
pub const DPI_DNS_FLAG_TUNNELING_SUSPECT: u8 = 0x02;
|
||||
/// DPI flags for SSH detection.
|
||||
pub const DPI_SSH_FLAG_SUSPICIOUS_SW: u8 = 0x01;
|
||||
|
||||
/// RingBuf size for DPI events (64 KB, power of 2).
|
||||
pub const DPI_RINGBUF_SIZE_BYTES: u32 = 64 * 1024;
|
||||
|
||||
// --- eBPF Native DNAT Types ---
|
||||
|
||||
/// Tarpit DNAT configuration pushed from userspace into eBPF map.
|
||||
/// PerCpuArray[0] — single-element scratch for tarpit routing.
|
||||
/// 16 bytes, naturally aligned, zero-copy safe.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TarpitTarget {
|
||||
/// Tarpit listen port (host byte order).
|
||||
pub port: u16,
|
||||
/// Padding for alignment.
|
||||
pub _pad: u16,
|
||||
/// Local interface IP (network byte order as stored by eBPF).
|
||||
/// Used for response matching in TC reverse-NAT.
|
||||
pub local_ip: u32,
|
||||
/// Whether DNAT is enabled (1=yes, 0=no).
|
||||
pub enabled: u32,
|
||||
/// Reserved for future use.
|
||||
pub _reserved: u32,
|
||||
}
|
||||
|
||||
/// NAT tracking key for tarpit DNAT connections.
|
||||
/// Identifies a unique inbound flow from an attacker.
|
||||
/// 8 bytes, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct NatKey {
|
||||
/// Attacker source IP (network byte order).
|
||||
pub src_ip: u32,
|
||||
/// Attacker source port (host byte order, stored as u32 for BPF alignment).
|
||||
pub src_port: u32,
|
||||
}
|
||||
|
||||
/// NAT tracking value storing the original destination before DNAT rewrite.
|
||||
/// 8 bytes, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct NatValue {
|
||||
/// Original destination port before DNAT (host byte order).
|
||||
pub orig_dst_port: u16,
|
||||
pub _pad: u16,
|
||||
/// Timestamp (lower 32 bits of bpf_ktime_get_ns / 1e9 for LRU approx).
|
||||
pub timestamp: u32,
|
||||
}
|
||||
|
||||
/// Connection tracking key — 5-tuple identifying a unique flow.
|
||||
/// 16 bytes, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ConnTrackKey {
|
||||
/// Source IP (network byte order).
|
||||
pub src_ip: u32,
|
||||
/// Destination IP (network byte order).
|
||||
pub dst_ip: u32,
|
||||
/// Source port (host byte order).
|
||||
pub src_port: u16,
|
||||
/// Destination port (host byte order).
|
||||
pub dst_port: u16,
|
||||
/// IP protocol (6=TCP, 17=UDP).
|
||||
pub protocol: u8,
|
||||
pub _pad: [u8; 3],
|
||||
}
|
||||
|
||||
/// Connection tracking value — per-flow state and counters.
|
||||
/// 16 bytes, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct ConnTrackValue {
|
||||
/// TCP state: 0=NEW, 1=SYN_SENT, 2=SYN_RECV, 3=ESTABLISHED,
|
||||
/// 4=FIN_WAIT, 5=CLOSE_WAIT, 6=CLOSED
|
||||
pub state: u8,
|
||||
/// Cumulative TCP flags seen in this flow.
|
||||
pub flags_seen: u8,
|
||||
pub _pad: u16,
|
||||
/// Packet count in this flow.
|
||||
pub packet_count: u32,
|
||||
/// Total bytes transferred.
|
||||
pub byte_count: u32,
|
||||
/// Timestamp of last packet (lower 32 of bpf_ktime_get_ns).
|
||||
pub last_seen: u32,
|
||||
}
|
||||
|
||||
/// Per-IP rate limit token bucket state for XDP rate limiting.
|
||||
/// 8 bytes, naturally aligned.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct RateLimitValue {
|
||||
/// Available tokens (decremented per packet, refilled per second).
|
||||
pub tokens: u32,
|
||||
/// Last refill timestamp (seconds since boot, from bpf_ktime_get_boot_ns).
|
||||
pub last_refill: u32,
|
||||
}
|
||||
|
||||
/// TCP connection states for ConnTrackValue.state
|
||||
pub const CT_STATE_NEW: u8 = 0;
|
||||
pub const CT_STATE_SYN_SENT: u8 = 1;
|
||||
pub const CT_STATE_SYN_RECV: u8 = 2;
|
||||
pub const CT_STATE_ESTABLISHED: u8 = 3;
|
||||
pub const CT_STATE_FIN_WAIT: u8 = 4;
|
||||
pub const CT_STATE_CLOSE_WAIT: u8 = 5;
|
||||
pub const CT_STATE_CLOSED: u8 = 6;
|
||||
|
||||
/// PROG_ARRAY indices for DPI tail call programs.
|
||||
pub const DPI_PROG_HTTP: u32 = 0;
|
||||
pub const DPI_PROG_DNS: u32 = 1;
|
||||
pub const DPI_PROG_SSH: u32 = 2;
|
||||
|
||||
// --- Pod safety (aya requirement for BPF map types, userspace only) ---
|
||||
// SAFETY: All types are #[repr(C)], contain only fixed-width integers,
|
||||
// have no padding holes (explicit padding fields), and no pointers.
|
||||
// eBPF side has no Pod trait — types just need #[repr(C)] + Copy.
|
||||
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for PacketEvent {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for RuleKey {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for RuleValue {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for CidrKey {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for Counters {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for TlsComponentsEvent {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for EgressEvent {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for DpiEvent {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for TarpitTarget {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for NatKey {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for NatValue {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for ConnTrackKey {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for ConnTrackValue {}
|
||||
#[cfg(feature = "aya")]
|
||||
unsafe impl aya::Pod for RateLimitValue {}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
/// TLS record content type for Handshake.
|
||||
pub const TLS_CONTENT_TYPE_HANDSHAKE: u8 = 22;
|
||||
|
||||
/// TLS handshake type for ClientHello.
|
||||
pub const TLS_HANDSHAKE_CLIENT_HELLO: u8 = 1;
|
||||
|
||||
/// RingBuf size for TLS events (64 KB, power of 2).
|
||||
pub const TLS_RINGBUF_SIZE_BYTES: u32 = 64 * 1024;
|
||||
|
||||
/// RingBuf size for egress events (64 KB, power of 2).
|
||||
pub const EGRESS_RINGBUF_SIZE_BYTES: u32 = 64 * 1024;
|
||||
|
||||
/// DNS query name length threshold for tunneling detection.
|
||||
pub const DNS_TUNNEL_QUERY_LEN_THRESHOLD: u16 = 200;
|
||||
|
||||
/// Byte diversity threshold. Payloads above this → anomaly event.
|
||||
/// Scale: unique_count × 31 (encrypted traffic: ~7000–7936, ASCII: ~1200–1800).
|
||||
pub const ENTROPY_ANOMALY_THRESHOLD: u32 = 6500;
|
||||
|
||||
/// Maximum payload bytes to analyze for entropy (must fit in eBPF bounded loop).
|
||||
pub const MAX_PAYLOAD_ANALYSIS_BYTES: usize = 128;
|
||||
|
||||
/// RingBuf size in bytes (must be power of 2). 256 KB.
|
||||
pub const RINGBUF_SIZE_BYTES: u32 = 256 * 1024;
|
||||
|
||||
/// Maximum entries in IP blocklist HashMap.
|
||||
pub const BLOCKLIST_MAX_ENTRIES: u32 = 65536;
|
||||
|
||||
/// Maximum entries in CIDR LpmTrie.
|
||||
pub const CIDR_MAX_ENTRIES: u32 = 4096;
|
||||
|
||||
/// Maximum entries in NAT tracking table (per-connection tarpit DNAT).
|
||||
pub const NAT_TABLE_MAX_ENTRIES: u32 = 65536;
|
||||
|
||||
/// Maximum entries in connection tracking LRU map.
|
||||
pub const CONN_TRACK_MAX_ENTRIES: u32 = 131072;
|
||||
|
||||
/// Maximum entries in per-IP rate limit LRU map.
|
||||
pub const RATE_LIMIT_MAX_ENTRIES: u32 = 131072;
|
||||
|
||||
/// Rate limit: max packets per second per IP before XDP_DROP.
|
||||
pub const RATE_LIMIT_PPS: u32 = 100;
|
||||
|
||||
/// Rate limit: burst capacity (tokens). Allows short bursts above PPS.
|
||||
pub const RATE_LIMIT_BURST: u32 = 200;
|
||||
|
||||
/// Tarpit default port.
|
||||
pub const TARPIT_PORT: u16 = 2222;
|
||||
|
||||
/// Tarpit base delay milliseconds.
|
||||
pub const TARPIT_BASE_DELAY_MS: u64 = 50;
|
||||
|
||||
/// Tarpit max delay milliseconds.
|
||||
pub const TARPIT_MAX_DELAY_MS: u64 = 500;
|
||||
|
||||
/// Tarpit jitter range milliseconds.
|
||||
pub const TARPIT_JITTER_MS: u64 = 100;
|
||||
|
||||
/// Tarpit min chunk size (bytes).
|
||||
pub const TARPIT_MIN_CHUNK: usize = 1;
|
||||
|
||||
/// Tarpit max chunk size (bytes).
|
||||
pub const TARPIT_MAX_CHUNK: usize = 15;
|
||||
|
||||
// --- Helper functions (std-only) ---
|
||||
|
||||
#[cfg(feature = "user")]
|
||||
pub mod util {
|
||||
use core::net::Ipv4Addr;
|
||||
|
||||
/// Convert u32 (network byte order stored on LE host) to displayable IPv4.
|
||||
///
|
||||
/// eBPF reads IP header fields as raw u32 on bpfel (little-endian).
|
||||
/// The wire bytes [A,B,C,D] become a LE u32 value. `u32::from_be()`
|
||||
/// converts that to a host-order value that `Ipv4Addr::from(u32)` expects.
|
||||
pub fn ip_from_u32(ip: u32) -> Ipv4Addr {
|
||||
Ipv4Addr::from(u32::from_be(ip))
|
||||
}
|
||||
|
||||
/// Convert IPv4 to u32 matching eBPF's bpfel representation.
|
||||
///
|
||||
/// `Ipv4Addr → u32` yields a host-order value (MSB = first octet).
|
||||
/// `.to_be()` converts to the same representation eBPF stores.
|
||||
pub fn ip_to_u32(ip: Ipv4Addr) -> u32 {
|
||||
u32::from(ip).to_be()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core::mem;
|
||||
|
||||
#[test]
|
||||
fn packet_event_size_and_alignment() {
|
||||
assert_eq!(mem::size_of::<PacketEvent>(), 32);
|
||||
assert_eq!(mem::align_of::<PacketEvent>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_key_size() {
|
||||
assert_eq!(mem::size_of::<RuleKey>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_value_size() {
|
||||
assert_eq!(mem::size_of::<RuleValue>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cidr_key_size() {
|
||||
assert_eq!(mem::size_of::<CidrKey>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counters_size() {
|
||||
assert_eq!(mem::size_of::<Counters>(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_components_event_size() {
|
||||
assert_eq!(mem::size_of::<TlsComponentsEvent>(), 140);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_components_event_alignment() {
|
||||
assert_eq!(mem::align_of::<TlsComponentsEvent>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn egress_event_size() {
|
||||
assert_eq!(mem::size_of::<EgressEvent>(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn egress_event_alignment() {
|
||||
assert_eq!(mem::align_of::<EgressEvent>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_threshold_in_range() {
|
||||
assert!(ENTROPY_ANOMALY_THRESHOLD <= 8000);
|
||||
assert!(ENTROPY_ANOMALY_THRESHOLD > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ringbuf_size_is_power_of_two() {
|
||||
assert!(RINGBUF_SIZE_BYTES.is_power_of_two());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ip_conversion_roundtrip() {
|
||||
use util::*;
|
||||
let ip = core::net::Ipv4Addr::new(192, 168, 1, 1);
|
||||
let raw = ip_to_u32(ip);
|
||||
assert_eq!(ip_from_u32(raw), ip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dpi_event_size() {
|
||||
assert_eq!(mem::size_of::<DpiEvent>(), 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dpi_event_alignment() {
|
||||
assert_eq!(mem::align_of::<DpiEvent>(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tarpit_target_size() {
|
||||
assert_eq!(mem::size_of::<TarpitTarget>(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_key_size() {
|
||||
assert_eq!(mem::size_of::<NatKey>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_value_size() {
|
||||
assert_eq!(mem::size_of::<NatValue>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conn_track_key_size() {
|
||||
assert_eq!(mem::size_of::<ConnTrackKey>(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conn_track_value_size() {
|
||||
assert_eq!(mem::size_of::<ConnTrackValue>(), 16);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue