v2.0.0: adaptive eBPF firewall with AI honeypot and P2P threat mesh

This commit is contained in:
Vladyslav Soliannikov 2026-04-07 22:28:11 +00:00
commit 37c6bbf5a1
133 changed files with 28073 additions and 0 deletions

17
common/Cargo.toml Executable file
View 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
View 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
View 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
View 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 07936).
/// 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: ~70007936, ASCII: ~12001800).
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);
}
}