Skip to content

Commit

Permalink
example: Add eBPF-based Netfilter blocklist example with Rust control…
Browse files Browse the repository at this point in the history
… program
  • Loading branch information
ThisSeanZhang committed Nov 2, 2024
1 parent a392b91 commit 28edb48
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ members = [
"examples/tcp_ca",
"examples/tcp_option",
"examples/tproxy",
"examples/netfilter_blocklist",
]
resolver = "2"
15 changes: 15 additions & 0 deletions examples/netfilter_blocklist/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions examples/netfilter_blocklist/LICENSE
1 change: 1 addition & 0 deletions examples/netfilter_blocklist/LICENSE.BSD-2-Clause
1 change: 1 addition & 0 deletions examples/netfilter_blocklist/LICENSE.LGPL-2.1
57 changes: 57 additions & 0 deletions examples/netfilter_blocklist/README.md
Original file line number Diff line number Diff line change
@@ -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 <target ip> --value <show in debug> --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
<idle>-0 [001] ..s3. 74216.785447: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [001] ..s3. 74217.801426: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [002] ..s3. 74218.825369: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [000] ..s3. 74219.849344: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [000] ..s3. 74220.873297: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [003] ..s3. 74222.889199: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
<idle>-0 [001] ..s3. 74227.145032: bpf_trace_printk: Blocked IP: 1.1.1.1, prefix length: 32, map value: 42
```
26 changes: 26 additions & 0 deletions examples/netfilter_blocklist/build.rs
Original file line number Diff line number Diff line change
@@ -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}");
}
62 changes: 62 additions & 0 deletions examples/netfilter_blocklist/src/bpf/netfilter_blocklist.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

#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";
107 changes: 107 additions & 0 deletions examples/netfilter_blocklist/src/main.rs
Original file line number Diff line number Diff line change
@@ -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(())
}

0 comments on commit 28edb48

Please sign in to comment.