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:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target-dir = "target"
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/target
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
1339
Cargo.lock
generated
Normal file
1339
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["xdp-firewall", "xdp-firewall-common", "xtask"]
|
||||
exclude = ["xdp-firewall-ebpf"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
18
scripts/setup-veth.sh
Executable file
18
scripts/setup-veth.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
ip link del veth0 2>/dev/null || true
|
||||
ip netns del testns 2>/dev/null || true
|
||||
|
||||
ip link add veth0 type veth peer name veth1
|
||||
ip addr add 10.200.1.1/24 dev veth0
|
||||
ip link set veth0 up
|
||||
|
||||
ip netns add testns
|
||||
ip link set veth1 netns testns
|
||||
ip netns exec testns ip addr add 10.200.1.2/24 dev veth1
|
||||
ip netns exec testns ip link set veth1 up
|
||||
ip netns exec testns ip link set lo up
|
||||
|
||||
echo "veth0 (host) = 10.200.1.1"
|
||||
echo "veth1 (testns) = 10.200.1.2"
|
||||
10
xdp-firewall-common/Cargo.toml
Normal file
10
xdp-firewall-common/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "xdp-firewall-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aya = { version = "0.13.1", optional = true }
|
||||
|
||||
[features]
|
||||
userspace = ["aya"]
|
||||
103
xdp-firewall-common/src/lib.rs
Normal file
103
xdp-firewall-common/src/lib.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
#![no_std]
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RuleKey {
|
||||
pub src_ip: u32,
|
||||
pub dst_ip: u32,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub proto: u8,
|
||||
pub _pad: [u8; 3],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RuleValue {
|
||||
pub action: u8,
|
||||
pub rule_id: u32,
|
||||
pub _pad: [u8; 3],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RateLimitKey {
|
||||
pub key_type: u8,
|
||||
pub _pad: [u8; 3],
|
||||
pub src_ip: u32,
|
||||
pub dst_ip: u32,
|
||||
pub proto: u8,
|
||||
pub _pad2: [u8; 3],
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RateLimitState {
|
||||
pub tokens: u64,
|
||||
pub last_update_ns: u64,
|
||||
pub violations: u32,
|
||||
pub _pad: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct RateLimitConfig {
|
||||
pub rate_per_sec: u64,
|
||||
pub burst: u64,
|
||||
pub ban_threshold: u32,
|
||||
pub ban_duration_sec: u32,
|
||||
pub enabled: u8,
|
||||
pub _pad: [u8; 3],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Stats {
|
||||
pub packets: u64,
|
||||
pub bytes: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct FirewallEvent {
|
||||
pub ts_ns: u64,
|
||||
pub src_ip: u32,
|
||||
pub dst_ip: u32,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
pub proto: u8,
|
||||
pub action: u8,
|
||||
pub rule_id: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Ipv4LpmKey {
|
||||
pub prefix_len: u32,
|
||||
pub addr: u32,
|
||||
}
|
||||
|
||||
pub const ACTION_DROP: u8 = 0;
|
||||
pub const ACTION_PASS: u8 = 1;
|
||||
pub const ACTION_LOG: u8 = 2;
|
||||
pub const ACTION_RATELIMIT_DROP: u8 = 3;
|
||||
pub const ACTION_BLOCKLIST_DROP: u8 = 4;
|
||||
pub const ACTION_RANGE_DROP: u8 = 5;
|
||||
|
||||
pub const RL_TYPE_IP: u8 = 0;
|
||||
pub const RL_TYPE_FLOW: u8 = 1;
|
||||
|
||||
#[cfg(feature = "userspace")]
|
||||
mod pod {
|
||||
use super::*;
|
||||
unsafe impl aya::Pod for RuleKey {}
|
||||
unsafe impl aya::Pod for RuleValue {}
|
||||
unsafe impl aya::Pod for RateLimitKey {}
|
||||
unsafe impl aya::Pod for RateLimitState {}
|
||||
unsafe impl aya::Pod for RateLimitConfig {}
|
||||
unsafe impl aya::Pod for Stats {}
|
||||
unsafe impl aya::Pod for FirewallEvent {}
|
||||
unsafe impl aya::Pod for Ipv4LpmKey {}
|
||||
}
|
||||
3
xdp-firewall-ebpf/.cargo/config.toml
Normal file
3
xdp-firewall-ebpf/.cargo/config.toml
Normal 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
346
xdp-firewall-ebpf/Cargo.lock
generated
Normal 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"
|
||||
16
xdp-firewall-ebpf/Cargo.toml
Normal file
16
xdp-firewall-ebpf/Cargo.toml
Normal 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]
|
||||
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() }
|
||||
}
|
||||
21
xdp-firewall/Cargo.toml
Normal file
21
xdp-firewall/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "xdp-firewall"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
aya = { version = "0.13.1", features = ["async_tokio"] }
|
||||
aya-log = "0.2.1"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
tokio = { version = "1.44", features = ["macros", "rt", "rt-multi-thread", "time", "sync", "signal"] }
|
||||
ratatui = "0.29"
|
||||
crossterm = { version = "0.29", features = ["event-stream"] }
|
||||
anyhow = "1.0"
|
||||
thiserror = "2.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
ipnetwork = "0.21"
|
||||
xdp-firewall-common = { path = "../xdp-firewall-common", features = ["userspace"] }
|
||||
|
||||
[build-dependencies]
|
||||
duct = "0.13"
|
||||
22
xdp-firewall/build.rs
Normal file
22
xdp-firewall/build.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
|
||||
let root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let ebpf_target = root.join("target/bpfel-unknown-none/release/xdp-firewall");
|
||||
|
||||
if !ebpf_target.exists() {
|
||||
let status = std::process::Command::new("cargo")
|
||||
.args(["run", "--package", "xtask", "--", "build-ebpf"])
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.expect("failed to build eBPF program");
|
||||
assert!(status.success(), "eBPF build failed");
|
||||
}
|
||||
|
||||
std::fs::copy(&ebpf_target, out_dir.join("xdp-firewall.o")).unwrap();
|
||||
println!("cargo:rerun-if-changed={}", ebpf_target.display());
|
||||
}
|
||||
118
xdp-firewall/src/cli.rs
Normal file
118
xdp-firewall/src/cli.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "xdp-firewall")]
|
||||
#[command(about = "High-performance XDP firewall with real-time monitoring")]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Commands {
|
||||
Load {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
config: Option<String>,
|
||||
},
|
||||
Unload {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
},
|
||||
Monitor {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
},
|
||||
Block {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
ip: String,
|
||||
#[arg(short, long, default_value = "0")]
|
||||
duration: u64,
|
||||
},
|
||||
Unblock {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
ip: String,
|
||||
},
|
||||
AddRule {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(long, default_value = "0.0.0.0")]
|
||||
src_ip: String,
|
||||
#[arg(long, default_value = "0.0.0.0")]
|
||||
dst_ip: String,
|
||||
#[arg(long, default_value = "0")]
|
||||
proto: u8,
|
||||
#[arg(long, default_value = "0")]
|
||||
src_port: u16,
|
||||
#[arg(long, default_value = "0")]
|
||||
dst_port: u16,
|
||||
#[arg(short, long, value_enum)]
|
||||
action: ActionArg,
|
||||
},
|
||||
DelRule {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
id: u32,
|
||||
},
|
||||
ListRules {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
},
|
||||
AddRange {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
cidr: String,
|
||||
#[arg(short, long, value_enum, default_value = "drop")]
|
||||
action: ActionArg,
|
||||
},
|
||||
DelRange {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(short, long)]
|
||||
cidr: String,
|
||||
},
|
||||
SetRatelimit {
|
||||
#[arg(short, long)]
|
||||
iface: String,
|
||||
#[arg(long)]
|
||||
rate: u64,
|
||||
#[arg(long)]
|
||||
burst: u64,
|
||||
#[arg(long)]
|
||||
ban_threshold: u32,
|
||||
#[arg(long)]
|
||||
ban_duration: u32,
|
||||
#[arg(long, value_enum)]
|
||||
mode: RatelimitModeArg,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub enum ActionArg {
|
||||
Drop,
|
||||
Pass,
|
||||
Log,
|
||||
}
|
||||
|
||||
impl ActionArg {
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
match self {
|
||||
ActionArg::Drop => xdp_firewall_common::ACTION_DROP,
|
||||
ActionArg::Pass => xdp_firewall_common::ACTION_PASS,
|
||||
ActionArg::Log => xdp_firewall_common::ACTION_LOG,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub enum RatelimitModeArg {
|
||||
Ip,
|
||||
Flow,
|
||||
}
|
||||
108
xdp-firewall/src/firewall.rs
Normal file
108
xdp-firewall/src/firewall.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use std::process::Command;
|
||||
|
||||
use aya::{
|
||||
maps::{Array, HashMap, lpm_trie::LpmTrie, MapData, RingBuf},
|
||||
programs::{Xdp, xdp::XdpFlags},
|
||||
EbpfLoader,
|
||||
};
|
||||
use aya_log::EbpfLogger;
|
||||
|
||||
use xdp_firewall_common::*;
|
||||
|
||||
pub struct FirewallHandle {
|
||||
pub bpf: aya::Ebpf,
|
||||
}
|
||||
|
||||
const BPF_ELF: &[u8] = include_bytes!("../../target/bpfel-unknown-none/release/xdp-firewall");
|
||||
|
||||
const PIN_PATH: &str = "/sys/fs/bpf/xdp-firewall";
|
||||
|
||||
impl FirewallHandle {
|
||||
pub fn load(iface: &str) -> anyhow::Result<Self> {
|
||||
eprintln!("Step 1: create_dir_all {}", PIN_PATH);
|
||||
std::fs::create_dir_all(PIN_PATH)?;
|
||||
eprintln!("Step 2: EbpfLoader::load");
|
||||
let elf_bytes = BPF_ELF.to_vec();
|
||||
let mut bpf = EbpfLoader::new()
|
||||
.map_pin_path(PIN_PATH)
|
||||
.load(&elf_bytes)
|
||||
.map_err(|e| {
|
||||
eprintln!("EbpfLoader::load failed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
let _ = EbpfLogger::init(&mut bpf);
|
||||
|
||||
let prog: &mut Xdp = bpf.program_mut("xdp_firewall").unwrap().try_into()?;
|
||||
prog.load().map_err(|e| {
|
||||
eprintln!("prog.load() failed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
prog.attach(iface, XdpFlags::default()).map_err(|e| {
|
||||
eprintln!("prog.attach() failed: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let mut global: Array<_, Stats> = Array::try_from(bpf.map_mut("GLOBAL_STATS").unwrap())?;
|
||||
global.set(0, Stats { packets: 0, bytes: 0 }, 0)?;
|
||||
|
||||
Ok(Self { bpf })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unload(iface: &str) -> anyhow::Result<()> {
|
||||
let status = Command::new("ip")
|
||||
.args(["link", "set", "dev", iface, "xdp", "off"])
|
||||
.status()?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("failed to detach XDP program from {}", iface);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_blocklist() -> anyhow::Result<HashMap<MapData, u32, u64>> {
|
||||
let data = MapData::from_pin(format!("{}/BLOCKLIST", PIN_PATH))?;
|
||||
let map = aya::maps::Map::HashMap(data);
|
||||
Ok(HashMap::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_ip_ranges() -> anyhow::Result<LpmTrie<MapData, u32, u8>> {
|
||||
let data = MapData::from_pin(format!("{}/IP_RANGES", PIN_PATH))?;
|
||||
let map = aya::maps::Map::LpmTrie(data);
|
||||
Ok(LpmTrie::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_rules() -> anyhow::Result<HashMap<MapData, RuleKey, RuleValue>> {
|
||||
let data = MapData::from_pin(format!("{}/RULES", PIN_PATH))?;
|
||||
let map = aya::maps::Map::HashMap(data);
|
||||
Ok(HashMap::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_rate_limit_state() -> anyhow::Result<HashMap<MapData, RateLimitKey, RateLimitState>> {
|
||||
let data = MapData::from_pin(format!("{}/RATE_LIMIT_STATE", PIN_PATH))?;
|
||||
let map = aya::maps::Map::HashMap(data);
|
||||
Ok(HashMap::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_rate_limit_config() -> anyhow::Result<HashMap<MapData, u8, RateLimitConfig>> {
|
||||
let data = MapData::from_pin(format!("{}/RATE_LIMIT_CONFIG", PIN_PATH))?;
|
||||
let map = aya::maps::Map::HashMap(data);
|
||||
Ok(HashMap::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_stats() -> anyhow::Result<HashMap<MapData, u32, Stats>> {
|
||||
let data = MapData::from_pin(format!("{}/STATS", PIN_PATH))?;
|
||||
let map = aya::maps::Map::HashMap(data);
|
||||
Ok(HashMap::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_global_stats() -> anyhow::Result<Array<MapData, Stats>> {
|
||||
let data = MapData::from_pin(format!("{}/GLOBAL_STATS", PIN_PATH))?;
|
||||
let map = aya::maps::Map::Array(data);
|
||||
Ok(Array::try_from(map)?)
|
||||
}
|
||||
|
||||
pub fn open_events() -> anyhow::Result<RingBuf<MapData>> {
|
||||
let data = MapData::from_pin(format!("{}/EVENTS", PIN_PATH))?;
|
||||
let map = aya::maps::Map::RingBuf(data);
|
||||
Ok(RingBuf::try_from(map)?)
|
||||
}
|
||||
24
xdp-firewall/src/logger.rs
Normal file
24
xdp-firewall/src/logger.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use aya::maps::MapData;
|
||||
use tokio::io::unix::AsyncFd;
|
||||
|
||||
use xdp_firewall_common::FirewallEvent;
|
||||
|
||||
pub async fn run_logger<T: Borrow<MapData>>(
|
||||
mut ring_buf: aya::maps::RingBuf<T>,
|
||||
tx: tokio::sync::mpsc::Sender<FirewallEvent>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut async_fd = AsyncFd::new(ring_buf)?;
|
||||
loop {
|
||||
while let Some(item) = async_fd.get_mut().next() {
|
||||
let data: &[u8] = &item;
|
||||
if data.len() >= core::mem::size_of::<FirewallEvent>() {
|
||||
let ev = unsafe { *(data.as_ptr() as *const FirewallEvent) };
|
||||
let _ = tx.try_send(ev);
|
||||
}
|
||||
}
|
||||
let mut guard = async_fd.readable().await?;
|
||||
guard.clear_ready();
|
||||
}
|
||||
}
|
||||
159
xdp-firewall/src/main.rs
Normal file
159
xdp-firewall/src/main.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
mod cli;
|
||||
mod firewall;
|
||||
mod logger;
|
||||
mod maps;
|
||||
mod monitor;
|
||||
mod stats;
|
||||
|
||||
use aya::maps::lpm_trie::Key as LpmKey;
|
||||
use clap::Parser;
|
||||
use xdp_firewall_common::*;
|
||||
|
||||
use crate::cli::{ActionArg, Cli, Commands, RatelimitModeArg};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Load { iface, .. } => {
|
||||
let _handle = firewall::FirewallHandle::load(&iface)?;
|
||||
tracing::info!("XDP firewall loaded on {}", iface);
|
||||
tokio::signal::ctrl_c().await?;
|
||||
}
|
||||
Commands::Unload { iface } => {
|
||||
firewall::unload(&iface)?;
|
||||
tracing::info!("XDP firewall unloaded from {}", iface);
|
||||
}
|
||||
Commands::Monitor { iface } => {
|
||||
monitor::run(&iface).await?;
|
||||
}
|
||||
Commands::Block { ip, duration, .. } => {
|
||||
let ip = maps::parse_ipv4(&ip)?;
|
||||
let mut blocklist = firewall::open_blocklist()?;
|
||||
let expiry = if duration == 0 {
|
||||
u64::MAX
|
||||
} else {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)?
|
||||
.as_secs()
|
||||
+ duration
|
||||
};
|
||||
blocklist.insert(ip, expiry, 0)?;
|
||||
println!("Blocked {} until epoch {}", maps::fmt_ipv4(ip), expiry);
|
||||
}
|
||||
Commands::Unblock { ip, .. } => {
|
||||
let ip = maps::parse_ipv4(&ip)?;
|
||||
let mut blocklist = firewall::open_blocklist()?;
|
||||
blocklist.remove(&ip)?;
|
||||
println!("Unblocked {}", maps::fmt_ipv4(ip));
|
||||
}
|
||||
Commands::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action, .. } => {
|
||||
let mut rules = firewall::open_rules()?;
|
||||
let mut stats = firewall::open_stats()?;
|
||||
let rule_id = stats.iter().count() as u32;
|
||||
let key = RuleKey {
|
||||
src_ip: maps::parse_ipv4(&src_ip)?,
|
||||
dst_ip: maps::parse_ipv4(&dst_ip)?,
|
||||
src_port,
|
||||
dst_port,
|
||||
proto,
|
||||
_pad: [0; 3],
|
||||
};
|
||||
let value = RuleValue {
|
||||
action: action.as_u8(),
|
||||
rule_id,
|
||||
_pad: [0; 3],
|
||||
};
|
||||
rules.insert(key, value, 0)?;
|
||||
stats.insert(rule_id, Stats::default(), 0)?;
|
||||
println!("Added rule id={}", rule_id);
|
||||
}
|
||||
Commands::DelRule { id, .. } => {
|
||||
let mut rules = firewall::open_rules()?;
|
||||
let mut stats = firewall::open_stats()?;
|
||||
let target = rules.iter().find(|r| {
|
||||
if let Ok((_, v)) = r {
|
||||
v.rule_id == id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if let Some(Ok((k, _))) = target {
|
||||
let key = k;
|
||||
rules.remove(&key)?;
|
||||
let _ = stats.remove(&id);
|
||||
println!("Deleted rule id={}", id);
|
||||
} else {
|
||||
println!("Rule id={} not found", id);
|
||||
}
|
||||
}
|
||||
Commands::ListRules { .. } => {
|
||||
let rules = firewall::open_rules()?;
|
||||
let stats = firewall::open_stats()?;
|
||||
println!("{:<5} {:<15} {:<15} {:<6} {:<6} {:<6} {:<10} {:<12} {:<12}",
|
||||
"ID", "SRC_IP", "DST_IP", "PROTO", "SPORT", "DPORT", "ACTION", "PACKETS", "BYTES");
|
||||
for item in rules.iter() {
|
||||
let (k, v) = item?;
|
||||
let s = stats.get(&v.rule_id, 0).unwrap_or_default();
|
||||
let action = match v.action {
|
||||
ACTION_DROP => "drop",
|
||||
ACTION_PASS => "pass",
|
||||
ACTION_LOG => "log",
|
||||
_ => "?",
|
||||
};
|
||||
println!("{:<5} {:<15} {:<15} {:<6} {:<6} {:<6} {:<10} {:<12} {:<12}",
|
||||
v.rule_id,
|
||||
if k.src_ip == 0 { "*".to_string() } else { maps::fmt_ipv4(k.src_ip) },
|
||||
if k.dst_ip == 0 { "*".to_string() } else { maps::fmt_ipv4(k.dst_ip) },
|
||||
k.proto,
|
||||
if k.src_port == 0 { "*".to_string() } else { k.src_port.to_string() },
|
||||
if k.dst_port == 0 { "*".to_string() } else { k.dst_port.to_string() },
|
||||
action,
|
||||
s.packets,
|
||||
s.bytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
Commands::AddRange { cidr, action, .. } => {
|
||||
let net = cidr.parse::<ipnetwork::Ipv4Network>()?;
|
||||
let prefix = net.prefix();
|
||||
let addr = u32::from_be_bytes(net.network().octets());
|
||||
let mut ranges = firewall::open_ip_ranges()?;
|
||||
let key = LpmKey::new(prefix.into(), addr.to_be());
|
||||
ranges.insert(&key, action.as_u8(), 0)?;
|
||||
println!("Added range {}/{} -> {:?}", maps::fmt_ipv4(addr), prefix, action);
|
||||
}
|
||||
Commands::DelRange { cidr, .. } => {
|
||||
let net = cidr.parse::<ipnetwork::Ipv4Network>()?;
|
||||
let prefix = net.prefix();
|
||||
let addr = u32::from_be_bytes(net.network().octets());
|
||||
let mut ranges = firewall::open_ip_ranges()?;
|
||||
let key = LpmKey::new(prefix.into(), addr.to_be());
|
||||
ranges.remove(&key)?;
|
||||
println!("Deleted range {}/{}", maps::fmt_ipv4(addr), prefix);
|
||||
}
|
||||
Commands::SetRatelimit { rate, burst, ban_threshold, ban_duration, mode, .. } => {
|
||||
let mut config_map = firewall::open_rate_limit_config()?;
|
||||
let fixed_rate = rate * 1_000_000;
|
||||
let fixed_burst = burst * 1_000_000;
|
||||
let config = RateLimitConfig {
|
||||
rate_per_sec: fixed_rate,
|
||||
burst: fixed_burst,
|
||||
ban_threshold,
|
||||
ban_duration_sec: ban_duration,
|
||||
enabled: 1,
|
||||
_pad: [0; 3],
|
||||
};
|
||||
let key = match mode {
|
||||
RatelimitModeArg::Ip => RL_TYPE_IP,
|
||||
RatelimitModeArg::Flow => RL_TYPE_FLOW,
|
||||
};
|
||||
config_map.insert(key, config, 0)?;
|
||||
println!("Set rate limit: rate={}pps burst={} mode={:?}", rate, burst, mode);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
55
xdp-firewall/src/maps.rs
Normal file
55
xdp-firewall/src/maps.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use aya::{
|
||||
maps::{Array, HashMap, lpm_trie::{LpmTrie, Key as LpmKey}, RingBuf},
|
||||
Ebpf,
|
||||
};
|
||||
use xdp_firewall_common::*;
|
||||
|
||||
pub type BpfBlocklist<'a> = HashMap<&'a mut aya::maps::MapData, u32, u64>;
|
||||
pub type BpfIpRanges<'a> = LpmTrie<&'a mut aya::maps::MapData, u32, u8>;
|
||||
pub type BpfRules<'a> = HashMap<&'a mut aya::maps::MapData, RuleKey, RuleValue>;
|
||||
pub type BpfRateLimitState<'a> = HashMap<&'a mut aya::maps::MapData, RateLimitKey, RateLimitState>;
|
||||
pub type BpfRateLimitConfig<'a> = HashMap<&'a mut aya::maps::MapData, u8, RateLimitConfig>;
|
||||
pub type BpfStats<'a> = HashMap<&'a mut aya::maps::MapData, u32, Stats>;
|
||||
pub type BpfGlobalStats<'a> = Array<&'a mut aya::maps::MapData, Stats>;
|
||||
pub type BpfEvents<'a> = RingBuf<&'a mut aya::maps::MapData>;
|
||||
|
||||
pub fn blocklist(bpf: &mut Ebpf) -> anyhow::Result<BpfBlocklist> {
|
||||
Ok(HashMap::try_from(bpf.map_mut("BLOCKLIST").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn ip_ranges(bpf: &mut Ebpf) -> anyhow::Result<BpfIpRanges> {
|
||||
Ok(LpmTrie::try_from(bpf.map_mut("IP_RANGES").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn rules(bpf: &mut Ebpf) -> anyhow::Result<BpfRules> {
|
||||
Ok(HashMap::try_from(bpf.map_mut("RULES").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn rate_limit_state(bpf: &mut Ebpf) -> anyhow::Result<BpfRateLimitState> {
|
||||
Ok(HashMap::try_from(bpf.map_mut("RATE_LIMIT_STATE").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn rate_limit_config(bpf: &mut Ebpf) -> anyhow::Result<BpfRateLimitConfig> {
|
||||
Ok(HashMap::try_from(bpf.map_mut("RATE_LIMIT_CONFIG").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn stats(bpf: &mut Ebpf) -> anyhow::Result<BpfStats> {
|
||||
Ok(HashMap::try_from(bpf.map_mut("STATS").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn global_stats(bpf: &mut Ebpf) -> anyhow::Result<BpfGlobalStats> {
|
||||
Ok(Array::try_from(bpf.map_mut("GLOBAL_STATS").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn events(bpf: &mut Ebpf) -> anyhow::Result<BpfEvents> {
|
||||
Ok(RingBuf::try_from(bpf.map_mut("EVENTS").unwrap())?)
|
||||
}
|
||||
|
||||
pub fn parse_ipv4(s: &str) -> anyhow::Result<u32> {
|
||||
let addr: std::net::Ipv4Addr = s.parse()?;
|
||||
Ok(u32::from_be_bytes(addr.octets()))
|
||||
}
|
||||
|
||||
pub fn fmt_ipv4(ip: u32) -> String {
|
||||
std::net::Ipv4Addr::from(ip).to_string()
|
||||
}
|
||||
84
xdp-firewall/src/monitor/app.rs
Normal file
84
xdp-firewall/src/monitor/app.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use xdp_firewall_common::{FirewallEvent, RuleKey, RuleValue, Stats};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Pane {
|
||||
Rules,
|
||||
Blocklist,
|
||||
Logs,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub iface: String,
|
||||
pub quit: bool,
|
||||
pub pane: Pane,
|
||||
pub global_stats: Stats,
|
||||
pub prev_global_stats: Stats,
|
||||
pub rules: Vec<(RuleKey, RuleValue, Stats)>,
|
||||
pub blocklist: Vec<(u32, u64)>,
|
||||
pub logs: VecDeque<FirewallEvent>,
|
||||
pub max_logs: usize,
|
||||
pub pps: u64,
|
||||
pub bps: u64,
|
||||
pub dialog: Dialog,
|
||||
pub rules_selected: usize,
|
||||
pub blocklist_selected: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
pub enum Dialog {
|
||||
#[default]
|
||||
None,
|
||||
BlockIp { input: String },
|
||||
AddRule { src_ip: String, dst_ip: String, proto: String, src_port: String, dst_port: String, action: String },
|
||||
ConfirmDelete,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(iface: String) -> Self {
|
||||
Self {
|
||||
iface,
|
||||
quit: false,
|
||||
pane: Pane::Rules,
|
||||
global_stats: Stats::default(),
|
||||
prev_global_stats: Stats::default(),
|
||||
rules: Vec::new(),
|
||||
blocklist: Vec::new(),
|
||||
logs: VecDeque::new(),
|
||||
max_logs: 100,
|
||||
pps: 0,
|
||||
bps: 0,
|
||||
dialog: Dialog::None,
|
||||
rules_selected: 0,
|
||||
blocklist_selected: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_tick(&mut self, global: Stats, rules: Vec<(RuleKey, RuleValue, Stats)>, blocklist: Vec<(u32, u64)>) {
|
||||
let elapsed = 0.1;
|
||||
let pkts = global.packets.saturating_sub(self.prev_global_stats.packets);
|
||||
let bytes = global.bytes.saturating_sub(self.prev_global_stats.bytes);
|
||||
self.pps = (pkts as f64 / elapsed) as u64;
|
||||
self.bps = (bytes as f64 / elapsed) as u64;
|
||||
self.prev_global_stats = self.global_stats;
|
||||
self.global_stats = global;
|
||||
self.rules = rules;
|
||||
self.blocklist = blocklist;
|
||||
}
|
||||
|
||||
pub fn add_log(&mut self, ev: FirewallEvent) {
|
||||
if self.logs.len() >= self.max_logs {
|
||||
self.logs.pop_back();
|
||||
}
|
||||
self.logs.push_front(ev);
|
||||
}
|
||||
|
||||
pub fn selected_rule_id(&self) -> Option<u32> {
|
||||
self.rules.get(self.rules_selected).map(|(_, v, _)| v.rule_id)
|
||||
}
|
||||
|
||||
pub fn selected_block_ip(&self) -> Option<u32> {
|
||||
self.blocklist.get(self.blocklist_selected).map(|(ip, _)| *ip)
|
||||
}
|
||||
}
|
||||
260
xdp-firewall/src/monitor/mod.rs
Normal file
260
xdp-firewall/src/monitor/mod.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
mod app;
|
||||
mod ui;
|
||||
|
||||
use std::{
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::{Backend, CrosstermBackend},
|
||||
Terminal,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
firewall, logger, maps,
|
||||
monitor::app::{App, Dialog, Pane},
|
||||
};
|
||||
use xdp_firewall_common::{FirewallEvent, Stats};
|
||||
|
||||
pub async fn run(iface: &str) -> anyhow::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let res = run_app(&mut terminal, iface.to_string()).await;
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
async fn run_app<B: Backend>(terminal: &mut Terminal<B>, iface: String) -> anyhow::Result<()> {
|
||||
let mut app = App::new(iface.clone());
|
||||
let (log_tx, mut log_rx) = mpsc::channel::<FirewallEvent>(1024);
|
||||
|
||||
let ring_buf = firewall::open_events()?;
|
||||
tokio::spawn(async move {
|
||||
let _ = logger::run_logger(ring_buf, log_tx).await;
|
||||
});
|
||||
|
||||
let mut last_tick = Instant::now();
|
||||
let tick_rate = Duration::from_millis(100);
|
||||
|
||||
loop {
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
handle_input(&mut app, key.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(ev) = log_rx.try_recv() {
|
||||
app.add_log(ev);
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
let (global, rules, blocklist) = poll_maps()?;
|
||||
app.on_tick(global, rules, blocklist);
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
|
||||
terminal.draw(|f| ui::draw(f, &app))?;
|
||||
|
||||
if app.quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_maps() -> anyhow::Result<(Stats, Vec<(xdp_firewall_common::RuleKey, xdp_firewall_common::RuleValue, Stats)>, Vec<(u32, u64)>)> {
|
||||
let global = firewall::open_global_stats()?.get(&0, 0).unwrap_or_default();
|
||||
let rules_map = firewall::open_rules()?;
|
||||
let stats_map = firewall::open_stats()?;
|
||||
let mut rules = Vec::new();
|
||||
for item in rules_map.iter() {
|
||||
let (k, v) = item?;
|
||||
let s = stats_map.get(&v.rule_id, 0).unwrap_or_default();
|
||||
rules.push((k, v, s));
|
||||
}
|
||||
let blocklist_map = firewall::open_blocklist()?;
|
||||
let mut blocklist = Vec::new();
|
||||
for item in blocklist_map.iter() {
|
||||
let (k, v) = item?;
|
||||
blocklist.push((k, v));
|
||||
}
|
||||
Ok((global, rules, blocklist))
|
||||
}
|
||||
|
||||
fn handle_input(app: &mut App, key: KeyCode) {
|
||||
match &mut app.dialog {
|
||||
Dialog::None => match key {
|
||||
KeyCode::Char('q') => app.quit = true,
|
||||
KeyCode::Tab => {
|
||||
app.pane = match app.pane {
|
||||
Pane::Rules => Pane::Blocklist,
|
||||
Pane::Blocklist => Pane::Logs,
|
||||
Pane::Logs => Pane::Rules,
|
||||
};
|
||||
}
|
||||
KeyCode::Down => match app.pane {
|
||||
Pane::Rules => {
|
||||
if app.rules_selected + 1 < app.rules.len() {
|
||||
app.rules_selected += 1;
|
||||
}
|
||||
}
|
||||
Pane::Blocklist => {
|
||||
if app.blocklist_selected + 1 < app.blocklist.len() {
|
||||
app.blocklist_selected += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Up => match app.pane {
|
||||
Pane::Rules => {
|
||||
if app.rules_selected > 0 {
|
||||
app.rules_selected -= 1;
|
||||
}
|
||||
}
|
||||
Pane::Blocklist => {
|
||||
if app.blocklist_selected > 0 {
|
||||
app.blocklist_selected -= 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char('b') => {
|
||||
app.dialog = Dialog::BlockIp { input: String::new() };
|
||||
}
|
||||
KeyCode::Char('u') => {
|
||||
if app.pane == Pane::Blocklist {
|
||||
if let Some(ip) = app.selected_block_ip() {
|
||||
let _ = unblock_ip(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') => {
|
||||
app.dialog = Dialog::AddRule {
|
||||
src_ip: "0.0.0.0".into(),
|
||||
dst_ip: "0.0.0.0".into(),
|
||||
proto: "0".into(),
|
||||
src_port: "0".into(),
|
||||
dst_port: "0".into(),
|
||||
action: "drop".into(),
|
||||
};
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
if app.pane == Pane::Rules && !app.rules.is_empty() {
|
||||
app.dialog = Dialog::ConfirmDelete;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Dialog::BlockIp { input } => match key {
|
||||
KeyCode::Enter => {
|
||||
if !input.is_empty() {
|
||||
if let Ok(ip) = input.parse::<std::net::Ipv4Addr>() {
|
||||
let _ = block_ip(u32::from_be_bytes(ip.octets()));
|
||||
}
|
||||
}
|
||||
app.dialog = Dialog::None;
|
||||
}
|
||||
KeyCode::Esc => app.dialog = Dialog::None,
|
||||
KeyCode::Char(c) => input.push(c),
|
||||
KeyCode::Backspace => { input.pop(); }
|
||||
_ => {}
|
||||
},
|
||||
Dialog::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action } => match key {
|
||||
KeyCode::Enter => {
|
||||
let _ = add_rule_from_dialog(src_ip, dst_ip, proto, src_port, dst_port, action);
|
||||
app.dialog = Dialog::None;
|
||||
}
|
||||
KeyCode::Esc => app.dialog = Dialog::None,
|
||||
_ => {}
|
||||
},
|
||||
Dialog::ConfirmDelete => match key {
|
||||
KeyCode::Enter => {
|
||||
if let Some(id) = app.selected_rule_id() {
|
||||
let _ = delete_rule(id);
|
||||
}
|
||||
app.dialog = Dialog::None;
|
||||
}
|
||||
KeyCode::Esc => app.dialog = Dialog::None,
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn block_ip(ip: u32) -> anyhow::Result<()> {
|
||||
let mut blocklist = firewall::open_blocklist()?;
|
||||
blocklist.insert(ip, u64::MAX, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unblock_ip(ip: u32) -> anyhow::Result<()> {
|
||||
let mut blocklist = firewall::open_blocklist()?;
|
||||
blocklist.remove(&ip)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_rule_from_dialog(src_ip: &str, dst_ip: &str, proto: &str, src_port: &str, dst_port: &str, action: &str) -> anyhow::Result<()> {
|
||||
use xdp_firewall_common::*;
|
||||
let mut rules = firewall::open_rules()?;
|
||||
let mut stats = firewall::open_stats()?;
|
||||
let rule_id = stats.iter().count() as u32;
|
||||
let key = RuleKey {
|
||||
src_ip: maps::parse_ipv4(src_ip)?,
|
||||
dst_ip: maps::parse_ipv4(dst_ip)?,
|
||||
src_port: src_port.parse()?,
|
||||
dst_port: dst_port.parse()?,
|
||||
proto: proto.parse()?,
|
||||
_pad: [0; 3],
|
||||
};
|
||||
let action_u8 = match action {
|
||||
"pass" => ACTION_PASS,
|
||||
"log" => ACTION_LOG,
|
||||
_ => ACTION_DROP,
|
||||
};
|
||||
let value = RuleValue {
|
||||
action: action_u8,
|
||||
rule_id,
|
||||
_pad: [0; 3],
|
||||
};
|
||||
rules.insert(key, value, 0)?;
|
||||
stats.insert(rule_id, Stats::default(), 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_rule(id: u32) -> anyhow::Result<()> {
|
||||
let mut rules = firewall::open_rules()?;
|
||||
let mut stats = firewall::open_stats()?;
|
||||
let target = rules.iter().find(|r| {
|
||||
if let Ok((_, v)) = r {
|
||||
v.rule_id == id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if let Some(Ok((k, _))) = target {
|
||||
let key = k;
|
||||
rules.remove(&key)?;
|
||||
let _ = stats.remove(&id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
203
xdp-firewall/src/monitor/ui.rs
Normal file
203
xdp-firewall/src/monitor/ui.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Margin, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, Borders, Cell, Clear, Paragraph, Row, Table, Wrap},
|
||||
Frame,
|
||||
};
|
||||
|
||||
use crate::maps::fmt_ipv4;
|
||||
use crate::monitor::app::{App, Dialog, Pane};
|
||||
use xdp_firewall_common::*;
|
||||
|
||||
pub fn draw(f: &mut Frame, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(f.area());
|
||||
|
||||
draw_header(f, app, chunks[0]);
|
||||
draw_main(f, app, chunks[1]);
|
||||
draw_footer(f, app, chunks[2]);
|
||||
|
||||
if app.dialog != Dialog::None {
|
||||
draw_dialog(f, app);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_header(f: &mut Frame, app: &App, area: Rect) {
|
||||
let header = Paragraph::new(format!(
|
||||
"Interface: {} | Global Packets: {} | Global Bytes: {} | PPS: {} | BPS: {}",
|
||||
app.iface, app.global_stats.packets, app.global_stats.bytes, app.pps, app.bps
|
||||
))
|
||||
.block(Block::default().borders(Borders::ALL).title("XDP Firewall Monitor"))
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(header, area);
|
||||
}
|
||||
|
||||
fn draw_main(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area);
|
||||
|
||||
draw_rules(f, app, chunks[0]);
|
||||
draw_right(f, app, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_rules(f: &mut Frame, app: &App, area: Rect) {
|
||||
let border_style = if app.pane == Pane::Rules {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let block = Block::default().borders(Borders::ALL).title("Rules").border_style(border_style);
|
||||
let header = Row::new(vec!["ID", "SRC", "DST", "P", "SP", "DP", "ACT", "PKTS", "BYTES"])
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
let rows: Vec<Row> = app.rules.iter().enumerate().map(|(i, (k, v, s))| {
|
||||
let style = if app.pane == Pane::Rules && i == app.rules_selected {
|
||||
Style::default().bg(Color::DarkGray)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let action = match v.action {
|
||||
ACTION_DROP => "DR",
|
||||
ACTION_PASS => "PS",
|
||||
ACTION_LOG => "LG",
|
||||
_ => "?",
|
||||
};
|
||||
Row::new(vec![
|
||||
Cell::from(v.rule_id.to_string()),
|
||||
Cell::from(if k.src_ip == 0 { "*".into() } else { fmt_ipv4(k.src_ip) }),
|
||||
Cell::from(if k.dst_ip == 0 { "*".into() } else { fmt_ipv4(k.dst_ip) }),
|
||||
Cell::from(k.proto.to_string()),
|
||||
Cell::from(if k.src_port == 0 { "*".into() } else { k.src_port.to_string() }),
|
||||
Cell::from(if k.dst_port == 0 { "*".into() } else { k.dst_port.to_string() }),
|
||||
Cell::from(action),
|
||||
Cell::from(s.packets.to_string()),
|
||||
Cell::from(s.bytes.to_string()),
|
||||
]).style(style)
|
||||
}).collect();
|
||||
let table = Table::new(rows, [
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(4),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(10),
|
||||
Constraint::Length(10),
|
||||
])
|
||||
.header(header)
|
||||
.block(block);
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn draw_right(f: &mut Frame, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
|
||||
.split(area);
|
||||
|
||||
draw_blocklist(f, app, chunks[0]);
|
||||
draw_logs(f, app, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_blocklist(f: &mut Frame, app: &App, area: Rect) {
|
||||
let border_style = if app.pane == Pane::Blocklist {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let block = Block::default().borders(Borders::ALL).title("Blocklist").border_style(border_style);
|
||||
let rows: Vec<Row> = app.blocklist.iter().enumerate().map(|(i, (ip, expiry))| {
|
||||
let style = if app.pane == Pane::Blocklist && i == app.blocklist_selected {
|
||||
Style::default().bg(Color::DarkGray)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let exp = if *expiry == u64::MAX {
|
||||
"permanent".into()
|
||||
} else {
|
||||
expiry.to_string()
|
||||
};
|
||||
Row::new(vec![fmt_ipv4(*ip), exp]).style(style)
|
||||
}).collect();
|
||||
let table = Table::new(rows, [Constraint::Length(16), Constraint::Min(0)])
|
||||
.header(Row::new(vec!["IP", "Expiry"]).style(Style::default().fg(Color::Yellow)))
|
||||
.block(block);
|
||||
f.render_widget(table, area);
|
||||
}
|
||||
|
||||
fn draw_logs(f: &mut Frame, app: &App, area: Rect) {
|
||||
let border_style = if app.pane == Pane::Logs {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
let block = Block::default().borders(Borders::ALL).title("Logs").border_style(border_style);
|
||||
let lines: Vec<Line> = app.logs.iter().map(|ev| {
|
||||
let action = match ev.action {
|
||||
ACTION_DROP => "DROP",
|
||||
ACTION_PASS => "PASS",
|
||||
ACTION_LOG => "LOG",
|
||||
ACTION_RATELIMIT_DROP => "RL_DROP",
|
||||
ACTION_BLOCKLIST_DROP => "BL_DROP",
|
||||
ACTION_RANGE_DROP => "RNG_DROP",
|
||||
_ => "?",
|
||||
};
|
||||
Line::from(vec![
|
||||
Span::raw(format!("{} ", action)),
|
||||
Span::raw(format!("{}:{} -> {}:{} proto={}",
|
||||
fmt_ipv4(ev.src_ip), ev.src_port,
|
||||
fmt_ipv4(ev.dst_ip), ev.dst_port, ev.proto)),
|
||||
])
|
||||
}).collect();
|
||||
let para = Paragraph::new(Text::from(lines)).block(block).wrap(Wrap { trim: true });
|
||||
f.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn draw_footer(f: &mut Frame, _app: &App, area: Rect) {
|
||||
let text = "q:quit | Tab:switch | b:block | u:unblock | a:add-rule | d:del-rule";
|
||||
let para = Paragraph::new(text).alignment(Alignment::Center);
|
||||
f.render_widget(para, area);
|
||||
}
|
||||
|
||||
fn draw_dialog(f: &mut Frame, app: &App) {
|
||||
let area = centered_rect(60, 40, f.area());
|
||||
f.render_widget(Clear, area);
|
||||
match &app.dialog {
|
||||
Dialog::BlockIp { input } => {
|
||||
let block = Block::default().borders(Borders::ALL).title("Block IP");
|
||||
let para = Paragraph::new(format!("IP: {}", input)).block(block);
|
||||
f.render_widget(para, area);
|
||||
}
|
||||
Dialog::AddRule { src_ip, dst_ip, proto, src_port, dst_port, action } => {
|
||||
let block = Block::default().borders(Borders::ALL).title("Add Rule");
|
||||
let text = format!(
|
||||
"src_ip: {}\ndst_ip: {}\nproto: {}\nsrc_port: {}\ndst_port: {}\naction: {}",
|
||||
src_ip, dst_ip, proto, src_port, dst_port, action
|
||||
);
|
||||
let para = Paragraph::new(text).block(block);
|
||||
f.render_widget(para, area);
|
||||
}
|
||||
Dialog::ConfirmDelete => {
|
||||
let block = Block::default().borders(Borders::ALL).title("Confirm Delete");
|
||||
let para = Paragraph::new("Press Enter to confirm, Esc to cancel").block(block).alignment(Alignment::Center);
|
||||
f.render_widget(para, area);
|
||||
}
|
||||
Dialog::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage((100 - percent_y) / 2), Constraint::Percentage(percent_y), Constraint::Percentage((100 - percent_y) / 2)])
|
||||
.split(r);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage((100 - percent_x) / 2), Constraint::Percentage(percent_x), Constraint::Percentage((100 - percent_x) / 2)])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
15
xdp-firewall/src/stats.rs
Normal file
15
xdp-firewall/src/stats.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use xdp_firewall_common::Stats;
|
||||
|
||||
pub struct Rate {
|
||||
pub pps: u64,
|
||||
pub bps: u64,
|
||||
}
|
||||
|
||||
pub fn compute_rate(now: Stats, last: Stats, elapsed_secs: f64) -> Rate {
|
||||
let pkts = now.packets.saturating_sub(last.packets);
|
||||
let bytes = now.bytes.saturating_sub(last.bytes);
|
||||
Rate {
|
||||
pps: (pkts as f64 / elapsed_secs) as u64,
|
||||
bps: (bytes as f64 / elapsed_secs) as u64,
|
||||
}
|
||||
}
|
||||
9
xtask/Cargo.toml
Normal file
9
xtask/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
duct = "0.13"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
51
xtask/src/main.rs
Normal file
51
xtask/src/main.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use std::{path::PathBuf, process::Command};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(about = "Build helpers for xdp-firewall")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
BuildEbpf,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::BuildEbpf => build_ebpf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ebpf() -> anyhow::Result<()> {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
|
||||
let ebpf_dir = root.join("xdp-firewall-ebpf");
|
||||
let target_dir = root.join("target");
|
||||
|
||||
let status = Command::new("rustup")
|
||||
.args([
|
||||
"run",
|
||||
"nightly",
|
||||
"cargo",
|
||||
"build",
|
||||
"-Zbuild-std=core",
|
||||
"--package",
|
||||
"xdp-firewall-ebpf",
|
||||
"--target",
|
||||
"bpfel-unknown-none",
|
||||
"--release",
|
||||
])
|
||||
.current_dir(&ebpf_dir)
|
||||
.env("CARGO_TARGET_DIR", &target_dir)
|
||||
.status()?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("eBPF build failed");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user