环境搭建 笔者这里是 Ubuntu 22.04 。
rust环境 1 2 rustup install stable rustup toolchain install nightly --component rust-src
bpflinker 安装bpflinker
1 cargo install bpf-linker
安装cargo generate 用于生成模板项目。
1 cargo install cargo-generate
生成模板项目 1 cargo generate https://github.com/aya-rs/aya-template
如果和我一样配置了git的ssh代理,那可能会不幸遇到这样的错误
1 2 3 4 5 ⚠️ Favorite `https://github.com/aya-rs/aya-template` not found in config, using it as a git repository: https://github.com/aya-rs/aya-template Error: Please check if the Git user / repository exists. Caused by: unknown http scheme 'socks5'; class=Http (34)
看起来cargo generate并不支持这种代理。
这种情况下,只需要将https://github.com/aya-rs/aya-template clone 到本地,然后
1 cargo generate -p aya-template
即可。
输入项目名后,我们可以选择众多,bpf程序类型的模板 ,这里面有用于内核追踪的,流控的,xdp等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 🔧 Destination: /home/fenix/rust/asdf ... 🔧 project-name: asdf ... 🔧 Generating template ... ? 🤷 Which type of eBPF program? › cgroup_skb cgroup_sockopt cgroup_sysctl classifier fentry fexit kprobe kretprobe lsm perf_event raw_tracepoint sk_msg sock_ops socket_filter tp_btf tracepoint uprobe uretprobe ❯ xdp
我们先选择xdp试试:
xdp 选择xdp后,我们可以看到生成的项目结构(我这里项目名为xdp-bpf-demo)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ├── Cargo.toml ├── README.md ├── xdp-bpf-demo │ ├── Cargo.toml │ └── src │ └── main.rs ├── xdp-bpf-demo-common │ ├── Cargo.toml │ └── src │ └── lib.rs ├── xdp-bpf-demo-ebpf │ ├── Cargo.toml │ ├── rust-toolchain.toml │ └── src │ └── main.rs └── xtask ├── Cargo.toml └── src ├── build_ebpf.rs ├── main.rs └── run.rs
其中xdp-bpf-demo-common
是公共模块,xtask
构建脚本,xdp-bpf-demo-ebpf
是内核端,xdp-bpf-demo
是用户端。
cargo generate为我们生成的README中包含了我们需要的命令。 我们先跑起来测试下:
构建内核端
构建用户端
启动,这里iface后面的参数是你的网卡名
1 RUST_LOG=info cargo xtask run -- --iface wlp2s0
不出意外的话就能看到:
1 2 3 4 5 [2022-12-21T18:03:09Z INFO xdp_hello] Waiting for Ctrl-C... [2022-12-21T18:03:11Z INFO xdp_hello] received a packet [2022-12-21T18:03:11Z INFO xdp_hello] received a packet [2022-12-21T18:03:11Z INFO xdp_hello] received a packet [2022-12-21T18:03:11Z INFO xdp_hello] received a packet
现在我们回过头来看看项目里都有什么
内核端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #![no_std] #![no_main] use aya_bpf::{bindings::xdp_action, macros::xdp, programs::XdpContext};use aya_log_ebpf::info;#[xdp(name = "xdp_bpf_demo" )] pub fn xdp_bpf_demo (ctx: XdpContext) -> u32 { match try_xdp_bpf_demo (ctx) { Ok (ret) => ret, Err (_) => xdp_action::XDP_ABORTED, } } fn try_xdp_bpf_demo (ctx: XdpContext) -> Result <u32 , u32 > { info!(&ctx, "received a packet" ); Ok (xdp_action::XDP_PASS) } #[panic_handler] fn panic (_info: &core::panic::PanicInfo) -> ! { unsafe { core::hint::unreachable_unchecked () } }
可以看到几个信息,首先内核端 是非std环境,所以不能调用alloc和collections 这些,只能使用core中的API了。非std也意味着,eBPF内核端不能调用系统调用。
内核的数据必须通过Map 传到 用户端。
解包 在内核端中加入一个新crate用于解包
我们的模板项目中给出了这样一个方法:
1 2 3 4 5 6 pub fn xdp_bpf_demo (ctx: XdpContext) -> u32 { match try_xdp_bpf_demo (ctx) { Ok (ret) => ret, Err (_) => xdp_action::XDP_ABORTED, } }
XdpContext中有两个函数data() ,data_end() 我们需要将其转为指针,进一步解析。 我们引入这样一个函数,尝试将data()转为Rust裸指针。
1 2 3 4 5 6 7 8 9 10 11 12 #[inline(always)] fn ptr_at <T>(ctx: &XdpContext, offset: usize ) -> Result <*const T, ()> { let start = ctx.data (); let end = ctx.data_end (); let len = mem::size_of::<T>(); if start + offset + len > end { return Err (()); } Ok ((start + offset) as *const T) }
尝试转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 fn try_xdp_bpf_demo (ctx: XdpContext) -> Result <u32 , ()> { let ethhdr : *const EthHdr = ptr_at (&ctx, 0 )?; match unsafe { (*ethhdr).ether_type } { EtherType::Ipv4 => {} _ => return Ok (xdp_action::XDP_PASS), } let ipv4hdr : *const Ipv4Hdr = ptr_at (&ctx, EthHdr::LEN)?; let source_addr = u32 ::from_be (unsafe { (*ipv4hdr).src_addr }); let source_port = match unsafe { (*ipv4hdr).proto } { IpProto::Tcp => { let tcphdr : *const TcpHdr = ptr_at (&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; u16 ::from_be (unsafe { (*tcphdr).source }) } IpProto::Udp => { let udphdr : *const UdpHdr = ptr_at (&ctx, EthHdr::LEN + Ipv4Hdr::LEN)?; u16 ::from_be (unsafe { (*udphdr).source }) } _ => return Err (()), }; info!( &ctx, "SRC IP: {:ipv4}, SRC PORT: {}" , source_addr, source_port ); Ok (xdp_action::XDP_PASS) }
同样的方式编译执行,就可以看到抓包效果
1 2 3 4 5 6 7 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] Waiting for Ctrl-C... [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 192.168.31.202, SRC PORT: 54915 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 114.114.114.114, SRC PORT: 53 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 114.114.114.114, SRC PORT: 53 [2023-02-28T13:57:32Z INFO xdp_bpf_demo] SRC IP: 143.42.173.181, SRC PORT: 443
丢包 我们目前尝试了打印包,那么如何丢包呢?我们来通过*内核端和用户端的交互来实现一个防火墙,这个防火墙能够将黑名单内的ip的包丢弃 ,其余放通。
我们对我们的代码做些小改造,首先是内核端需要配置MAP
1 2 3 4 5 6 #[map(name = "BLOCKLIST" )] static mut BLOCKLIST: HashMap<u32 , u32 > = HashMap::<u32 , u32 >::with_max_entries (1024 , 0 );fn is_blockip (addr: u32 ) -> bool { unsafe { BLOCKLIST.get (&addr).is_some () } }
解包部分,如果在黑名单中就Drop,如果不在黑名单中则pass。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn try_xdp_bpf_demo (ctx: XdpContext) -> Result <u32 , ()> { let ethhdr : *const EthHdr = ptr_at (&ctx, 0 )?; match unsafe { (*ethhdr).ether_type } { EtherType::Ipv4 => {} _ => return Ok (xdp_action::XDP_PASS), } let ipv4hdr : *const Ipv4Hdr = ptr_at (&ctx, EthHdr::LEN)?; let source_addr = u32 ::from_be (unsafe { (*ipv4hdr).src_addr }); let action = if is_blockip (source_addr) { xdp_action::XDP_DROP } else { xdp_action::XDP_PASS }; info!(&ctx, "SRC: {:ipv4}, ACTION: {}" , source_addr, action); Ok (xdp_action::XDP_PASS) }
用户端部分也要改造,我们要将MAP配置到用户端中,才能实现在用户端中动态配置黑名单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 use std::net::Ipv4Addr;use anyhow::Context;use aya::maps::HashMap;use aya::programs::{Xdp, XdpFlags};use aya::{include_bytes_aligned, Bpf};use aya_log::BpfLogger;use clap::Parser;use log::{info, warn};use tokio::signal;#[derive(Debug, Parser)] struct Opt { #[clap(short, long, default_value = "wlp2s0" )] iface: String , } #[tokio::main] async fn main () -> Result <(), anyhow::Error> { let opt = Opt::parse (); env_logger::init (); #[cfg(debug_assertions)] let mut bpf = Bpf::load (include_bytes_aligned!( "../../target/bpfel-unknown-none/debug/xdp-bpf-demo" ))?; #[cfg(not(debug_assertions))] let mut bpf = Bpf::load (include_bytes_aligned!( "../../target/bpfel-unknown-none/release/xdp-bpf-demo" ))?; if let Err (e) = BpfLogger::init (&mut bpf) { warn!("failed to initialize eBPF logger: {}" , e); } let program : &mut Xdp = bpf.program_mut ("xdp_bpf_demo" ).unwrap ().try_into ()?; program.load ()?; program.attach (&opt.iface, XdpFlags::default ()) .context ("failed to attach the XDP program with default flags - try changing XdpFlags::default() to XdpFlags::SKB_MODE" )?; let mut block_list : HashMap<_, u32 , u32 > = HashMap::try_from (bpf.map_mut ("BLOCKLIST" )?)?; let block_addr : u32 = Ipv4Addr::new (110 , 242 , 68 , 66 ).try_into ()?; block_list.insert (block_addr, 0 , 0 )?; info!("Waiting for Ctrl-C..." ); signal::ctrl_c ().await ?; info!("Exiting..." ); Ok (()) }
这里固定配置了黑名单(baidu.com百度的ip 110.242.68.66),也可以设计为从stdin中读取更新,或者api。
同样 RUST_LOG=info cargo xtask run
然后就可以curl baidu.com
测试下 ,当然需要注意的是,如果你开了代理需要加上-no-proxy
参数,否则将会和本地ip,端口建立链接不会走黑名单。
1 2 3 4 5 6 7 8 9 10 11 12 [2023-02-28T16:36:12Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:13Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:14Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:15Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1 [2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1 [2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 110.242.68.66, ACTION: 1 [2023-02-28T16:36:16Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:17Z INFO xdp_bpf_demo] SRC: 114.38.97.176, ACTION: 2 [2023-02-28T16:36:17Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2 [2023-02-28T16:36:18Z INFO xdp_bpf_demo] SRC: 143.42.173.181, ACTION: 2 [2023-02-28T16:36:18Z INFO xdp_bpf_demo] SRC: 192.168.31.202, ACTION: 2