From 28edb488c63046f2005fc55915b070a4defac57e Mon Sep 17 00:00:00 2001 From: ThisSeanZhang Date: Sun, 27 Oct 2024 19:23:15 -0700 Subject: [PATCH] example: Add eBPF-based Netfilter blocklist example with Rust control program --- Cargo.lock | 12 ++ Cargo.toml | 1 + examples/netfilter_blocklist/Cargo.toml | 15 +++ examples/netfilter_blocklist/LICENSE | 1 + .../netfilter_blocklist/LICENSE.BSD-2-Clause | 1 + examples/netfilter_blocklist/LICENSE.LGPL-2.1 | 1 + examples/netfilter_blocklist/README.md | 57 ++++++++++ examples/netfilter_blocklist/build.rs | 26 +++++ .../src/bpf/netfilter_blocklist.bpf.c | 62 ++++++++++ examples/netfilter_blocklist/src/main.rs | 107 ++++++++++++++++++ 10 files changed, 283 insertions(+) create mode 100644 examples/netfilter_blocklist/Cargo.toml create mode 120000 examples/netfilter_blocklist/LICENSE create mode 120000 examples/netfilter_blocklist/LICENSE.BSD-2-Clause create mode 120000 examples/netfilter_blocklist/LICENSE.LGPL-2.1 create mode 100644 examples/netfilter_blocklist/README.md create mode 100644 examples/netfilter_blocklist/build.rs create mode 100644 examples/netfilter_blocklist/src/bpf/netfilter_blocklist.bpf.c create mode 100644 examples/netfilter_blocklist/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 17a62f7a..4907b052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,18 @@ dependencies = [ "autocfg", ] +[[package]] +name = "netfilter_blocklist" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "ctrlc", + "libbpf-cargo", + "libbpf-rs", + "plain", +] + [[package]] name = "nix" version = "0.28.0" diff --git a/Cargo.toml b/Cargo.toml index 4ce100af..726587cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,5 +18,6 @@ members = [ "examples/tcp_ca", "examples/tcp_option", "examples/tproxy", + "examples/netfilter_blocklist", ] resolver = "2" diff --git a/examples/netfilter_blocklist/Cargo.toml b/examples/netfilter_blocklist/Cargo.toml new file mode 100644 index 00000000..f3e2779b --- /dev/null +++ b/examples/netfilter_blocklist/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "netfilter_blocklist" +version = "0.1.0" +edition.workspace = true +license = "LGPL-2.1-only OR BSD-2-Clause" + +[build-dependencies] +libbpf-cargo = { path = "../../libbpf-cargo" } + +[dependencies] +anyhow = "1.0" +clap = { version = "4.0.32", default-features = false, features = ["std", "derive", "help", "usage"] } +ctrlc = "3.2" +libbpf-rs = { path = "../../libbpf-rs" } +plain = "0.2" diff --git a/examples/netfilter_blocklist/LICENSE b/examples/netfilter_blocklist/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/examples/netfilter_blocklist/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/examples/netfilter_blocklist/LICENSE.BSD-2-Clause b/examples/netfilter_blocklist/LICENSE.BSD-2-Clause new file mode 120000 index 00000000..5eaca90a --- /dev/null +++ b/examples/netfilter_blocklist/LICENSE.BSD-2-Clause @@ -0,0 +1 @@ +../../LICENSE.BSD-2-Clause \ No newline at end of file diff --git a/examples/netfilter_blocklist/LICENSE.LGPL-2.1 b/examples/netfilter_blocklist/LICENSE.LGPL-2.1 new file mode 120000 index 00000000..b5999a01 --- /dev/null +++ b/examples/netfilter_blocklist/LICENSE.LGPL-2.1 @@ -0,0 +1 @@ +../../LICENSE.LGPL-2.1 \ No newline at end of file diff --git a/examples/netfilter_blocklist/README.md b/examples/netfilter_blocklist/README.md new file mode 100644 index 00000000..5a862d76 --- /dev/null +++ b/examples/netfilter_blocklist/README.md @@ -0,0 +1,57 @@ +# eBPF Netfilter Blocklist Example + +This project demonstrates how to use eBPF and Rust to implement a Netfilter hook that blocks specific IPv4 traffic based on a blacklist of IP addresses. The eBPF program uses an LPM Trie map to efficiently store and lookup IP addresses, and the Rust program handles the loading, configuration, and management of the eBPF program. + + +## ⚠ Requirements ⚠ + +Linux kernel [version 6.4 or later](https://github.com/torvalds/linux/commit/84601d6ee68ae820dec97450934797046d62db4b) with eBPF support. + +```shell +bpftool btf dump file /sys/kernel/btf/vmlinux format c > ./examples/netfilter_blocklist/src/bpf/vmlinux.h +``` + +## Building + +```shell +$ cargo build +``` + +## Usage + +```shell +$ sudo ./target/release/netfilter_blocklist --block-ip --value --verbose +``` + +## Trigger + +```shell +# Start +$ sudo ./target/release/netfilter_blocklist --block-ip 1.1.1.1 --value 42 --verbose + +# Trigger using curl +curl 1.1.1.1 +``` + +## Output + +```shell +$ sudo cat /sys/kernel/debug/tracing/trace_pipe +``` +```text + curl-106738 [001] ...1. 74215.769718: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [001] ..s3. 74216.785447: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [001] ..s3. 74217.801426: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [002] ..s3. 74218.825369: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [000] ..s3. 74219.849344: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [000] ..s3. 74220.873297: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [003] ..s3. 74222.889199: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 + + -0 [001] ..s3. 74227.145032: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42 +``` diff --git a/examples/netfilter_blocklist/build.rs b/examples/netfilter_blocklist/build.rs new file mode 100644 index 00000000..39b08726 --- /dev/null +++ b/examples/netfilter_blocklist/build.rs @@ -0,0 +1,26 @@ +use std::env; +use std::ffi::OsStr; +use std::path::PathBuf; + +use libbpf_cargo::SkeletonBuilder; + +const SRC: &str = "src/bpf/netfilter_blocklist.bpf.c"; + +fn main() { + let out = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR must be set in build script"), + ) + .join("src") + .join("bpf") + .join("netfilter_blocklist.skel.rs"); + + SkeletonBuilder::new() + .source(SRC) + .clang_args([ + OsStr::new("-Wno-compare-distinct-pointer-types"), + OsStr::new("-I"), + ]) + .build_and_generate(&out) + .unwrap(); + println!("cargo:rerun-if-changed={SRC}"); +} diff --git a/examples/netfilter_blocklist/src/bpf/netfilter_blocklist.bpf.c b/examples/netfilter_blocklist/src/bpf/netfilter_blocklist.bpf.c new file mode 100644 index 00000000..73a1e22e --- /dev/null +++ b/examples/netfilter_blocklist/src/bpf/netfilter_blocklist.bpf.c @@ -0,0 +1,62 @@ +#include "vmlinux.h" +#include +#include + +#define NF_DROP 0 +#define NF_ACCEPT 1 + +int bpf_dynptr_from_skb(struct sk_buff *skb, + __u64 flags, struct bpf_dynptr *ptr__uninit) __ksym; +void *bpf_dynptr_slice(const struct bpf_dynptr *ptr, + uint32_t offset, void *buffer, uint32_t buffer__sz) __ksym; + + +struct lpm_key { + __u32 prefixlen; + __be32 addr; +}; + +struct { + __uint(type, BPF_MAP_TYPE_LPM_TRIE); + __type(key, struct lpm_key); + __type(value, __u32); + __uint(map_flags, BPF_F_NO_PREALLOC); + __uint(max_entries, 200); +} block_ips SEC(".maps"); + +SEC("netfilter") +int netfilter_local_in(struct bpf_nf_ctx *ctx) { + + struct sk_buff *skb = ctx->skb; + struct bpf_dynptr ptr; + struct iphdr *p, iph = {}; + struct lpm_key key; + __u32 *match_value; + + if (skb->len <= 20 || bpf_dynptr_from_skb(skb, 0, &ptr)) + return NF_ACCEPT; + p = bpf_dynptr_slice(&ptr, 0, &iph, sizeof(iph)); + if (!p) + return NF_ACCEPT; + + /* ip4 only */ + if (p->version != 4) + return NF_ACCEPT; + + /* search p->daddr in trie */ + key.prefixlen = 32; + key.addr = p->daddr; + match_value = bpf_map_lookup_elem(&block_ips, &key); + if (match_value) { + /* To view log output, use: cat /sys/kernel/debug/tracing/trace_pipe */ + __be32 addr_host = bpf_ntohl(key.addr); + bpf_printk("Blocked IP: %d.%d.%d.%d, prefix length: %d, map value: %d\n", + (addr_host >> 24) & 0xFF, (addr_host >> 16) & 0xFF, + (addr_host >> 8) & 0xFF, addr_host & 0xFF, + key.prefixlen, *match_value); + return NF_DROP; + } + return NF_ACCEPT; +} + +char _license[] SEC("license") = "GPL"; diff --git a/examples/netfilter_blocklist/src/main.rs b/examples/netfilter_blocklist/src/main.rs new file mode 100644 index 00000000..46e1f3c2 --- /dev/null +++ b/examples/netfilter_blocklist/src/main.rs @@ -0,0 +1,107 @@ +use std::mem::MaybeUninit; +use std::net::Ipv4Addr; +use std::str::FromStr; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::thread::sleep; +use std::time::Duration; + +use anyhow::Result; +use clap::Parser; + +use libbpf_rs::skel::OpenSkel; +use libbpf_rs::skel::SkelBuilder; +use libbpf_rs::ErrorExt; +use libbpf_rs::MapCore; +use libbpf_rs::MapFlags; +use libbpf_rs::NetfilterOpts; +use libbpf_rs::NFPROTO_IPV4; +use libbpf_rs::NF_INET_LOCAL_OUT; + +mod netfilter { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/bpf/netfilter_blocklist.skel.rs" + )); +} + +use netfilter::*; + +/// Netfilter Blocklist Example +/// +/// Drop specified IP packets in netfilter hook +#[derive(Debug, Parser)] +struct Command { + /// Add the specified IP to the blocked IP list + #[arg(long, value_parser, default_value = "1.1.1.1")] + block_ip: String, + + /// show the value in the debug info + #[arg(long, value_parser, default_value = "42")] + value: u32, + + /// Verbose debug output + #[arg(short, long)] + verbose: bool, +} + +fn main() -> Result<()> { + let opts = Command::parse(); + + // Install Ctrl-C handler + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + })?; + + let mut skel_builder = NetfilterBlocklistSkelBuilder::default(); + + if opts.verbose { + skel_builder.obj_builder.debug(true); + } + + // Set constants + let mut open_object = MaybeUninit::uninit(); + let open_skel = skel_builder.open(&mut open_object)?; + + // Load into kernel + let skel = open_skel.load()?; + + let block_ip_key = types::lpm_key { + prefixlen: (32 as u32), + addr: Ipv4Addr::from_str(&opts.block_ip)?.to_bits().to_be(), + }; + + let block_ip_key = unsafe { plain::as_bytes(&block_ip_key) }; + let value = opts.value; + + skel.maps + .block_ips + .update(block_ip_key, &value.to_le_bytes(), MapFlags::ANY) + .context("update new record to map fail")?; + + + let local_in_netfilter_opt = NetfilterOpts { + pf: NFPROTO_IPV4, + hooknum: NF_INET_LOCAL_OUT, + priority: -128, + ..NetfilterOpts::default() + }; + + let local_in_link = skel + .progs + .netfilter_local_in + .attach_netfilter(local_in_netfilter_opt) + .unwrap(); + + // Block until SIGINT + while running.load(Ordering::SeqCst) { + sleep(Duration::new(1, 0)); + } + + local_in_link.detach().unwrap(); + + Ok(()) +}