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,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() }
}