Implement XDP firewall with real-time TUI monitoring

Features:
- High-performance packet filtering via eBPF/XDP
- Instant blocklist with dynamic CLI management
- Exact-match rules with Drop/Pass/Log actions
- CIDR-based IP range dropping via LPM trie
- Token-bucket rate limiting (IP-based and flow-based)
- Auto temp bans for rate limit violators
- Real-time event logging via BPF ring buffer
- Interactive TUI monitor with live stats

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 09:10:52 +07:00
commit 6101de6887
24 changed files with 3235 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
[target.bpfel-unknown-none]
linker = "bpf-linker"
rustflags = ["-C", "link-arg=--btf"]

346
xdp-firewall-ebpf/Cargo.lock generated Normal file
View File

@@ -0,0 +1,346 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aya-build"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59bc42f3c5ddacc34eca28a420b47e3cbb3f0f484137cb2bf1ad2153d0eae52a"
dependencies = [
"anyhow",
"cargo_metadata",
]
[[package]]
name = "aya-ebpf"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8dbaf5409a1a0982e5c9bdc0f499a55fe5ead39fe9c846012053faf0d404f73"
dependencies = [
"aya-ebpf-bindings",
"aya-ebpf-cty",
"aya-ebpf-macros",
"rustversion",
]
[[package]]
name = "aya-ebpf-bindings"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71ee8e6a617f040d8da7565ec4010aea75e33cda4662f64c019c66ee97d17889"
dependencies = [
"aya-build",
"aya-ebpf-cty",
]
[[package]]
name = "aya-ebpf-cty"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f33396742e7fd0f519c1e0de5141d84e1a8df69146a557c08cc222b0ceace4"
dependencies = [
"aya-build",
]
[[package]]
name = "aya-ebpf-macros"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96fd02363736177e7e91d6c95d7effbca07be87502c7b5b32fc194aed8b177a0"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]]
name = "aya-log-common"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "befef9fe882e63164a2ba0161874e954648a72b0e1c4b361f532d590638c4eec"
dependencies = [
"num_enum",
]
[[package]]
name = "aya-log-ebpf"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a10bbadd0829895a91eb1cd2bb02d7af145704087f03812bed60cb9fe65dbb3"
dependencies = [
"aya-ebpf",
"aya-log-common",
"aya-log-ebpf-macros",
]
[[package]]
name = "aya-log-ebpf-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d8251a75f56077db51892041aa6b77c70ef2723845d7a210979700b2f01bc4"
dependencies = [
"aya-log-common",
"aya-log-parser",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "aya-log-parser"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b102eb5c88c9aa0b49102d3fbcee08ecb0dfa81014f39b373311de7a7032cb"
dependencies = [
"aya-log-common",
]
[[package]]
name = "camino"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
dependencies = [
"serde_core",
]
[[package]]
name = "cargo-platform"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "cargo_metadata"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "network-types"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f06f1863cb5565864300c6bfb012312969908878d2ca5881eaf0bbdb8b519c23"
dependencies = [
"memoffset",
]
[[package]]
name = "num_enum"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "xdp-firewall-common"
version = "0.1.0"
[[package]]
name = "xdp-firewall-ebpf"
version = "0.1.0"
dependencies = [
"aya-ebpf",
"aya-log-ebpf",
"network-types",
"xdp-firewall-common",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -0,0 +1,16 @@
[package]
name = "xdp-firewall-ebpf"
version = "0.1.0"
edition = "2021"
[dependencies]
aya-ebpf = "0.1.1"
aya-log-ebpf = "0.1.0"
network-types = "0.1.0"
xdp-firewall-common = { path = "../xdp-firewall-common" }
[[bin]]
name = "xdp-firewall"
path = "src/main.rs"
[workspace]

View File

@@ -0,0 +1,257 @@
#![no_std]
#![no_main]
use aya_ebpf::{
bindings::xdp_action::{XDP_ABORTED, XDP_DROP, XDP_PASS},
macros::{map, xdp},
maps::{Array, HashMap, LpmTrie, RingBuf},
maps::lpm_trie::Key as LpmKey,
programs::XdpContext,
helpers::gen::bpf_ktime_get_ns,
};
use network_types::{
eth::{EthHdr, EtherType},
ip::{IpProto, Ipv4Hdr},
tcp::TcpHdr,
udp::UdpHdr,
};
use xdp_firewall_common::*;
#[map(name = "BLOCKLIST")]
static BLOCKLIST: HashMap<u32, u64> = HashMap::pinned(1024, 0);
#[map(name = "IP_RANGES")]
static IP_RANGES: LpmTrie<u32, u8> = LpmTrie::pinned(256, 0);
#[map(name = "RULES")]
static RULES: HashMap<RuleKey, RuleValue> = HashMap::pinned(1024, 0);
#[map(name = "RATE_LIMIT_STATE")]
static RATE_LIMIT_STATE: HashMap<RateLimitKey, RateLimitState> = HashMap::pinned(8192, 0);
#[map(name = "RATE_LIMIT_CONFIG")]
static RATE_LIMIT_CONFIG: HashMap<u8, RateLimitConfig> = HashMap::pinned(1, 0);
#[map(name = "STATS")]
static STATS: HashMap<u32, Stats> = HashMap::pinned(2048, 0);
#[map(name = "GLOBAL_STATS")]
static GLOBAL_STATS: Array<Stats> = Array::pinned(1, 0);
#[map(name = "EVENTS")]
static EVENTS: RingBuf = RingBuf::pinned(256 * 1024, 0);
const FIXED_POINT: u64 = 1_000_000;
#[inline(always)]
fn ptr_at<T>(ctx: &XdpContext, offset: usize) -> Option<*const T> {
let start = ctx.data();
let end = ctx.data_end();
if start + offset + core::mem::size_of::<T>() > end {
return None;
}
Some((start + offset) as *const T)
}
#[inline(always)]
unsafe fn update_stats(rule_id: u32, bytes: u64) {
if let Some(s) = STATS.get_ptr_mut(&rule_id) {
let s = &mut *s;
s.packets += 1;
s.bytes += bytes;
}
if let Some(g) = GLOBAL_STATS.get_ptr_mut(0) {
let g = &mut *g;
g.packets += 1;
g.bytes += bytes;
}
}
#[inline(always)]
unsafe fn emit_event(_ctx: &XdpContext, action: u8, src_ip: u32, dst_ip: u32, proto: u8, src_port: u16, dst_port: u16, rule_id: u32) {
if let Some(mut entry) = EVENTS.reserve::<FirewallEvent>(0) {
let ev = FirewallEvent {
ts_ns: bpf_ktime_get_ns(),
src_ip,
dst_ip,
src_port,
dst_port,
proto,
action,
rule_id,
};
entry.write(ev);
entry.submit(0);
}
}
#[inline(always)]
unsafe fn check_ratelimit(key: &RateLimitKey, bytes: u64, src_ip: u32) -> bool {
let config = match RATE_LIMIT_CONFIG.get(&0u8) {
Some(c) if c.enabled != 0 => c,
_ => return false,
};
let now = bpf_ktime_get_ns();
let state = match RATE_LIMIT_STATE.get_ptr_mut(key) {
Some(s) => {
let s = &mut *s;
let elapsed = now - s.last_update_ns;
let added = elapsed * config.rate_per_sec / 1_000_000_000;
let max_tokens = config.burst;
s.tokens = if s.tokens + added > max_tokens { max_tokens } else { s.tokens + added };
s.last_update_ns = now;
s
}
None => {
let new = RateLimitState {
tokens: config.burst,
last_update_ns: now,
violations: 0,
_pad: 0,
};
let _ = RATE_LIMIT_STATE.insert(key, &new, 0);
return false;
}
};
if state.tokens >= FIXED_POINT {
state.tokens -= FIXED_POINT;
return false;
}
state.violations += 1;
update_stats(0xFFFF_FFFF, bytes);
if state.violations >= config.ban_threshold {
let ban_ns = (config.ban_duration_sec as u64) * 1_000_000_000;
let expiry = now + ban_ns;
let _ = BLOCKLIST.insert(&src_ip, &expiry, 0);
}
true
}
#[xdp]
pub fn xdp_firewall(ctx: XdpContext) -> u32 {
match try_xdp_firewall(ctx) {
Ok(ret) => ret,
Err(_) => XDP_ABORTED,
}
}
fn try_xdp_firewall(ctx: XdpContext) -> Result<u32, ()> {
let eth_hdr: *const EthHdr = ptr_at(&ctx, 0).ok_or(())?;
let eth = unsafe { &*eth_hdr };
if eth.ether_type != EtherType::Ipv4 as u16 {
return Ok(XDP_PASS);
}
let ipv4_hdr: *const Ipv4Hdr = ptr_at(&ctx, core::mem::size_of::<EthHdr>()).ok_or(())?;
let ipv4 = unsafe { &*ipv4_hdr };
let src_ip = u32::from_be_bytes(ipv4.src_addr);
let dst_ip = u32::from_be_bytes(ipv4.dst_addr);
let proto = ipv4.proto as u8;
let ihl = ipv4.ihl() as usize;
let l4_offset = core::mem::size_of::<EthHdr>() + ihl;
let pkt_len = (u16::from_be_bytes(ipv4.tot_len) as usize).saturating_sub(ihl);
let bytes = (core::mem::size_of::<EthHdr>() + ihl + pkt_len) as u64;
let (src_port, dst_port) = match ipv4.proto {
IpProto::Tcp => {
let tcp: *const TcpHdr = ptr_at(&ctx, l4_offset).ok_or(())?;
let t = unsafe { &*tcp };
(u16::from_be_bytes(t.source), u16::from_be_bytes(t.dest))
}
IpProto::Udp => {
let udp: *const UdpHdr = ptr_at(&ctx, l4_offset).ok_or(())?;
let u = unsafe { &*udp };
(u16::from_be_bytes(u.src), u16::from_be_bytes(u.dst))
}
_ => (0, 0),
};
let now = unsafe { bpf_ktime_get_ns() };
unsafe {
if let Some(expiry) = BLOCKLIST.get(&src_ip) {
if now < *expiry {
update_stats(0xFFFF_FFFE, bytes);
emit_event(&ctx, ACTION_BLOCKLIST_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0);
return Ok(XDP_DROP);
}
}
let lpm_key = LpmKey::new(32, src_ip.to_be());
if let Some(action) = IP_RANGES.get(&lpm_key) {
if *action == ACTION_DROP {
update_stats(0xFFFF_FFFD, bytes);
emit_event(&ctx, ACTION_RANGE_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0);
return Ok(XDP_DROP);
}
}
let rl_ip_key = RateLimitKey {
key_type: RL_TYPE_IP,
_pad: [0; 3],
src_ip,
dst_ip: 0,
proto: 0,
_pad2: [0; 3],
src_port: 0,
dst_port: 0,
};
if check_ratelimit(&rl_ip_key, bytes, src_ip) {
emit_event(&ctx, ACTION_RATELIMIT_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0);
return Ok(XDP_DROP);
}
let rl_flow_key = RateLimitKey {
key_type: RL_TYPE_FLOW,
_pad: [0; 3],
src_ip,
dst_ip,
proto,
_pad2: [0; 3],
src_port,
dst_port,
};
if check_ratelimit(&rl_flow_key, bytes, src_ip) {
emit_event(&ctx, ACTION_RATELIMIT_DROP, src_ip, dst_ip, proto, src_port, dst_port, 0);
return Ok(XDP_DROP);
}
let rule_key = RuleKey {
src_ip,
dst_ip,
src_port,
dst_port,
proto,
_pad: [0; 3],
};
if let Some(rule) = RULES.get(&rule_key) {
update_stats(rule.rule_id, bytes);
match rule.action {
ACTION_DROP => {
emit_event(&ctx, ACTION_DROP, src_ip, dst_ip, proto, src_port, dst_port, rule.rule_id);
Ok(XDP_DROP)
}
ACTION_LOG => {
emit_event(&ctx, ACTION_LOG, src_ip, dst_ip, proto, src_port, dst_port, rule.rule_id);
Ok(XDP_PASS)
}
_ => Ok(XDP_PASS),
}
} else {
Ok(XDP_PASS)
}
}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
unsafe { core::hint::unreachable_unchecked() }
}