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:
257
xdp-firewall-ebpf/src/main.rs
Normal file
257
xdp-firewall-ebpf/src/main.rs
Normal 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() }
|
||||
}
|
||||
Reference in New Issue
Block a user